#!/bin/bash -eu

set -o pipefail

# This test exercises the bootstrapping process of the build system
# in a source tree that only contains enough files for Bazel and Soong to work.

source "$(dirname "$0")/lib.sh"

readonly GENERATED_BUILD_FILE_NAME="BUILD.bazel"

readonly target_product="${TARGET_PRODUCT:-aosp_arm}"

function test_smoke {
  setup
  run_soong
}

function test_null_build() {
  setup
  run_soong
  local -r bootstrap_mtime1=$(stat -c "%y" out/soong/bootstrap.ninja)
  local -r output_mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  run_soong
  local -r bootstrap_mtime2=$(stat -c "%y" out/soong/bootstrap.ninja)
  local -r output_mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$bootstrap_mtime1" == "$bootstrap_mtime2" ]]; then
    # Bootstrapping is always done. It doesn't take a measurable amount of time.
    fail "Bootstrap Ninja file did not change on null build"
  fi

  if [[ "$output_mtime1" != "$output_mtime2" ]]; then
    fail "Output Ninja file changed on null build"
  fi
}

function test_soong_build_rebuilt_if_blueprint_changes() {
  setup
  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/bootstrap.ninja)

  sed -i 's/pluginGenSrcCmd/pluginGenSrcCmd2/g' build/blueprint/bootstrap/bootstrap.go

  run_soong
  local -r mtime2=$(stat -c "%y" out/soong/bootstrap.ninja)

  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Bootstrap Ninja file did not change"
  fi
}

function test_change_android_bp() {
  setup
  mkdir -p a
  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_little_binary_host",
  srcs: ["my_little_binary_host.py"]
}
EOF
  touch a/my_little_binary_host.py
  run_soong

  grep -q "^# Module:.*my_little_binary_host" out/soong/build."${target_product}".ninja || fail "module not found"

  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_great_binary_host",
  srcs: ["my_great_binary_host.py"]
}
EOF
  touch a/my_great_binary_host.py
  run_soong

  grep -q "^# Module:.*my_little_binary_host" out/soong/build."${target_product}".ninja && fail "old module found"
  grep -q "^# Module:.*my_great_binary_host" out/soong/build."${target_product}".ninja || fail "new module not found"
}

function test_add_android_bp() {
  setup
  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  mkdir -p a
  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_little_binary_host",
  srcs: ["my_little_binary_host.py"]
}
EOF
  touch a/my_little_binary_host.py
  run_soong

  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Output Ninja file did not change"
  fi

  grep -q "^# Module:.*my_little_binary_host$" out/soong/build."${target_product}".ninja || fail "New module not in output"

  run_soong
}

function test_delete_android_bp() {
  setup
  mkdir -p a
  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_little_binary_host",
  srcs: ["my_little_binary_host.py"]
}
EOF
  touch a/my_little_binary_host.py
  run_soong

  grep -q "^# Module:.*my_little_binary_host$" out/soong/build."${target_product}".ninja || fail "Module not in output"

  rm a/Android.bp
  run_soong

  if grep -q "^# Module:.*my_little_binary_host$" out/soong/build."${target_product}".ninja; then
    fail "Old module in output"
  fi
}

# Test that an incremental build with a glob doesn't rerun soong_build, and
# only regenerates the globs on the first but not the second incremental build.
function test_glob_noop_incremental() {
  setup

  # This test needs to start from a clean build, but setup creates an
  # initialized tree that has already been built once.  Clear the out
  # directory to start from scratch (see b/185591972)
  rm -rf out

  mkdir -p a
  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_little_binary_host",
  srcs: ["*.py"],
}
EOF
  touch a/my_little_binary_host.py
  run_soong
  local -r ninja_mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  run_soong
  local -r ninja_mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$ninja_mtime1" != "$ninja_mtime2" ]]; then
    fail "Ninja file rewritten on null incremental build"
  fi

  run_soong
  local -r ninja_mtime3=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$ninja_mtime2" != "$ninja_mtime3" ]]; then
    fail "Ninja file rewritten on null incremental build"
  fi
}

function test_add_file_to_glob() {
  setup

  mkdir -p a
  cat > a/Android.bp <<'EOF'
python_binary_host {
  name: "my_little_binary_host",
  srcs: ["*.py"],
}
EOF
  touch a/my_little_binary_host.py
  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  touch a/my_little_library.py
  run_soong

  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Output Ninja file did not change"
  fi

  grep -q my_little_library.py out/soong/build."${target_product}".ninja || fail "new file is not in output"
}

function test_soong_build_rerun_iff_environment_changes() {
  setup

  mkdir -p build/soong/cherry
  cat > build/soong/cherry/Android.bp <<'EOF'
bootstrap_go_package {
  name: "cherry",
  pkgPath: "android/soong/cherry",
  deps: [
    "blueprint",
    "soong",
    "soong-android",
  ],
  srcs: [
    "cherry.go",
  ],
  pluginFor: ["soong_build"],
}
EOF

  cat > build/soong/cherry/cherry.go <<'EOF'
package cherry

import (
  "android/soong/android"
  "github.com/google/blueprint"
)

var (
  pctx = android.NewPackageContext("cherry")
)

func init() {
  android.RegisterSingletonType("cherry", CherrySingleton)
}

func CherrySingleton() android.Singleton {
  return &cherrySingleton{}
}

type cherrySingleton struct{}

func (p *cherrySingleton) GenerateBuildActions(ctx android.SingletonContext) {
  cherryRule := ctx.Rule(pctx, "cherry",
    blueprint.RuleParams{
      Command: "echo CHERRY IS " + ctx.Config().Getenv("CHERRY") + " > ${out}",
      CommandDeps: []string{},
      Description: "Cherry",
    })

  outputFile := android.PathForOutput(ctx, "cherry", "cherry.txt")
  var deps android.Paths

  ctx.Build(pctx, android.BuildParams{
    Rule: cherryRule,
    Output: outputFile,
    Inputs: deps,
  })
}
EOF

  export CHERRY=TASTY
  run_soong
  grep -q "CHERRY IS TASTY" out/soong/build."${target_product}".ninja \
    || fail "first value of environment variable is not used"

  export CHERRY=RED
  run_soong
  grep -q "CHERRY IS RED" out/soong/build."${target_product}".ninja \
    || fail "second value of environment variable not used"
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  run_soong
  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" != "$mtime2" ]]; then
    fail "Output Ninja file changed when environment variable did not"
  fi

}

function test_create_global_include_directory() {
  setup
  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  # Soong needs to know if top level directories like hardware/ exist for use
  # as global include directories.  Make sure that doesn't cause regens for
  # unrelated changes to the top level directory.
  mkdir -p system/core

  run_soong
  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" != "$mtime2" ]]; then
    fail "Output Ninja file changed when top level directory changed"
  fi

  # Make sure it does regen if a missing directory in the path of a global
  # include directory is added.
  mkdir -p system/core/include

  run_soong
  local -r mtime3=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime2" = "$mtime3" ]]; then
    fail "Output Ninja file did not change when global include directory created"
  fi

}

function test_add_file_to_soong_build() {
  setup
  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  mkdir -p vendor/foo/picard
  cat > vendor/foo/picard/Android.bp <<'EOF'
bootstrap_go_package {
  name: "picard-soong-rules",
  pkgPath: "android/soong/picard",
  deps: [
    "blueprint",
    "soong",
    "soong-android",
  ],
  srcs: [
    "picard.go",
  ],
  pluginFor: ["soong_build"],
}
EOF

  cat > vendor/foo/picard/picard.go <<'EOF'
package picard

import (
  "android/soong/android"
  "github.com/google/blueprint"
)

var (
  pctx = android.NewPackageContext("picard")
)

func init() {
  android.RegisterSingletonType("picard", PicardSingleton)
}

func PicardSingleton() android.Singleton {
  return &picardSingleton{}
}

type picardSingleton struct{}

func (p *picardSingleton) GenerateBuildActions(ctx android.SingletonContext) {
  picardRule := ctx.Rule(pctx, "picard",
    blueprint.RuleParams{
      Command: "echo Make it so. > ${out}",
      CommandDeps: []string{},
      Description: "Something quotable",
    })

  outputFile := android.PathForOutput(ctx, "picard", "picard.txt")
  var deps android.Paths

  ctx.Build(pctx, android.BuildParams{
    Rule: picardRule,
    Output: outputFile,
    Inputs: deps,
  })
}

EOF

  run_soong
  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Output Ninja file did not change"
  fi

  grep -q "Make it so" out/soong/build."${target_product}".ninja || fail "New action not present"
}

# Tests a glob in a build= statement in an Android.bp file, which is interpreted
# during bootstrapping.
function test_glob_during_bootstrapping() {
  setup

  mkdir -p build/soong/picard
  cat > build/soong/picard/Android.bp <<'EOF'
build=["foo*.bp"]
EOF
  cat > build/soong/picard/fooa.bp <<'EOF'
bootstrap_go_package {
  name: "picard-soong-rules",
  pkgPath: "android/soong/picard",
  deps: [
    "blueprint",
    "soong",
    "soong-android",
  ],
  srcs: [
    "picard.go",
  ],
  pluginFor: ["soong_build"],
}
EOF

  cat > build/soong/picard/picard.go <<'EOF'
package picard

import (
  "android/soong/android"
  "github.com/google/blueprint"
)

var (
  pctx = android.NewPackageContext("picard")
)

func init() {
  android.RegisterSingletonType("picard", PicardSingleton)
}

func PicardSingleton() android.Singleton {
  return &picardSingleton{}
}

type picardSingleton struct{}

var Message = "Make it so."

func (p *picardSingleton) GenerateBuildActions(ctx android.SingletonContext) {
  picardRule := ctx.Rule(pctx, "picard",
    blueprint.RuleParams{
      Command: "echo " + Message + " > ${out}",
      CommandDeps: []string{},
      Description: "Something quotable",
    })

  outputFile := android.PathForOutput(ctx, "picard", "picard.txt")
  var deps android.Paths

  ctx.Build(pctx, android.BuildParams{
    Rule: picardRule,
    Output: outputFile,
    Inputs: deps,
  })
}

EOF

  run_soong
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  grep -q "Make it so" out/soong/build."${target_product}".ninja || fail "Original action not present"

  cat > build/soong/picard/foob.bp <<'EOF'
bootstrap_go_package {
  name: "worf-soong-rules",
  pkgPath: "android/soong/worf",
  deps: [
    "blueprint",
    "soong",
    "soong-android",
    "picard-soong-rules",
  ],
  srcs: [
    "worf.go",
  ],
  pluginFor: ["soong_build"],
}
EOF

  cat > build/soong/picard/worf.go <<'EOF'
package worf

import "android/soong/picard"

func init() {
   picard.Message = "Engage."
}
EOF

  run_soong
  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Output Ninja file did not change"
  fi

  grep -q "Engage" out/soong/build."${target_product}".ninja || fail "New action not present"

  if grep -q "Make it so" out/soong/build."${target_product}".ninja; then
    fail "Original action still present"
  fi
}

function test_soong_docs_smoke() {
  setup

  run_soong soong_docs

  [[ -e "out/soong/docs/soong_build.html" ]] || fail "Documentation for main page not created"
  [[ -e "out/soong/docs/cc.html" ]] || fail "Documentation for C++ modules not created"
}

function test_null_build_after_soong_docs() {
  setup

  run_soong
  local -r ninja_mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  run_soong soong_docs
  local -r docs_mtime1=$(stat -c "%y" out/soong/docs/soong_build.html)

  run_soong soong_docs
  local -r docs_mtime2=$(stat -c "%y" out/soong/docs/soong_build.html)

  if [[ "$docs_mtime1" != "$docs_mtime2" ]]; then
    fail "Output Ninja file changed on null build"
  fi

  run_soong
  local -r ninja_mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$ninja_mtime1" != "$ninja_mtime2" ]]; then
    fail "Output Ninja file changed on null build"
  fi
}

function test_write_to_source_tree {
  setup
  mkdir -p a
  cat > a/Android.bp <<EOF
genrule {
  name: "write_to_source_tree",
  out: ["write_to_source_tree"],
  cmd: "touch file_in_source_tree && touch \$(out)",
}
EOF
  readonly EXPECTED_OUT=out/soong/.intermediates/a/write_to_source_tree/gen/write_to_source_tree
  readonly ERROR_LOG=${MOCK_TOP}/out/error.log
  readonly ERROR_MSG="Read-only file system"
  readonly ERROR_HINT_PATTERN="BUILD_BROKEN_SRC_DIR"
  # Test in ReadOnly source tree
  run_ninja BUILD_BROKEN_SRC_DIR_IS_WRITABLE=false ${EXPECTED_OUT} &> /dev/null && \
    fail "Write to source tree should not work in a ReadOnly source tree"

  if grep -q "${ERROR_MSG}" "${ERROR_LOG}" && grep -q "${ERROR_HINT_PATTERN}" "${ERROR_LOG}" ; then
    echo Error message and error hint found in logs >/dev/null
  else
    fail "Did not find Read-only error AND error hint in error.log"
  fi

  # Test in ReadWrite source tree
  run_ninja BUILD_BROKEN_SRC_DIR_IS_WRITABLE=true ${EXPECTED_OUT} &> /dev/null || \
    fail "Write to source tree did not succeed in a ReadWrite source tree"

  if  grep -q "${ERROR_MSG}\|${ERROR_HINT_PATTERN}" "${ERROR_LOG}" ; then
    fail "Found read-only error OR error hint in error.log"
  fi
}

function test_dump_json_module_graph() {
  setup
  run_soong json-module-graph
  if [[ ! -r "out/soong/module-graph.json" ]]; then
    fail "JSON file was not created"
  fi
}

function test_json_module_graph_back_and_forth_null_build() {
  setup

  run_soong
  local -r ninja_mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  run_soong json-module-graph
  local -r json_mtime1=$(stat -c "%y" out/soong/module-graph.json)

  run_soong
  local -r ninja_mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)
  if [[ "$ninja_mtime1" != "$ninja_mtime2" ]]; then
    fail "Output Ninja file changed after writing JSON module graph"
  fi

  run_soong json-module-graph
  local -r json_mtime2=$(stat -c "%y" out/soong/module-graph.json)
  if [[ "$json_mtime1" != "$json_mtime2" ]]; then
    fail "JSON module graph file changed after writing Ninja file"
  fi

}

# This test verifies that adding a new glob to a blueprint file only
# causes build."${target_product}".ninja to be regenerated on the *next* build, and *not*
# the build after. (This is a regression test for a bug where globs
# resulted in two successive regenerations.)
function test_new_glob_incrementality {
  setup

  run_soong nothing
  local -r mtime1=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  mkdir -p globdefpkg/
  cat > globdefpkg/Android.bp <<'EOF'
filegroup {
  name: "fg_with_glob",
  srcs: ["*.txt"],
}
EOF

  run_soong nothing
  local -r mtime2=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$mtime1" == "$mtime2" ]]; then
    fail "Ninja file was not regenerated, despite a new bp file"
  fi

  run_soong nothing
  local -r mtime3=$(stat -c "%y" out/soong/build."${target_product}".ninja)

  if [[ "$mtime2" != "$mtime3" ]]; then
    fail "Ninja file was regenerated despite no previous bp changes"
  fi
}

scan_and_run_tests
