// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.rules.objc;

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.getFirstArtifactEndingWith;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.AbstractAction;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.ArtifactOwner;
import com.google.devtools.build.lib.actions.CommandAction;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.actions.ActionTemplate.ActionTemplateExpansionException;
import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.ApplePlatform.PlatformType;
import com.google.devtools.build.lib.rules.apple.AppleToolchain;
import com.google.devtools.build.lib.rules.apple.DottedVersion;
import com.google.devtools.build.lib.rules.cpp.CppCompileActionTemplate;
import com.google.devtools.build.lib.rules.cpp.CppModuleMapAction;
import com.google.devtools.build.lib.rules.cpp.UmbrellaHeaderAction;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.ByteArrayOutputStream;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Unit test for Java source file translation into ObjC in {@link
 * com.google.devtools.build.lib.rules.java.JavaLibrary} and translated file compilation and linking
 * in {@link ObjcBinary} and {@link J2ObjcLibrary}.
 */
@RunWith(JUnit4.class)
public class BazelJ2ObjcLibraryTest extends J2ObjcLibraryTest {

  /**
   * Gets the target with the given label, using the apple_binary multi-arch split transition with
   * the default version of iOS as the platform.
   */
  private ConfiguredTarget getConfiguredTargetInAppleBinaryTransition(String label)
      throws Exception {
    BuildConfiguration childConfig =
        Iterables.getOnlyElement(
            getSplitConfigurations(
                targetConfig,
                new MultiArchSplitTransitionProvider.AppleBinaryTransition(
                    PlatformType.IOS, Optional.<DottedVersion>absent())));
    return getConfiguredTarget(label, childConfig);
  }

  @Test
  public void testJ2ObjCInformationExportedFromJ2ObjcLibrary() throws Exception {
    ConfiguredTarget j2objcLibraryTarget = getConfiguredTarget(
        "//java/com/google/dummy/test:transpile");
    ObjcProvider provider = j2objcLibraryTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY))).containsExactly(
        "third_party/java/j2objc/libjre_core_lib.a",
        "java/com/google/dummy/test/libtest_j2objc.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER))).containsExactly(
        "third_party/java/j2objc/jre_core.h",
        "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h");

    String execPath = j2objcLibraryTarget.getConfiguration().getBinDirectory(RepositoryName.MAIN)
        .getExecPath() + "/";
    assertThat(
            Iterables.transform(
                provider.get(ObjcProvider.INCLUDE), PathFragment::getSafePathString))
        .containsExactly(execPath + "java/com/google/dummy/test/_j2objc/test");
  }

  @Test
  public void testJ2ObjCInformationExportedWithGeneratedJavaSources() throws Exception {
    scratch.file("java/com/google/test/in.txt");
    scratch.file("java/com/google/test/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "genrule(",
        "    name = 'dummy_gen',",
        "    srcs = ['in.txt'],",
        "    outs = ['test.java'],",
        "    cmd = 'dummy')",
        "",
        "java_library(",
        "    name = 'test',",
        "    srcs = [':test.java'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    deps = ['test'])");

    ConfiguredTarget target = getConfiguredTarget("//java/com/google/test:transpile");
    ObjcProvider provider = target.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    String genfilesFragment = target.getConfiguration().getGenfilesFragment().toString();
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY))).containsExactly(
        "third_party/java/j2objc/libjre_core_lib.a",
        "java/com/google/test/libtest_j2objc.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER))).containsExactly(
        "third_party/java/j2objc/jre_core.h",
        "java/com/google/test/_j2objc/test/" + genfilesFragment + "/java/com/google/test/test.h");

    String execPath = target.getConfiguration().getBinDirectory(RepositoryName.MAIN)
        .getExecPath() + "/";
    assertThat(
            Iterables.transform(
                provider.get(ObjcProvider.INCLUDE), PathFragment::getSafePathString))
        .containsExactly(
            execPath + "java/com/google/test/_j2objc/test/" + genfilesFragment,
            execPath + "java/com/google/test/_j2objc/test");
  }

  @Test
  public void testJ2ObjcProtoRuntimeLibraryAndHeadersExported() throws Exception {
    scratch.file("java/com/google/dummy/test/proto/test.java");
    scratch.file("java/com/google/dummy/test/proto/test.proto");
    scratch.file("java/com/google/dummy/test/proto/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "proto_library(",
        "    name = 'test_proto',",
        "    srcs = ['test.proto'],",
        "    java_api_version = 2,",
        "    j2objc_api_version = 1)",
        "",
        "java_library(",
        "    name = 'test',",
        "    srcs = ['test.java'],",
        "    deps = [':test_proto'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    deps = ['test'])");

    ConfiguredTarget j2objcLibraryTarget = getConfiguredTarget(
        "//java/com/google/dummy/test/proto:transpile");
    ObjcProvider provider = j2objcLibraryTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY))).containsExactly(
        "third_party/java/j2objc/libjre_core_lib.a",
        "third_party/java/j2objc/libproto_runtime.a",
        "java/com/google/dummy/test/proto/libtest_j2objc.a",
        "java/com/google/dummy/test/proto/libtest_proto_j2objc.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER))).containsExactly(
        "third_party/java/j2objc/jre_core.h",
        "third_party/java/j2objc/runtime.h",
        "java/com/google/dummy/test/proto/test.j2objc.pb.h",
        "java/com/google/dummy/test/proto/_j2objc/test/java/com/google/dummy/test/proto/test.h");
  }

  @Test
  public void testJ2ObjcHeaderMapExportedInJavaLibrary() throws Exception {
    scratch.file("java/com/google/transpile/BUILD",
        "java_library(name = 'dummy',",
        "    srcs = ['dummy.java'])");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//java/com/google/transpile:dummy");
    J2ObjcMappingFileProvider provider = target.getProvider(J2ObjcMappingFileProvider.class);

    assertThat(Iterables.getOnlyElement(provider.getHeaderMappingFiles())
        .getRootRelativePath().toString()).isEqualTo(
            "java/com/google/transpile/dummy.mapping.j2objc");
  }

  @Test
  public void testDepsJ2ObjcHeaderMapExportedInJavaLibraryWithNoSourceFile() throws Exception {
    scratch.file("java/com/google/transpile/BUILD",
        "java_library(name = 'dummy',",
        "    exports = ['//java/com/google/dep:dep'])");
    scratch.file("java/com/google/dep/BUILD",
        "java_library(name = 'dep',",
        "    srcs = ['dummy.java'])");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//java/com/google/transpile:dummy");
    J2ObjcMappingFileProvider provider = target.getProvider(J2ObjcMappingFileProvider.class);

    assertThat(Iterables.getOnlyElement(provider.getHeaderMappingFiles())
        .getRootRelativePath().toString()).isEqualTo(
            "java/com/google/dep/dep.mapping.j2objc");
  }

  @Test
  public void testJ2ObjcProtoClassMappingFilesExportedInJavaLibrary() throws Exception {
    scratch.file("java/com/google/dummy/test/proto/test.java");
    scratch.file("java/com/google/dummy/test/proto/test.proto");
    scratch.file("java/com/google/dummy/test/proto/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "proto_library(",
        "    name = 'test_proto',",
        "    srcs = ['test.proto'],",
        "    java_api_version = 2,",
        "    j2objc_api_version = 1)",
        "",
        "java_library(",
        "    name = 'test',",
        "    srcs = ['test.java'],",
        "    deps = [':test_proto'])");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget(
        "//java/com/google/dummy/test/proto:test");
    ConfiguredTarget proto =
        getConfiguredTarget(
            "//java/com/google/dummy/test/proto:test", getAppleCrosstoolConfiguration());
    J2ObjcMappingFileProvider provider = target.getProvider(J2ObjcMappingFileProvider.class);
    Artifact classMappingFile = getGenfilesArtifact("test.clsmap.properties", proto);

    assertThat(provider.getClassMappingFiles()).containsExactly(classMappingFile);
  }

  @Test
  public void testJavaProtoLibraryWithProtoLibrary() throws Exception {
    scratch.file("x/BUILD",
        "proto_library(",
        "    name = 'test_proto',",
        "    srcs = ['test.proto'],",
        "    java_api_version = 2,",
        "    j2objc_api_version = 1)",
        "",
        "java_proto_library(",
        "    name = 'test_java_proto',",
        "    deps = [':test_proto'])",
        "",
        "java_library(",
        "    name = 'test',",
        "    srcs = ['test.java'],",
        "    deps = [':test_java_proto'])");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//x:test");
    ConfiguredTarget test = getConfiguredTarget("//x:test_proto", getAppleCrosstoolConfiguration());
    J2ObjcMappingFileProvider provider = target.getProvider(J2ObjcMappingFileProvider.class);
    Artifact classMappingFile = getGenfilesArtifact("test.clsmap.properties", test);
    assertThat(provider.getClassMappingFiles()).containsExactly(classMappingFile);

    ObjcProvider objcProvider = target.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    Artifact headerFile = getGenfilesArtifact("test.j2objc.pb.h", test);
    Artifact sourceFile = getGenfilesArtifact("test.j2objc.pb.m", test);
    assertThat(objcProvider.get(ObjcProvider.HEADER)).contains(headerFile);
    assertThat(objcProvider.get(ObjcProvider.SOURCE)).contains(sourceFile);
  }

  @Test
  public void testJ2ObjcInfoExportedInJavaImport() throws Exception {
    scratch.file("java/com/google/transpile/BUILD",
        "java_import(name = 'dummy',",
        "    jars = ['dummy.jar'],",
        "    srcjar = 'dummy.srcjar',",
        ")");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//java/com/google/transpile:dummy");
    J2ObjcMappingFileProvider provider = target.getProvider(J2ObjcMappingFileProvider.class);

    assertThat(Iterables.getOnlyElement(provider.getHeaderMappingFiles())
        .getRootRelativePath().toString()).isEqualTo(
        "java/com/google/transpile/dummy.mapping.j2objc");
    assertThat(provider.getClassMappingFiles()).isEmpty();
  }

  protected void checkObjcArchiveAndLinkActions(
      String targetLabel,
      String archiveFileName,
      String objFileName,
      Iterable<String> compilationInputExecPaths)
      throws Exception {
    String labelName = Label.parseAbsolute(targetLabel).getName();
    CommandAction linkAction =
        (CommandAction)
            getGeneratingAction(
                getBinArtifact(
                    String.format("%s_bin", labelName),
                    getConfiguredTargetInAppleBinaryTransition(targetLabel)));

    checkObjcCompileActions(
        getFirstArtifactEndingWith(linkAction.getInputs(), archiveFileName),
        objFileName, compilationInputExecPaths);
  }

  @Test
  public void testMissingEntryClassesError() throws Exception {
    useConfiguration("--j2objc_dead_code_removal");
    checkError("java/com/google/dummy", "transpile", J2ObjcLibrary.NO_ENTRY_CLASS_ERROR_MSG,
        "j2objc_library(name = 'transpile', deps = ['//java/com/google/dummy/test:test'])");
  }

  @Test
  public void testNoJ2ObjcDeadCodeRemovalActionWithoutOptFlag() throws Exception {
    useConfiguration("--noj2objc_dead_code_removal");
    addSimpleJ2ObjcLibraryWithEntryClasses();
    addSimpleBinaryTarget("//java/com/google/app/test:transpile");

    Artifact expectedPrunedSource = getBinArtifact(
        "_j2objc_pruned/app/java/com/google/app/test/_j2objc/test/"
        + "java/com/google/app/test/test_pruned.m", getConfiguredTarget("//app:app"));
    assertThat(getGeneratingAction(expectedPrunedSource)).isNull();
  }

  @Test
  public void testExplicitJreDeps() throws Exception {
    ConfiguredTarget j2objcLibraryTarget = getConfiguredTarget(
        "//java/com/google/dummy/test:transpile");
    ObjcProvider provider = j2objcLibraryTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    // jre_io_lib and jre_emul_lib should be excluded.
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY))).containsExactly(
        "third_party/java/j2objc/libjre_core_lib.a",
        "java/com/google/dummy/test/libtest_j2objc.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER))).containsExactly(
        "third_party/java/j2objc/jre_core.h",
        "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h");
  }

  @Test
  public void testTranspilationActionTreeArtifactOutputsFromSourceJar() throws Exception {
    useConfiguration("--ios_cpu=i386", "--ios_minimum_os=1.0");
    scratch.file("java/com/google/transpile/dummy.java");
    scratch.file("java/com/google/transpile/dummyjar.srcjar");
    scratch.file("java/com/google/transpile/BUILD",
        "java_library(name = 'dummy',",
        "    srcs = ['dummy.java', 'dummyjar.srcjar'],",
        ")");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//java/com/google/transpile:dummy");
    ObjcProvider provider = target.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    Artifact srcJarSources = getFirstArtifactEndingWith(
        provider.get(ObjcProvider.SOURCE), "source_files");
    Artifact srcJarHeaders = getFirstArtifactEndingWith(
        provider.get(ObjcProvider.HEADER), "header_files");
    assertThat(srcJarSources.getRootRelativePathString())
        .isEqualTo("java/com/google/transpile/_j2objc/src_jar_files/dummy/source_files");
    assertThat(srcJarHeaders.getRootRelativePathString())
        .isEqualTo("java/com/google/transpile/_j2objc/src_jar_files/dummy/header_files");
    assertThat(srcJarSources.isTreeArtifact()).isTrue();
    assertThat(srcJarHeaders.isTreeArtifact()).isTrue();
  }

  @Test
  public void testGeneratedTreeArtifactFromGenJar() throws Exception {
    useConfiguration("--ios_cpu=i386", "--ios_minimum_os=1.0");
    addSimpleJ2ObjcLibraryWithJavaPlugin();
    ConfiguredTarget j2objcLibraryTarget =
        getConfiguredTarget("//java/com/google/app/test:transpile");
    ObjcProvider provider = j2objcLibraryTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    Artifact headers =
        getFirstArtifactEndingWith(provider.get(ObjcProvider.HEADER), "header_files");
    Artifact sources =
        getFirstArtifactEndingWith(provider.get(ObjcProvider.SOURCE), "source_files");
    assertThat(headers.isTreeArtifact()).isTrue();
    assertThat(sources.isTreeArtifact()).isTrue();

    SpawnAction j2objcAction = (SpawnAction) getGeneratingAction(headers);
    assertThat(j2objcAction.getOutputs()).containsAllOf(headers, sources);

    Artifact paramFile = getFirstArtifactEndingWith(j2objcAction.getInputs(), ".param.j2objc");
    ParameterFileWriteAction paramFileAction =
        (ParameterFileWriteAction) getGeneratingAction(paramFile);
    assertContainsSublist(
        ImmutableList.copyOf(paramFileAction.getContents()),
        ImmutableList.of(
            "--output_gen_source_dir",
            sources.getExecPathString(),
            "--output_gen_header_dir",
            headers.getExecPathString()));
  }

  @Test
  public void testJ2ObjcHeaderMappingAction() throws Exception {
    scratch.file("java/com/google/transpile/BUILD",
        "java_library(name = 'lib1',",
        "    srcs = ['libOne.java', 'jar.srcjar'],",
        "    deps = [':lib2']",
        ")",
        "",
        "java_library(name = 'lib2',",
        "    srcs = ['libTwo.java'],",
        ")");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget(
        "//java/com/google/transpile:lib1");
    J2ObjcMappingFileProvider mappingFileProvider =
        target.getProvider(J2ObjcMappingFileProvider.class);
    assertThat(Artifact.toRootRelativePaths(mappingFileProvider.getHeaderMappingFiles()))
        .containsExactly(
            "java/com/google/transpile/lib1.mapping.j2objc",
            "java/com/google/transpile/lib2.mapping.j2objc");

    Artifact mappingFile = getFirstArtifactEndingWith(
        mappingFileProvider.getHeaderMappingFiles(), "lib1.mapping.j2objc");
    SpawnAction headerMappingAction = (SpawnAction) getGeneratingAction(mappingFile);
    String execPath = target.getConfiguration().getBinDirectory(RepositoryName.MAIN)
        .getExecPath() + "/";
    assertThat(Artifact.toRootRelativePaths(headerMappingAction.getInputs()))
        .containsAllOf(
            "java/com/google/transpile/libOne.java", "java/com/google/transpile/jar.srcjar");
    assertThat(headerMappingAction.getArguments())
        .containsExactly(
            "tools/j2objc/j2objc_header_map.py",
            "--source_files",
            "java/com/google/transpile/libOne.java",
            "--source_jars",
            "java/com/google/transpile/jar.srcjar",
            "--output_mapping_file",
            execPath + "java/com/google/transpile/lib1.mapping.j2objc")
        .inOrder();
  }

  protected void checkObjcCompileActions(
      Artifact archiveFile, String objFileName, Iterable<String> compilationInputExecPaths)
      throws Exception {
    CommandAction compileAction = getObjcCompileAction(archiveFile, objFileName);
    assertThat(Artifact.toRootRelativePaths(compileAction.getPossibleInputsForTesting()))
        .containsAllIn(compilationInputExecPaths);
  }

  protected CommandAction getObjcCompileAction(Artifact archiveFile, String objFileName)
      throws Exception {
    CommandAction archiveAction = (CommandAction) getGeneratingAction(archiveFile);
    CommandAction compileAction =
        (CommandAction)
            getGeneratingAction(getFirstArtifactEndingWith(archiveAction.getInputs(), objFileName));
    return compileAction;
  }

  protected void addSimpleBinaryTarget(String j2objcLibraryTargetDep) throws Exception {
    scratch.file("app/app.m");
    scratch.file("app/Info.plist");
    scratch.file(
        "app/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "objc_library(",
        "    name = 'lib',",
        "    deps = ['" + j2objcLibraryTargetDep + "'])",
        "",
        "apple_binary(",
        "    name = 'app',",
        "    platform_type = 'ios',",
        "    deps = [':main_lib'],",
        ")",
        "objc_library(",
        "    name = 'main_lib',",
        "    srcs = ['app.m'],",
        "    deps = [':lib'],",
        ")");
  }

  protected void addSimpleJ2ObjcLibraryWithEntryClasses() throws Exception {
    scratch.file("java/com/google/app/test/test.java");
    scratch.file("java/com/google/app/test/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "java_library(",
        "    name = 'test',",
        "    srcs = ['test.java'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    entry_classes = ['com.google.app.test.test'],",
        "    deps = ['test'])");
  }

  protected void addSimpleJ2ObjcLibraryWithJavaPlugin() throws Exception {
    scratch.file("java/com/google/app/test/test.java");
    scratch.file("java/com/google/app/test/plugin.java");
    scratch.file(
        "java/com/google/app/test/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "java_library(",
        "    name = 'test',",
        "    srcs = ['test.java'],",
        "    plugins = [':plugin'])",
        "",
        "java_plugin(",
        "    name = 'plugin',",
        "    processor_class = 'com.google.process.stuff',",
        "    srcs = ['plugin.java'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    deps = [':test'])");
  }

  protected Artifact j2objcArchive(String j2objcLibraryTarget, String javaTargetName)
      throws Exception {
    ConfiguredTarget target = getConfiguredTarget(j2objcLibraryTarget);
    ObjcProvider provider = target.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    String archiveName = String.format("lib%s_j2objc.a", javaTargetName);
    return getFirstArtifactEndingWith(provider.get(ObjcProvider.LIBRARY), archiveName);
  }

  @Test
  public void testJ2ObjcInformationExportedFromObjcLibrary() throws Exception {
    scratch.file("app/lib.m");
    scratch.file(
        "app/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "objc_library(",
        "    name = 'lib',",
        "    srcs = ['lib.m'],",
        "    deps = ['//java/com/google/dummy/test:transpile'])");

    ConfiguredTarget objcTarget = getConfiguredTarget("//app:lib");

    ObjcProvider provider = objcTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY)))
        .containsExactly(
            "third_party/java/j2objc/libjre_core_lib.a",
            "java/com/google/dummy/test/libtest_j2objc.a",
            "app/liblib.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER)))
        .containsExactly(
            "third_party/java/j2objc/jre_core.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h");

    String execPath =
        objcTarget.getConfiguration().getBinDirectory(RepositoryName.MAIN).getExecPath() + "/";
    assertThat(
            Iterables.transform(
                provider.get(ObjcProvider.INCLUDE), PathFragment::getSafePathString))
        .containsExactly(execPath + "java/com/google/dummy/test/_j2objc/test");
  }

  @Test
  public void testJ2ObjcInfoExportedInObjcLibraryFromRuntimeDeps() throws Exception {
    scratch.file("app/lib.m");
    scratch.file(
        "app/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "",
        "java_library(name = 'dummyOne',",
        "    srcs = ['dummyOne.java'])",
        "java_library(name = 'dummyTwo',",
        "    srcs = ['dummyTwo.java'],",
        "    runtime_deps = [':dummyOne'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    deps = [':dummyTwo'])",
        "",
        "objc_library(",
        "    name = 'lib',",
        "    srcs = ['lib.m'],",
        "    deps = ['//app:transpile'])");

    ConfiguredTarget objcTarget = getConfiguredTarget("//app:lib");

    ObjcProvider provider = objcTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.LIBRARY)))
        .containsExactly(
            "third_party/java/j2objc/libjre_core_lib.a",
            "app/libdummyOne_j2objc.a",
            "app/libdummyTwo_j2objc.a",
            "app/liblib.a");
    assertThat(Artifact.toRootRelativePaths(provider.get(ObjcProvider.HEADER)))
        .containsExactly(
            "third_party/java/j2objc/jre_core.h",
            "app/_j2objc/dummyOne/app/dummyOne.h",
            "app/_j2objc/dummyTwo/app/dummyTwo.h");

    String execPath =
        objcTarget.getConfiguration().getBinDirectory(RepositoryName.MAIN).getExecPath() + "/";
    assertThat(
            Iterables.transform(
                provider.get(ObjcProvider.INCLUDE), PathFragment::getSafePathString))
        .containsExactly(execPath + "app/_j2objc/dummyOne", execPath + "app/_j2objc/dummyTwo");
  }

  @Test
  public void testJ2ObjcAppearsInLinkArgs() throws Exception {
    scratch.file(
        "java/c/y/BUILD", "java_library(", "    name = 'ylib',", "    srcs = ['lib.java'],", ")");
    scratch.file(
        "x/BUILD",
        "j2objc_library(",
        "    name = 'j2',",
        "    deps = [ '//java/c/y:ylib' ],",
        "    jre_deps = [ '//third_party/java/j2objc:jre_io_lib' ])",
        "apple_binary(",
        "    name = 'test',",
        "    platform_type = 'ios',",
        "    deps = [':main_lib'],",
        ")",
        "objc_library(",
        "    name = 'main_lib',",
        "    srcs = ['test.m'],",
        "    deps = [':j2'],",
        ")");

    CommandAction linkAction = linkAction("//x:test");
    List<String> linkArgs = normalizeBashArgs(linkAction.getArguments());
    ConfiguredTarget target = getConfiguredTargetInAppleBinaryTransition("//x:test");
    String binDir =
        target.getConfiguration().getBinDirectory(RepositoryName.MAIN).getExecPathString();
    Artifact fileList = getFirstArtifactEndingWith(linkAction.getInputs(), "test-linker.objlist");
    ParameterFileWriteAction filelistWriteAction =
        (ParameterFileWriteAction) getGeneratingAction(fileList);
    assertThat(linkArgs).contains(fileList.getExecPathString());
    assertThat(filelistWriteAction.getContents())
        .containsAllOf(
            binDir + "/java/c/y/libylib_j2objc.a",
            // All jre libraries mus appear after java libraries in the link order.
            binDir + "/third_party/java/j2objc/libjre_io_lib.a",
            binDir + "/third_party/java/j2objc/libjre_core_lib.a")
        .inOrder();
  }

  @Test
  public void testArchiveLinkActionWithTreeArtifactFromGenJar() throws Exception {
    useConfiguration("--ios_cpu=i386", "--ios_minimum_os=1.0");
    addSimpleJ2ObjcLibraryWithJavaPlugin();
    Artifact archive = j2objcArchive("//java/com/google/app/test:transpile", "test");
    CommandAction archiveAction = (CommandAction) getGeneratingAction(archive);
    Artifact archiveObjList =
        getFirstArtifactEndingWith(archiveAction.getInputs(), "-archive.objlist");
    Artifact objectFilesFromGenJar =
        getFirstArtifactEndingWith(archiveAction.getInputs(), "source_files");
    Artifact normalObjectFile = getFirstArtifactEndingWith(archiveAction.getInputs(), "test.o");

    ParameterFileWriteAction paramFileAction =
        (ParameterFileWriteAction) getGeneratingAction(archiveObjList);

    // Test that the archive obj list param file contains the individual object files inside
    // the object file tree artifact.
    assertThat(paramFileAction.getContents(DUMMY_ARTIFACT_EXPANDER))
        .containsExactly(
            objectFilesFromGenJar.getExecPathString() + "/children1",
            objectFilesFromGenJar.getExecPathString() + "/children2",
            normalObjectFile.getExecPathString());
  }

  @Test
  public void testJ2ObjCCustomModuleMap() throws Exception {
    useConfiguration("--experimental_objc_enable_module_maps");
    scratch.file("java/com/google/transpile/dummy.java");
    scratch.file("java/com/google/transpile/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "java_library(name = 'dummy',",
        "    srcs = ['dummy.java'])");

    ConfiguredTarget target = getJ2ObjCAspectConfiguredTarget("//java/com/google/transpile:dummy");

    ObjcProvider provider = target.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    Artifact moduleMap =
        getFirstArtifactEndingWith(
            provider.get(ObjcProvider.MODULE_MAP), "dummy.modulemaps/module.modulemap");

    Artifact umbrellaHeader =
        getFirstArtifactEndingWith(
            provider.get(ObjcProvider.UMBRELLA_HEADER), "dummy.modulemaps/umbrella.h");

    CppModuleMapAction moduleMapAction = (CppModuleMapAction) getGeneratingAction(moduleMap);
    UmbrellaHeaderAction umbrellaHeaderAction =
        (UmbrellaHeaderAction) getGeneratingAction(umbrellaHeader);

    ActionExecutionContext dummyActionExecutionContext =
        new ActionExecutionContext(
            null,
            null,
            ActionInputPrefetcher.NONE,
            actionKeyContext,
            null,
            null,
            ImmutableMap.<String, String>of(),
            DUMMY_ARTIFACT_EXPANDER);
    ByteArrayOutputStream moduleMapStream = new ByteArrayOutputStream();
    ByteArrayOutputStream umbrellaHeaderStream = new ByteArrayOutputStream();
    moduleMapAction.newDeterministicWriter(dummyActionExecutionContext)
        .writeOutputFile(moduleMapStream);
    umbrellaHeaderAction.newDeterministicWriter(dummyActionExecutionContext)
        .writeOutputFile(umbrellaHeaderStream);
    String moduleMapContent = moduleMapStream.toString();
    String umbrellaHeaderContent = umbrellaHeaderStream.toString();

    assertThat(moduleMapContent).contains("umbrella header \"umbrella.h\"");
    assertThat(umbrellaHeaderContent).contains("/dummy.h");
    assertThat(umbrellaHeaderContent).contains("#include");
  }

  @Test
  public void testModuleMapFromGenJarTreeArtifact() throws Exception {
    useConfiguration("--ios_cpu=i386", "--ios_minimum_os=1.0");
    addSimpleJ2ObjcLibraryWithJavaPlugin();
    ConfiguredTarget j2objcLibraryTarget =
        getConfiguredTarget("//java/com/google/app/test:transpile");
    ObjcProvider provider = j2objcLibraryTarget.get(ObjcProvider.SKYLARK_CONSTRUCTOR);
    Artifact moduleMap =
        getFirstArtifactEndingWith(
            provider.get(ObjcProvider.MODULE_MAP), "test.modulemaps/module.modulemap");
    Artifact umbrellaHeader =
        getFirstArtifactEndingWith(
            provider.get(ObjcProvider.UMBRELLA_HEADER), "test.modulemaps/umbrella.h");

    CppModuleMapAction moduleMapAction = (CppModuleMapAction) getGeneratingAction(moduleMap);
    UmbrellaHeaderAction umbrellaHeaderAction =
        (UmbrellaHeaderAction) getGeneratingAction(umbrellaHeader);
    Artifact headers =
        getFirstArtifactEndingWith(provider.get(ObjcProvider.HEADER), "header_files");

    // Test that the module map action contains the header tree artifact as both the public header
    // and part of the action inputs.
    assertThat(moduleMapAction.getPublicHeaders()).contains(headers);
    assertThat(moduleMapAction.getInputs()).contains(headers);

    ActionExecutionContext dummyActionExecutionContext =
        new ActionExecutionContext(
            null,
            null,
            ActionInputPrefetcher.NONE,
            actionKeyContext,
            null,
            null,
            ImmutableMap.<String, String>of(),
            DUMMY_ARTIFACT_EXPANDER);

    ByteArrayOutputStream moduleMapStream = new ByteArrayOutputStream();
    ByteArrayOutputStream umbrellaHeaderStream = new ByteArrayOutputStream();
    moduleMapAction
        .newDeterministicWriter(dummyActionExecutionContext)
        .writeOutputFile(moduleMapStream);
    umbrellaHeaderAction
        .newDeterministicWriter(dummyActionExecutionContext)
        .writeOutputFile(umbrellaHeaderStream);
    String moduleMapContent = moduleMapStream.toString();
    String umbrellaHeaderContent = umbrellaHeaderStream.toString();

    // Test that the module map content contains the individual headers inside the header tree
    // artifact.
    assertThat(moduleMapContent).contains("umbrella header \"umbrella.h\"");
    assertThat(umbrellaHeaderContent).contains(headers.getExecPathString() + "/children1");
    assertThat(umbrellaHeaderContent).contains(headers.getExecPathString() + "/children2");
  }

  @Test
  public void testJ2ObjCFullyLinkAction() throws Exception {
    AbstractAction linkAction = (AbstractAction) getGeneratingActionForLabel(
        "//java/com/google/dummy/test:transpile_fully_linked.a");
    String fullyLinkBinaryPath =
        Iterables.getOnlyElement(linkAction.getOutputs()).getExecPathString();
    assertThat(fullyLinkBinaryPath).contains("transpile_fully_linked.a");
  }

  @Test
  public void testObjcCompileAction() throws Exception {
    Artifact archive = j2objcArchive("//java/com/google/dummy/test:transpile", "test");
    CommandAction compileAction = getObjcCompileAction(archive, "test.o");
    assertThat(Artifact.toRootRelativePaths(compileAction.getPossibleInputsForTesting()))
        .containsAllOf(
            "third_party/java/j2objc/jre_core.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.m");
    assertThat(compileAction.getArguments()).containsAllOf("-fno-objc-arc", "-fno-strict-overflow");
  }

  @Test
  public void testJ2ObjcSourcesCompilationAndLinking() throws Exception {
    addSimpleBinaryTarget("//java/com/google/dummy/test:transpile");

    checkObjcArchiveAndLinkActions(
        "//app:app",
        "libtest_j2objc.a",
        "test.o",
        ImmutableList.of(
            "third_party/java/j2objc/jre_core.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.m"));
  }

  @Test
  public void testNestedJ2ObjcLibraryDeps() throws Exception {
    scratch.file("java/com/google/dummy/dummy.java");
    scratch.file(
        "java/com/google/dummy/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "java_library(",
        "    name = 'dummy',",
        "    srcs = ['dummy.java'])",
        "",
        "j2objc_library(",
        "    name = 'transpile',",
        "    deps = [",
        "        ':dummy',",
        "        '//java/com/google/dummy/test:transpile',",
        "    ])");
    addSimpleBinaryTarget("//java/com/google/dummy:transpile");

    checkObjcArchiveAndLinkActions(
        "//app:app",
        "libtest_j2objc.a",
        "test.o",
        ImmutableList.of(
            "third_party/java/j2objc/jre_core.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h",
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.m"));

    checkObjcArchiveAndLinkActions(
        "//app:app",
        "libdummy_j2objc.a",
        "dummy.o",
        ImmutableList.of(
            "third_party/java/j2objc/jre_core.h",
            "java/com/google/dummy/_j2objc/dummy/java/com/google/dummy/dummy.h",
            "java/com/google/dummy/_j2objc/dummy/java/com/google/dummy/dummy.m"));
  }

  @Test
  public void testJ2ObjcTranspiledHeaderInCompilationAction() throws Exception {
    scratch.file("app/lib.m");
    scratch.file(
        "app/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "objc_library(",
        "    name = 'lib',",
        "    srcs = ['lib.m'],",
        "    deps = ['//java/com/google/dummy/test:transpile'])");

    checkObjcCompileActions(
        getBinArtifact("liblib.a", getConfiguredTarget("//app:lib")),
        "lib.o",
        ImmutableList.of(
            "java/com/google/dummy/test/_j2objc/test/java/com/google/dummy/test/test.h"));
  }

  @Test
  public void testJ2ObjcDeadCodeRemovalActionWithOptFlag() throws Exception {
    useConfiguration("--j2objc_dead_code_removal");
    addSimpleJ2ObjcLibraryWithEntryClasses();
    addSimpleBinaryTarget("//java/com/google/app/test:transpile");

    ConfiguredTarget appTarget = getConfiguredTargetInAppleBinaryTransition("//app:app");
    Artifact prunedArchive =
        getBinArtifact(
            "_j2objc_pruned/app/java/com/google/app/test/libtest_j2objc_pruned.a", appTarget);
    Artifact paramFile =
        getBinArtifact("_j2objc_pruned/app/java/com/google/app/test/test.param.j2objc", appTarget);

    ConfiguredTarget javaTarget =
        getConfiguredTargetInAppleBinaryTransition("//java/com/google/app/test:test");
    Artifact inputArchive = getBinArtifact("libtest_j2objc.a", javaTarget);
    Artifact headerMappingFile = getBinArtifact("test.mapping.j2objc", javaTarget);
    Artifact dependencyMappingFile = getBinArtifact("test.dependency_mapping.j2objc", javaTarget);
    Artifact archiveSourceMappingFile =
        getBinArtifact("test.archive_source_mapping.j2objc", javaTarget);
    String execPath =
        javaTarget.getConfiguration().getBinDirectory(RepositoryName.MAIN).getExecPath() + "/";

    ParameterFileWriteAction paramFileAction =
        (ParameterFileWriteAction) getGeneratingAction(paramFile);
    assertContainsSublist(
        ImmutableList.copyOf(paramFileAction.getContents()),
        new ImmutableList.Builder<String>()
            .add("--input_archive")
            .add(inputArchive.getExecPathString())
            .add("--output_archive")
            .add(prunedArchive.getExecPathString())
            .add("--dummy_archive")
            .add(execPath + "tools/objc/libdummy_lib.a")
            .add("--xcrunwrapper")
            .add(MOCK_XCRUNWRAPPER_EXECUTABLE_PATH)
            .add("--dependency_mapping_files")
            .add(dependencyMappingFile.getExecPathString())
            .add("--header_mapping_files")
            .add(headerMappingFile.getExecPathString())
            .add("--archive_source_mapping_files")
            .add(archiveSourceMappingFile.getExecPathString())
            .add("--entry_classes")
            .add("com.google.app.test.test")
            .build());

    SpawnAction deadCodeRemovalAction = (SpawnAction) getGeneratingAction(prunedArchive);
    assertContainsSublist(
        deadCodeRemovalAction.getArguments(),
        new ImmutableList.Builder<String>()
            .add("tools/objc/j2objc_dead_code_pruner.py")
            .add("@" + paramFile.getExecPathString())
            .build());
    assertThat(deadCodeRemovalAction.getOutputs()).containsExactly(prunedArchive);
  }

  /** Returns the actions created by the action template corresponding to given artifact. */
  protected Iterable<CommandAction> getActionsForInputsOfGeneratingActionTemplate(
      Artifact artifact, TreeFileArtifact treeFileArtifact)
      throws ActionTemplateExpansionException {
    CppCompileActionTemplate template =
        (CppCompileActionTemplate) getActionGraph().getGeneratingAction(artifact);
    return ImmutableList.<CommandAction>builder()
        .addAll(
            template.generateActionForInputArtifacts(
                ImmutableList.of(treeFileArtifact), ArtifactOwner.NullArtifactOwner.INSTANCE))
        .build();
  }

  @Test
  public void testCompileActionTemplateFromGenJar() throws Exception {
    useConfiguration("--cpu=ios_i386", "--ios_minimum_os=1.0");
    addSimpleJ2ObjcLibraryWithJavaPlugin();
    Artifact archive = j2objcArchive("//java/com/google/app/test:transpile", "test");
    CommandAction archiveAction = (CommandAction) getGeneratingAction(archive);
    Artifact objectFilesFromGenJar =
        getFirstArtifactEndingWith(archiveAction.getInputs(), "source_files");

    assertThat(objectFilesFromGenJar.isTreeArtifact()).isTrue();
    assertThat(objectFilesFromGenJar.getRootRelativePathString())
        .isEqualTo(
            "java/com/google/app/test/_objs/test/java/com/google/app/test/_j2objc/"
                + "src_jar_files/test/source_files");

    ActionAnalysisMetadata actionTemplate =
        getActionGraph().getGeneratingAction(objectFilesFromGenJar);
    Artifact sourceFilesFromGenJar =
        getFirstArtifactEndingWith(actionTemplate.getInputs(), "source_files");
    Artifact headerFilesFromGenJar =
        getFirstArtifactEndingWith(actionTemplate.getInputs(), "header_files");
    assertThat(sourceFilesFromGenJar.getRootRelativePathString())
        .isEqualTo("java/com/google/app/test/_j2objc/src_jar_files/test/source_files");
    assertThat(headerFilesFromGenJar.getRootRelativePathString())
        .isEqualTo("java/com/google/app/test/_j2objc/src_jar_files/test/header_files");

    // The files contained inside the tree artifacts are not known until execution time.
    // Therefore we need to fake some files inside them to test the action template in this
    // analysis-time test.
    TreeFileArtifact oneSourceFileFromGenJar =
        ActionInputHelper.treeFileArtifact(sourceFilesFromGenJar, "children1.m");
    TreeFileArtifact oneObjFileFromGenJar =
        ActionInputHelper.treeFileArtifact(objectFilesFromGenJar, "children1.o");
    Iterable<CommandAction> compileActions =
        getActionsForInputsOfGeneratingActionTemplate(
            objectFilesFromGenJar, oneSourceFileFromGenJar);
    CommandAction compileAction = Iterables.getOnlyElement(compileActions);
    ConfiguredTarget j2objcLibraryTarget =
        getConfiguredTarget("//java/com/google/dummy/test:transpile");
    String genfilesFragment =
        j2objcLibraryTarget.getConfiguration().getGenfilesFragment().toString();
    String binFragment = j2objcLibraryTarget.getConfiguration().getBinFragment().toString();
    AppleConfiguration appleConfiguration =
        j2objcLibraryTarget.getConfiguration().getFragment(AppleConfiguration.class);

    String commandLine = Joiner.on(" ").join(compileAction.getArguments());
    ImmutableList<String> expectedArgs =
        new ImmutableList.Builder<String>()
            .addAll(AppleToolchain.DEFAULT_WARNINGS.values())
            .add("-fexceptions")
            .add("-fasm-blocks")
            .add("-fobjc-abi-version=2")
            .add("-fobjc-legacy-dispatch")
            .add("-DOS_IOS")
            .add("-mios-simulator-version-min=1.0")
            .add("-arch", "i386")
            .add("-isysroot")
            .add(AppleToolchain.sdkDir())
            .add("-F")
            .add(AppleToolchain.sdkDir() + "/Developer/Library/Frameworks")
            .add("-F")
            .add(AppleToolchain.platformDeveloperFrameworkDir(appleConfiguration))
            .add("-O0")
            .add("-DDEBUG=1")
            .add("-iquote")
            .add(".")
            .add("-iquote")
            .add(genfilesFragment)
            .add("-I")
            .add(binFragment + "/java/com/google/app/test/_j2objc/test")
            .add("-I")
            .add(headerFilesFromGenJar.getExecPathString())
            .add("-fno-strict-overflow")
            .add("-fno-objc-arc")
            .add("-c")
            .add(oneSourceFileFromGenJar.getExecPathString())
            .add("-o")
            .add(oneObjFileFromGenJar.getExecPathString())
            .build();
    for (String expectedArg : expectedArgs) {
      assertThat(commandLine).contains(expectedArg);
    }
  }
}
