// Copyright 2020 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.buildtool;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests correctness of the build summary output produced by BuildTool.showBuildResult() method. */
public abstract class BuildResultTestCase extends BuildIntegrationTestCase {
  private RecordingOutErr recOutErr = new RecordingOutErr();

  private static final String GENRULE_ERROR = "Exit 42";

  /**
   * Hook for subclasses to define which executor we use.  (The two concrete
   * subclasses, {Sequential,Parallel}BuildResultTest, are at the bottom of this
   * source file.)
   */
  protected abstract int numJobs();

  @Before
  public final void addOptions() throws Exception  {
    this.outErr = recOutErr;
    addOptions("--jobs", "" + numJobs(), "--show_result", "1000");
  }

  private void build(boolean expectFailure, String expectedError, String... targets)
      throws Exception {
    try {
      buildTarget(targets);
      if (expectFailure) {
        fail();
      }
    } catch (BuildFailedException e) {
      if (e.getMessage() != null) {
        assertThat(e).hasMessageThat().containsMatch(expectedError);
      }
    } finally {
      OutErr.SYSTEM_OUT_ERR.printErr(recOutErr.errAsLatin1());
      OutErr.SYSTEM_OUT_ERR.printOut(recOutErr.outAsLatin1());
    }
  }

  @Test
  public void testKeepGoingResult() throws Exception {
    write("test/BUILD",
        "genrule(name='A', srcs=['A1','A2'], outs=['A.out']," +
        " cmd='/bin/cp test/in $(location A.out)')\n" +
        "genrule(name='A1', srcs=[], outs=['A1.out']," +
        " cmd='/bin/cp test/in $(location A1.out)')\n" +
        "genrule(name='A2', srcs=[], outs=['A2.out']," +
        " cmd='exit 42')\n" +
        "genrule(name='B', srcs=[], outs=['B.out']," +
        " cmd='/bin/cp test/in $(location B.out)')\n");
    write("test/in", "(input)");

    addOptions("--keep_going");
    build(true, GENRULE_ERROR, "//test:B", "//test:A");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //test:A failed to build\n");
    assertThat(stderr).containsMatch("Target //test:B up-to-date:\n" + "  .*/test/B\\.out\n");
  }

  @Test
  public void testNoKeepGoingResult() throws Exception {
    write("test/BUILD",
        "genrule(name='A', srcs=['A2'], outs=['A.out']," +
        " cmd='/bin/cp test/in $(location A.out)')\n" +
        "genrule(name='A2', srcs=[], outs=['A2.out']," +
        " cmd='exit 42')\n" +
        "genrule(name='B', srcs=[], outs=['B.out']," +
        " cmd='/bin/cp test/in $(location B.out)')\n");
    write("test/in", "(input)");

    build(true, GENRULE_ERROR, "//test:A", "//test:B");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).containsMatch("Target //test:A failed to build\n");
  }

  /**
   * This is a regression test for bug #823077, in which presence of artifacts
   * with null generating action caused targets always to be reported as failed.
   */
  @Test
  public void testKeepGoingResultWithNullActions() throws Exception {
    write("foo/BUILD",
          "sh_test(name='test_bar', srcs=['test_bar.sh'])\n" +
          "sh_test(name='test_foo', srcs=['test_foo.sh'])\n");
    write("foo/test_bar.sh", "echo bar is fine").setExecutable(true);

    addOptions("--keep_going");
    build(true, "no-error", "//foo:test_foo", "//foo:test_bar");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //foo:test_bar up-to-date:\n");
    assertThat(stderr).containsMatch("\n  .*/foo/test_bar\n");
    assertThat(stderr).doesNotContainMatch("\n  .*/foo/test_bar\\.sh\n");
    assertThat(stderr).contains("Target //foo:test_foo failed to build\n");
  }

  /**
   * This is a regression test for bug #1044470, in which targets with failed data dependencies
   * were still being reported as "up-to-date".
   */
  @Test
  public void testWithMissingData() throws Exception {
    write("needsdata/BUILD",
        "cc_library(name = 'needsdata', data = [':data_lib'])",
        "cc_library(name = 'data_lib', data = [':does_not_exist'])");

    // TODO(bazel-team): figure out why error message is non-deterministic with Skyframe full.
    // LOADING_AND_ANALYSIS loading_and_analysis cleanup.
    build(true, "input", "//needsdata");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).doesNotContain("Target //needsdata:needsdata up-to-date:\n");
    assertThat(stderr).contains("Target //needsdata:needsdata failed to build\n");
  }

  /**
   * This is a regression test for bug #987608, in which the temp artifacts (.ii and .s)
   * were not being reported as having been created.
   */
  @Test
  public void testWithSaveTemps() throws Exception {
    write("my_clib/BUILD",
        "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc",
          "void f() {}");

    addOptions("--save_temps");
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr).contains("blaze-bin/my_clib/_objs/my_clib/myclib.pic.s\n");
    assertThat(stderr).contains("blaze-bin/my_clib/_objs/my_clib/myclib.pic.ii\n");
  }

  /**
   * Test that changing the symlink prefix builds into another directory.
   */
  @Test
  public void testSymlinkPrefix() throws Exception {
    write("my_clib/BUILD",
        "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc",
          "void f() {}");

    addOptions("--symlink_prefix=myblaze-");
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr).contains("myblaze-bin/my_clib/libmy_clib.so\n");
    assertThat(stderr).contains("myblaze-bin/my_clib/libmy_clib.a\n");
  }

  /**
   * This is a regression test for bug #1013874, in which the temp artifacts (.ii and .s)
   * were not being reported as having been created within a failing target.
   * It's possible that one or more temps were successfully created, even if the
   * compilation failed.
   */
  @Test
  public void testFailedTargetWithSaveTemps() throws Exception {
    write("bad_clib/BUILD",
        "cc_library(name='bad_clib', srcs=['badlib.cc'])\n");
    // trigger a warning to make the build fail:
    //   "control reaches end of non-void function [-Werror,-Wreturn-type]"
    write("bad_clib/badlib.cc",
        "int f() { }");

    // We need to set --keep_going so that the temps get built even though the compilation fails.
    addOptions("--save_temps", "--keep_going");
    build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build");
    assertThat(stderr).contains("See temp at blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii\n");
  }

  @Test
  public void testSymlinkOutputAtCwdUnderWorkspace_withFlagOn() throws Exception {
    write("my_clib/BUILD", "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc", "void f() {}");

    addOptions(
        "--print_workspace_in_output_paths_if_needed",
        "--client_cwd=" + getWorkspace().getChild("my_clib").getPathString());
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr)
        .contains(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.so") + "\n");
    assertThat(stderr)
        .contains(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.a") + "\n");
  }

  @Test
  public void testSymlinkOutputAtCwdUnderWorkspace_withFlagOff() throws Exception {
    write("my_clib/BUILD", "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc", "void f() {}");

    addOptions(
        "--noprint_workspace_in_output_paths_if_needed",
        "--client_cwd=" + getWorkspace().getChild("my_clib").getPathString());
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.so") + "\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.a") + "\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.so\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.a\n");
  }

  @Test
  public void testSymlinkOutputAtCwdEqualWorkspace_withFlagOn() throws Exception {
    write("my_clib/BUILD", "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc", "void f() {}");

    addOptions("--print_workspace_in_output_paths_if_needed");
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.so") + "\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.a") + "\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.so\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.a\n");
  }

  @Test
  public void testSymlinkPrefixAtCwdEqualWorkspace_withFlagOff() throws Exception {
    write("my_clib/BUILD", "cc_library(name='my_clib', srcs=['myclib.cc'])\n");
    write("my_clib/myclib.cc", "void f() {}");

    addOptions("--noprint_workspace_in_output_paths_if_needed");
    build(false, "no-error", "//my_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.so") + "\n");
    assertThat(stderr)
        .doesNotContain(getWorkspace().getRelative("blaze-bin/my_clib/libmy_clib.a") + "\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.so\n");
    assertThat(stderr).contains("blaze-bin/my_clib/libmy_clib.a\n");
  }

  @Test
  public void testSeeTempAtCwdUnderWorkspace_withFlagOn() throws Exception {
    write("bad_clib/BUILD", "cc_library(name='bad_clib', srcs=['badlib.cc'])\n");
    // trigger a warning to make the build fail:
    //   "control reaches end of non-void function [-Werror,-Wreturn-type]"
    write("bad_clib/badlib.cc", "int f() { }");

    // We need to set --keep_going so that the temps get built even though the compilation fails.
    addOptions(
        "--save_temps",
        "--keep_going",
        "--print_workspace_in_output_paths_if_needed",
        "--client_cwd=" + getWorkspace().getChild("bad_clib").getPathString());
    build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build");
    assertThat(stderr)
        .contains(
            "See temp at "
                + getWorkspace().getRelative("blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii")
                + "\n");
  }

  @Test
  public void testSeeTempAtCwdUnderWorkspace_withFlagOff() throws Exception {
    write("bad_clib/BUILD", "cc_library(name='bad_clib', srcs=['badlib.cc'])\n");
    // trigger a warning to make the build fail:
    //   "control reaches end of non-void function [-Werror,-Wreturn-type]"
    write("bad_clib/badlib.cc", "int f() { }");

    // We need to set --keep_going so that the temps get built even though the compilation fails.
    addOptions(
        "--save_temps",
        "--keep_going",
        "--noprint_workspace_in_output_paths_if_needed",
        "--client_cwd=" + getWorkspace().getChild("bad_clib").getPathString());
    build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build");
    assertThat(stderr).contains("See temp at blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii\n");
  }

  @Test
  public void testSeeTempAtCwdEqualWorkspace_withFlagOn() throws Exception {
    write("bad_clib/BUILD", "cc_library(name='bad_clib', srcs=['badlib.cc'])\n");
    // trigger a warning to make the build fail:
    //   "control reaches end of non-void function [-Werror,-Wreturn-type]"
    write("bad_clib/badlib.cc", "int f() { }");

    // We need to set --keep_going so that the temps get built even though the compilation fails.
    addOptions("--save_temps", "--keep_going", "--print_workspace_in_output_paths_if_needed");
    build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build");
    assertThat(stderr).contains("See temp at blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii\n");
  }

  @Test
  public void testSeeTempAtCwdEqualWorkspace_withFlagOff() throws Exception {
    write("bad_clib/BUILD", "cc_library(name='bad_clib', srcs=['badlib.cc'])\n");
    // trigger a warning to make the build fail:
    //   "control reaches end of non-void function [-Werror,-Wreturn-type]"
    write("bad_clib/badlib.cc", "int f() { }");

    // We need to set --keep_going so that the temps get built even though the compilation fails.
    addOptions("--save_temps", "--keep_going", "--noprint_workspace_in_output_paths_if_needed");
    build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib");

    String stderr = recOutErr.errAsLatin1();
    assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build");
    assertThat(stderr).contains("See temp at blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii\n");
  }

  // Concrete implementations of this abstract test:

  /** Tests with 1 job. */
  @RunWith(JUnit4.class)
  public static class SequentialBuildResultTest extends BuildResultTestCase {
    @Override
    protected int numJobs() {
      return 1;
    }
  }

  /** Tests with 100 jobs. */
  @RunWith(JUnit4.class)
  public static class ParallelBuildResultTest extends BuildResultTestCase {
    @Override
    protected int numJobs() { return 100; }
  }
}
