#!/bin/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 ---
# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
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; }

function set_up() {
  create_new_workspace
}

function create_transitions() {
  local pkg="${1}"
  mkdir -p "${pkg}"
  cat > "${pkg}/def.bzl" <<EOF

load("//third_party/bazel_skylib/rules:common_settings.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),
        "_allowlist_function_transition": attr.label(
            default = "//tools/allowlists/function_transition_allowlist:function_transition_allowlist",
        ),
    },
)

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("//third_party/bazel_skylib/rules:common_settings.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("//third_party/bazel_skylib/rules:common_settings.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),
        "_allowlist_function_transition": attr.label(
            default = "//tools/allowlists/function_transition_allowlist:function_transition_allowlist",
        ),
    },
)
EOF

  # create rules with transition attached
  cat > "${pkg}/BUILD" <<EOF
load(
    "//${pkg}:def.bzl",
    "transition_attached",
)
load("//third_party/bazel_skylib/rules:common_settings.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("//third_party/bazel_skylib/rules:common_settings.bzl", "BuildSettingInfo")

example_package = "${pkg}"

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

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

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

transition_attached = rule(
    implementation = _rule_impl,
    cfg = example_transition,
    attrs = {
        "cpu_name": attr.string(),
        "_allowlist_function_transition": attr.label(
            default = "//tools/allowlists/function_transition_allowlist:function_transition_allowlist",
        ),
    },
)
EOF

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

config_setting(
  name = "ios_x86_64",
  values = {
        "cpu": "ios_x86_64",
  },
)

config_setting(
  name = "darwin",
  values = {
        "cpu": "darwin",
  },
)

transition_attached(
    name = "top_level",
    cpu_name = select(
        {
            ":ios_x86_64": "ios_x86_64",
            ":darwin": "darwin",
        },
    ),
)
EOF
  bazel build "//${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("//third_party/bazel_skylib/rules:common_settings.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 = {
        "cpu_name": attr.string(),
        "_allowlist_function_transition": attr.label(
            default = "//tools/allowlists/function_transition_allowlist:function_transition_allowlist",
        ),
    },
)
EOF

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

config_setting(
  name = "ios_x86_64",
  values = {
        "cpu": "ios_x86_64",
  },
)

config_setting(
  name = "darwin",
  values = {
        "cpu": "darwin",
  },
)

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

run_suite "rule transition tests"
