#!/usr/bin/env bash
#
# Copyright 2023 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.
#
# Test rule transition can inspect configurable attribute.

# --- begin runfiles.bash initialization ---
set -euo pipefail
if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
  if [[ -f "$0.runfiles_manifest" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
    export RUNFILES_DIR="$0.runfiles"
  fi
fi
if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
  exit 1
fi
# --- end runfiles.bash initialization ---

source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }

#### SETUP #############################################################

add_to_bazelrc "build --genrule_strategy=local"
add_to_bazelrc "test --test_strategy=standalone"

function set_up() {
  create_new_workspace

  # Define common starlark flags.
  mkdir -p settings
  touch settings/BUILD
  cat > settings/flag.bzl <<EOF
BuildSettingInfo = provider(fields = ["value"])

def _flag_impl(ctx):
    return [BuildSettingInfo(value = ctx.build_setting_value)]

bool_flag = rule(
    implementation = _flag_impl,
    build_setting = config.bool(flag = True)
)

string_list_flag = rule(
    implementation = _flag_impl,
    build_setting = config.string_list(),
)
EOF
}

function create_transitions() {
  local pkg="${1}"
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF
load("//settings:flag.bzl", "BuildSettingInfo")

example_package = "${pkg}"

def _transition_impl(settings, attr):
    if getattr(attr, "apply_transition") and settings["//%s:transition_input_flag" % example_package]:
        return {"//%s:transition_output_flag" % example_package: True}
    return {"//%s:transition_output_flag" % example_package: False}

example_transition = transition(
    implementation = _transition_impl,
    inputs = ["//%s:transition_input_flag" % example_package],
    outputs = ["//%s:transition_output_flag" % example_package],
)

def _rule_impl(ctx):
    print("Flag value for %s: %s" % (
        ctx.label.name,
        ctx.attr._transition_output_flag[BuildSettingInfo].value,
    ))

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "apply_transition": attr.bool(default = False),
        "deps": attr.label_list(),
        "_transition_output_flag": attr.label(default = "//%s:transition_output_flag" % example_package),
    },
)

transition_not_attached = rule(
    implementation = _rule_impl,
    attrs = {
        "deps": attr.label_list(),
        "_transition_output_flag": attr.label(default = "//%s:transition_output_flag" % example_package),
    },
)
EOF
}

function create_rules_with_incoming_transition_and_selects() {
  local pkg="${1}"
  mkdir -p "${pkg}"
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
    "transition_not_attached",
)
load("//settings:flag.bzl", "bool_flag")

bool_flag(
    name = "transition_input_flag",
    build_setting_default = True,
)

bool_flag(
    name = "transition_output_flag",
    build_setting_default = False,
)

config_setting(
    name = "select_setting",
    flag_values = {":transition_input_flag": "True"},
)

# All should print "False" if
# "--no//${pkg}:transition_input_flag" is
# specified on the command line

# bazel build :top_level will print the results for all of the targets below

transition_attached(
    name = "top_level",
    apply_transition = select({
        ":select_setting": True,
        "//conditions:default": False,
    }),
    deps = [
        ":transition_attached_dep",
        ":transition_not_attached_dep",
    ],
)

# Should print "False"
transition_attached(
    apply_transition = False,
    name = "transition_attached_dep",
    deps = [
        ":transition_not_attached_dep_of_dep",
    ],
)

# Should print "True" when building top_level, "False" otherwise
transition_not_attached(
    name = "transition_not_attached_dep",
)

# Should print "False"
transition_not_attached(
    name = "transition_not_attached_dep_of_dep",
)
EOF
}

function test_rule_transition_can_inspect_configure_attributes(){
  local -r pkg="${FUNCNAME[0]}"
  create_transitions "${pkg}"
  create_rules_with_incoming_transition_and_selects "${pkg}"

  bazel build "//${pkg}:top_level" &> $TEST_log || fail "Build failed"

  expect_log 'Flag value for transition_not_attached_dep: True'
  expect_log 'Flag value for transition_not_attached_dep_of_dep: False'
  expect_log 'Flag value for transition_attached_dep: False'
  expect_log 'Flag value for top_level: True'
}

function test_rule_transition_can_inspect_configure_attributes_with_flag(){
  local -r pkg="${FUNCNAME[0]}"

  create_transitions "${pkg}"
  create_rules_with_incoming_transition_and_selects "${pkg}"

  bazel build --no//${pkg}:transition_input_flag "//${pkg}:top_level" &> $TEST_log || fail "Build failed"

  expect_log 'Flag value for transition_not_attached_dep: False'
  expect_log 'Flag value for transition_not_attached_dep_of_dep: False'
  expect_log 'Flag value for transition_attached_dep: False'
  expect_log 'Flag value for top_level: False'
}

function test_rule_transition_can_not_inspect_configure_attribute() {
  local -r pkg="${FUNCNAME[0]}"

  # create transition definition
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF
load("//settings:flag.bzl", "BuildSettingInfo")

example_package = "${pkg}"

def _transition_impl(settings, attr):
    if getattr(attr, "apply_transition") and settings["//%s:transition_input_flag" % example_package]:
        return {"//%s:transition_output_flag" % example_package: True}
    return {
      "//%s:transition_output_flag" % example_package: False,
      "//%s:transition_input_flag" % example_package: False
    }

example_transition = transition(
    implementation = _transition_impl,
    inputs = ["//%s:transition_input_flag" % example_package],
    outputs = [
      "//%s:transition_output_flag" % example_package,
      "//%s:transition_input_flag" % example_package,
    ],
)

def _rule_impl(ctx):
    print("Flag value for %s: %s" % (
        ctx.label.name,
        ctx.attr._transition_output_flag[BuildSettingInfo].value,
    ))

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "apply_transition": attr.bool(default = False),
        "deps": attr.label_list(),
        "_transition_output_flag": attr.label(default = "//%s:transition_output_flag" % example_package),
    },
)
EOF

  # create rules with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
)
load("//settings:flag.bzl", "bool_flag")

bool_flag(
    name = "transition_input_flag",
    build_setting_default = True,
)

bool_flag(
    name = "transition_output_flag",
    build_setting_default = False,
)

config_setting(
    name = "select_setting",
    flag_values = {":transition_input_flag": "True"},
)

# All should print "False" if
# "--no//${pkg}:transition_input_flag" is
# specified on the command line

# bazel build :top_level will print the results for all of the targets below

transition_attached(
    name = "top_level",
    apply_transition = select({
        ":select_setting": True,
        "//conditions:default": False,
    }),
)
EOF
  bazel build "//${pkg}:top_level" &> $TEST_log && fail "Build did NOT complete successfully"
  expect_log "No attribute 'apply_transition'. Either this attribute does not exist for this rule or the attribute was not resolved because it is set by a select that reads flags the transition may set."
}

function test_unresolvable_select_does_not_error_out_before_applying_rule_transition() {
  local -r pkg="${FUNCNAME[0]}"

  # create transition definition
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF
load("//settings:flag.bzl", "BuildSettingInfo")

example_package = "${pkg}"

# transition that sets compilation_mode so that unresolvable selects before rule transition
# becomes resolvable after rule transition
def _transition_impl(settings, attr):
    return {"//command_line_option:compilation_mode": "dbg"}

example_transition = transition(
    implementation = _transition_impl,
    inputs = [],
    outputs = ["//command_line_option:compilation_mode"],
)

def _rule_impl(ctx):
    print("Hello")

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "compilation_mode": attr.string(),
    },
)
EOF

  # create rules with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
)

config_setting(
    name = "dbg",
    values = {
        "compilation_mode": "dbg",
    },
)

config_setting(
    name = "opt",
    values = {
        "compilation_mode": "opt",
    },
)

transition_attached(
    name = "top_level",
    compilation_mode = select(
        {
            ":dbg": "dbg",
            ":opt": "opt",
        },
    ),
)
EOF
  bazel build --compilation_mode=fastbuild "//${pkg}:top_level" &> $TEST_log || fail "Build failed"
}

function test_unresolvable_select_error_out_after_applying_rule_transition() {
  local -r pkg="${FUNCNAME[0]}"

  # create transition definition
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF
load("//settings:flag.bzl", "BuildSettingInfo")

example_package = "${pkg}"

# dummy transition that does not set anything and we expect to see BUILD ERROR
# for unresolvable select after applying rule transition
def _transition_impl(settings, attr):
    return {}

example_transition = transition(
    implementation = _transition_impl,
    inputs = [],
    outputs = [],
)

def _rule_impl(ctx):
    print("Hello")

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "compilation_mode": attr.string(),
    },
)
EOF

  # create rules with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
)

config_setting(
  name = "dbg",
  values = {
        "compilation_mode": "dbg",
  },
)

config_setting(
  name = "opt",
  values = {
        "compilation_mode": "opt",
  },
)

transition_attached(
    name = "top_level",
    compilation_mode = select(
        {
            ":dbg": "dbg",
            ":opt": "opt",
        },
    ),
)
EOF
  bazel build --compilation_mode=fastbuild "//${pkg}:top_level" &> $TEST_log && fail "Build did NOT complete successfully"
  expect_log "configurable attribute \"compilation_mode\" in //test_unresolvable_select_error_out_after_applying_rule_transition:top_level doesn't match this configuration. Would a default condition help?"
}

# Regression test for b/338660045
function write_rule_list_transition() {
  local pkg="${1}"
  mkdir -p "${pkg}"

  # Create transition definition
  cat > "${pkg}/def.bzl" <<EOF
load("//settings:flag.bzl", "BuildSettingInfo")

example_package = "${pkg}"

# Transition that checks a list-typed attribute.
def _transition_impl(settings, attr):
    values = getattr(attr, "values")
    sorted_values = sorted(values)
    print("From transition: values = %s" % str(values))
    print("From transition: sorted values = %s" % str(sorted_values))
    return {"//%s:transition_output_flag" % example_package: sorted_values}

example_transition = transition(
    implementation = _transition_impl,
    inputs = [],
    outputs = ["//%s:transition_output_flag" % example_package],
)

def _rule_impl(ctx):
    attr_values = ctx.attr.values
    print("From rule attributes: values = %s" % str(attr_values))
    flag_values = ctx.attr._transition_output_flag[BuildSettingInfo].value
    print("From rule flag: values = %s" % str(flag_values))

    log = ctx.outputs.log
    content = "attr: %s\nflag: %s" % (str(attr_values), str(flag_values))
    ctx.actions.write(
        output = log,
        content = content,
    )

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "values": attr.string_list(),
        "_transition_output_flag": attr.label(default = "//%s:transition_output_flag" % example_package),
        "log": attr.output(),
    },
)
EOF

  # Create rules with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
)
load("//settings:flag.bzl", "bool_flag", "string_list_flag")

bool_flag(
    name = "select_flag",
    build_setting_default = True,
)

config_setting(
    name = "select_setting",
    flag_values = {":select_flag": "True"},
)

string_list_flag(
    name = "transition_output_flag",
    build_setting_default = [],
)

FRUITS = [
    # Deliberately not sorted.
    "banana",
    "grape",
    "apple",
]
ROCKS = [
    # Deliberately not sorted.
    "marble",
    "granite",
    "sandstone",
]

transition_attached(
    name = "top_level",
    log = "top_level.log",
    values = select({
        ":select_setting": FRUITS,
        "//conditions:default": ROCKS,
    }),
)
EOF
}

function test_inspect_attribute_list_direct() {
  local -r pkg="${FUNCNAME[0]}"

  write_rule_list_transition "${pkg}"

  bazel build --//"${pkg}":select_flag "//${pkg}:top_level" &> $TEST_log || fail "Build failed"
  expect_log 'From rule attributes: values = \["banana", "grape", "apple"\]'
  expect_log 'From rule flag: values = \["apple", "banana", "grape"\]'

  bazel build --//"${pkg}":select_flag=false "//${pkg}:top_level" &> $TEST_log || fail "Build failed"
  expect_log 'From rule attributes: values = \["marble", "granite", "sandstone"\]'
  expect_log 'From rule flag: values = \["granite", "marble", "sandstone"\]'
}

function test_inspect_attribute_list_via_output() {
  local -r pkg="${FUNCNAME[0]}"

  write_rule_list_transition "${pkg}"

  # Build via the output: this was the actual issue in b/338660045
  bazel build --//"${pkg}":select_flag "//${pkg}:top_level.log" &> $TEST_log || fail "Build failed"
  expect_log 'From rule attributes: values = \["banana", "grape", "apple"\]'
  expect_log 'From rule flag: values = \["apple", "banana", "grape"\]'

  bazel build --//"${pkg}":select_flag=false "//${pkg}:top_level.log" &> $TEST_log || fail "Build failed"
  expect_log 'From rule attributes: values = \["marble", "granite", "sandstone"\]'
  expect_log 'From rule flag: values = \["granite", "marble", "sandstone"\]'
}

function create_rule_transition_with_failure() {
  local pkg="${1}"

  # create transition definition
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF
example_package = "${pkg}"

def _transition_impl(settings, attr):
    my_map = {'x': 1, 'y': 2}
    # since my_map does not have a key for the input flag, the transition
    # will fail.
    res = my_map[settings["//%s:transition_input_flag" % example_package]]
    return {
      "//%s:transition_output_flag" % example_package: False,
    }

example_transition = transition(
    implementation = _transition_impl,
    inputs = ["//%s:transition_input_flag" % example_package],
    outputs = [
      "//%s:transition_output_flag" % example_package,
    ],
)

def _rule_impl(ctx):
    pass

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
)

no_transition_rule = rule(
    implementation = _rule_impl,
    attrs = {
        "deps": attr.label_list(),
    },
)

EOF

  # create top level target depending on a rule with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached", "no_transition_rule",
)
load("//settings:flag.bzl", "bool_flag")

bool_flag(
    name = "transition_input_flag",
    build_setting_default = True,
)

bool_flag(
    name = "transition_output_flag",
    build_setting_default = False,
)

transition_attached(
    name = "dep_with_transition",
)

no_transition_rule(
    name = "top_level",
    deps = [":dep_with_transition"],
)
EOF
}

function test_rule_transition_failure_doesnot_crash_bazel_with_keep_going() {
  local -r pkg="${FUNCNAME[0]}"
  create_rule_transition_with_failure "${pkg}"

  bazel build --keep_going "//${pkg}:top_level" &> $TEST_log && fail "Build did NOT complete successfully"
  # The build should fail with the transition failure.
  expect_log "Error: key True not found in dictionary"
  expect_log "${pkg}/BUILD:17:20: Errors encountered while applying Starlark transition"

  # Bazel should not crash.
  expect_not_log "IllegalStateException"
}

function test_rule_transition_failure_doesnot_crash_bazel_with_nokeep_going() {
  local -r pkg="${FUNCNAME[0]}"
  create_rule_transition_with_failure "${pkg}"

  bazel build --nokeep_going "//${pkg}:top_level" &> $TEST_log && fail "Build did NOT complete successfully"
  # The build should fail with the transition failure.
  expect_log "Error: key True not found in dictionary"
  expect_log "${pkg}/BUILD:17:20: Errors encountered while applying Starlark transition"

  # Bazel should not crash.
  expect_not_log "IllegalStateException"
}

run_suite "rule transition tests"
