# Copyright 2018 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.

import unittest

from py import mock

from google.protobuf import text_format
from third_party.com.github.bazelbuild.bazel.src.main.protobuf import crosstool_config_pb2
from tools.migration.ctoolchain_comparator_lib import compare_ctoolchains

try:
  # Python 2
  from cStringIO import StringIO
except ImportError:
  # Python 3
  from io import StringIO


def make_toolchain(toolchain_proto):
  toolchain = crosstool_config_pb2.CToolchain()
  text_format.Merge(toolchain_proto, toolchain)
  return toolchain


class CtoolchainComparatorLibTest(unittest.TestCase):

  def test_string_fields(self):
    first = make_toolchain("""
          toolchain_identifier: "first-id"
          host_system_name: "first-host"
          target_system_name: "first-target"
          target_cpu: "first-cpu"
          target_libc: "first-libc"
          compiler: "first-compiler"
          abi_version: "first-abi"
          abi_libc_version: "first-abi-libc"
          builtin_sysroot: "sysroot"
        """)
    second = make_toolchain("""
          toolchain_identifier: "second-id"
          host_system_name: "second-host"
          target_system_name: "second-target"
          target_cpu: "second-cpu"
          target_libc: "second-libc"
          compiler: "second-compiler"
          abi_version: "second-abi"
          abi_libc_version: "second-abi-libc"
          cc_target_os: "os"
        """)
    error_toolchain_identifier = (
        "Difference in 'toolchain_identifier' field:\n"
        "Value before change:\t'first-id'\n"
        "Value after change:\t'second-id'\n"
    )
    error_host_system_name = (
        "Difference in 'host_system_name' field:\n"
        "Value before change:\t'first-host'\n"
        "Value after change:\t'second-host'\n"
    )
    error_target_system_name = (
        "Difference in 'target_system_name' field:\n"
        "Value before change:\t'first-target'\n"
        "Value after change:\t'second-target'\n"
    )
    error_target_cpu = (
        "Difference in 'target_cpu' field:\n"
        "Value before change:\t'first-cpu'\n"
        "Value after change:\t'second-cpu'\n"
    )
    error_target_libc = (
        "Difference in 'target_libc' field:\n"
        "Value before change:\t'first-libc'\n"
        "Value after change:\t'second-libc'\n"
    )
    error_compiler = (
        "Difference in 'compiler' field:\n"
        "Value before change:\t'first-compiler'\n"
        "Value after change:\t'second-compiler'\n"
    )
    error_abi_version = (
        "Difference in 'abi_version' field:\n"
        "Value before change:\t'first-abi'\n"
        "Value after change:\t'second-abi'\n"
    )
    error_abi_libc_version = (
        "Difference in 'abi_libc_version' field:\n"
        "Value before change:\t'first-abi-libc'\n"
        "Value after change:\t'second-abi-libc'\n"
    )
    error_builtin_sysroot = (
        "Difference in 'builtin_sysroot' field:\n"
        "Value before change is set to 'sysroot'\n"
        "Value after change is not set\n"
    )
    error_cc_target_os = (
        "Difference in 'cc_target_os' field:\n"
        "Value before change is not set\n"
        "Value after change is set to 'os'\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_toolchain_identifier, mock_stdout.getvalue())
      self.assertIn(error_host_system_name, mock_stdout.getvalue())
      self.assertIn(error_target_system_name, mock_stdout.getvalue())
      self.assertIn(error_target_cpu, mock_stdout.getvalue())
      self.assertIn(error_target_libc, mock_stdout.getvalue())
      self.assertIn(error_compiler, mock_stdout.getvalue())
      self.assertIn(error_abi_version, mock_stdout.getvalue())
      self.assertIn(error_abi_libc_version, mock_stdout.getvalue())
      self.assertIn(error_builtin_sysroot, mock_stdout.getvalue())
      self.assertIn(error_cc_target_os, mock_stdout.getvalue())

  def test_tool_path(self):
    first = make_toolchain("""
        tool_path {
          name: "only_first"
          path: "/a/b/c"
        }
        tool_path {
          name: "paths_differ"
          path: "/path/first"
        }
    """)
    second = make_toolchain("""
        tool_path {
          name: "paths_differ"
          path: "/path/second"
        }
        tool_path {
          name: "only_second_1"
          path: "/a/b/c"
        }
        tool_path {
          name: "only_second_2"
          path: "/a/b/c"
        }
    """)
    error_only_first = (
        "* List before change contains entries for the "
        "following tools that the list after the change "
        "doesn't:\n[only_first]\n"
    )
    error_only_second = (
        "* List after change contains entries for the "
        "following tools that the list before the change "
        "doesn't:\n"
        "[\n"
        "\tonly_second_1\n"
        "\tonly_second_2\n"
        "]\n"
    )
    error_paths_differ = (
        "* Path for tool 'paths_differ' differs before and "
        "after the change:\n"
        "Value before change:\t'/path/first'\n"
        "Value after change:\t'/path/second'\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_only_first, mock_stdout.getvalue())
      self.assertIn(error_only_second, mock_stdout.getvalue())
      self.assertIn(error_paths_differ, mock_stdout.getvalue())

  def test_make_variable(self):
    first = make_toolchain("""
        make_variable {
          name: "only_first"
          value: "val"
        }
        make_variable {
          name: "value_differs"
          value: "first_value"
        }
    """)
    second = make_toolchain("""
        make_variable {
          name: "value_differs"
          value: "second_value"
        }
        make_variable {
          name: "only_second_1"
          value: "val"
        }
        make_variable {
          name: "only_second_2"
          value: "val"
        }
    """)
    error_only_first = (
        "* List before change contains entries for the "
        "following variables that the list after the "
        "change doesn't:\n[only_first]\n"
    )
    error_only_second = (
        "* List after change contains entries for the "
        "following variables that the list before the "
        "change doesn't:\n"
        "[\n"
        "\tonly_second_1\n"
        "\tonly_second_2\n"
        "]\n"
    )
    error_value_differs = (
        "* Value for variable 'value_differs' differs before"
        " and after the change:\n"
        "Value before change:\t'first_value'\n"
        "Value after change:\t'second_value'\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_only_first, mock_stdout.getvalue())
      self.assertIn(error_only_second, mock_stdout.getvalue())
      self.assertIn(error_value_differs, mock_stdout.getvalue())

  def test_cxx_builtin_include_directories(self):
    first = make_toolchain("""
        cxx_builtin_include_directory: "a/b/c"
        cxx_builtin_include_directory: "d/e/f"
    """)
    second = make_toolchain("""
        cxx_builtin_include_directory: "d/e/f"
        cxx_builtin_include_directory: "a/b/c"
    """)
    expect_error = (
        "Difference in 'cxx_builtin_include_directory' field:\n"
        "List of elements before change:\n"
        "[\n"
        "\ta/b/c\n"
        "\td/e/f\n"
        "]\n"
        "List of elements after change:\n"
        "[\n"
        "\td/e/f\n"
        "\ta/b/c\n"
        "]\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(expect_error, mock_stdout.getvalue())

  def test_artifact_name_pattern(self):
    first = make_toolchain("""
        artifact_name_pattern {
          category_name: 'object_file'
          prefix: ''
          extension: '.obj1'
        }
        artifact_name_pattern {
          category_name: 'executable'
          prefix: 'first'
          extension: '.exe'
        }
        artifact_name_pattern {
          category_name: 'dynamic_library'
          prefix: ''
          extension: '.dll'
        }
    """)
    second = make_toolchain("""
        artifact_name_pattern {
          category_name: 'object_file'
          prefix: ''
          extension: '.obj2'
        }
        artifact_name_pattern {
          category_name: 'static_library'
          prefix: ''
          extension: '.lib'
        }
        artifact_name_pattern {
          category_name: 'executable'
          prefix: 'second'
          extension: '.exe'
        }
        artifact_name_pattern {
          category_name: 'interface_library'
          prefix: ''
          extension: '.if.lib'
        }
    """)
    error_only_first = (
        "* List before change contains entries for the "
        "following categories that the list after the "
        "change doesn't:\n[dynamic_library]\n"
    )
    error_only_second = (
        "* List after change contains entries for the "
        "following categories that the list before the "
        "change doesn't:\n"
        "[\n"
        "\tinterface_library\n"
        "\tstatic_library\n"
        "]\n"
    )
    error_extension_differs = (
        "* Value for category 'object_file' differs "
        "before and after the change:\n"
        "Value before change:"
        "\tprefix:''"
        "\textension:'.obj1'\n"
        "Value after change:"
        "\tprefix:''"
        "\textension:'.obj2'\n"
    )
    error_prefix_differs = (
        "* Value for category 'executable' differs "
        "before and after the change:\n"
        "Value before change:"
        "\tprefix:'first'"
        "\textension:'.exe'\n"
        "Value after change:"
        "\tprefix:'second'"
        "\textension:'.exe'\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_only_first, mock_stdout.getvalue())
      self.assertIn(error_only_second, mock_stdout.getvalue())
      self.assertIn(error_extension_differs, mock_stdout.getvalue())
      self.assertIn(error_prefix_differs, mock_stdout.getvalue())

  def test_features_not_ordered(self):
    first = make_toolchain("""
        feature {
          name: 'feature1'
        }
        feature {
          name: 'feature2'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature2'
        }
        feature {
          name: 'feature1'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("Features not in right order", mock_stdout.getvalue())

  def test_features_missing(self):
    first = make_toolchain("""
        feature {
          name: 'feature1'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature2'
        }
    """)
    error_only_first = (
        "* List before change contains entries for the "
        "following features that the list after the "
        "change doesn't:\n[feature1]\n"
    )
    error_only_second = (
        "* List after change contains entries for the "
        "following features that the list before the "
        "change doesn't:\n[feature2]\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_only_first, mock_stdout.getvalue())
      self.assertIn(error_only_second, mock_stdout.getvalue())

  def test_feature_enabled(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          enabled: true
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          enabled: false
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )

  def test_feature_provides(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          provides: 'a'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          provides: 'b'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_provides_preserves_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          provides: 'a'
          provides: 'b'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          provides: 'b'
          provides: 'a'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_implies(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          implies: 'a'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_implies_preserves_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          implies: 'a'
          implies: 'b'
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          implies: 'b'
          implies: 'a'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_requires_preserves_list_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature1'
          }
          requires: {
            feature: 'feature2'
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature2'
          }
          requires: {
            feature: 'feature1'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_requires_ignores_required_features_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature1'
            feature: 'feature2'
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature2'
            feature: 'feature1'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_feature_requires_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature1'
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          requires: {
            feature: 'feature2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_action_config_ignores_requires(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          requires: {
            feature: 'feature1'
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          requires: {
            feature: 'feature2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_env_set_actions_differ(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            action: 'a1'
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set: {
            action: 'a1'
            action: 'a2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_env_set_ignores_actions_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            action: 'a2'
            action: 'a1'
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set: {
            action: 'a1'
            action: 'a2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_env_set_env_entries_not_ordered(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'k1'
              value: 'v1'
            }
            env_entry {
              key: 'k2'
              value: 'v2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'k2'
              value: 'v2'
            }
            env_entry {
              key: 'k1'
              value: 'v1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_env_set_env_entries_differ(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'k1'
              value: 'value_first'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'k1'
              value: 'value_second'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_feature_preserves_env_set_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'first'
              value: 'first'
            }
          }
          env_set {
            env_entry {
              key: 'second'
              value: 'second'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            env_entry {
              key: 'second'
              value: 'second'
            }
          }
          env_set {
            env_entry {
              key: 'first'
              value: 'first'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after the change:",
          mock_stdout.getvalue(),
      )

  def test_action_config_ignores_env_set(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          env_set {
            env_entry {
              key: 'k1'
              value: 'value_first'
            }
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          env_set {
            env_entry {
              key: 'k1'
              value: 'value_second'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_env_set_ignores_with_feature_set_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set{
            with_feature {
              feature: 'feature1'
            }
            with_feature {
              not_feature: 'feature2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set {
            with_feature {
              not_feature: 'feature2'
            }
            with_feature {
              feature: 'feature1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_env_set_ignores_with_feature_set_lists_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          env_set{
            with_feature {
              feature: 'feature1'
              feature: 'feature2'
              not_feature: 'not_feature1'
              not_feature: 'not_feature2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          env_set{
            with_feature {
              feature: 'feature2'
              feature: 'feature1'
              not_feature: 'not_feature2'
              not_feature: 'not_feature1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_set_ignores_actions_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set {
             action: 'a1'
             action: 'a2'
          }
        }
    """)
    second = make_toolchain("""
       feature {
          name: 'feature'
          flag_set {
             action: 'a2'
             action: 'a1'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_action_config_flag_set_actions_ignored(self):
    first = make_toolchain("""
      action_config {
          config_name: 'config'
          flag_set {
            action: 'a1'
          }
        }
    """)
    second = make_toolchain("""
      action_config {
          config_name: 'config'
          flag_set {
            action: 'a2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_set_ignores_with_feature_set_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set {
            with_feature {
              feature: 'feature1'
            }
            with_feature {
              not_feature: 'feature2'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set {
            with_feature {
              feature: 'feature1'
            }
            with_feature {
              not_feature: 'feature2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set {
            with_feature {
              not_feature: 'feature2'
            }
            with_feature {
              feature: 'feature1'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set {
            with_feature {
              not_feature: 'feature2'
            }
            with_feature {
              feature: 'feature1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_set_ignores_with_feature_set_lists_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            with_feature {
              feature: 'feature1'
              feature: 'feature2'
              not_feature: 'not_feature1'
              not_feature: 'not_feature2'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            with_feature {
              feature: 'feature1'
              feature: 'feature2'
              not_feature: 'not_feature1'
              not_feature: 'not_feature2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            with_feature {
              feature: 'feature2'
              feature: 'feature1'
              not_feature: 'not_feature2'
              not_feature: 'not_feature1'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            with_feature {
              feature: 'feature2'
              feature: 'feature1'
              not_feature: 'not_feature2'
              not_feature: 'not_feature1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_set_preserves_flag_group_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set {
            flag_group {
              flag: 'a'
            }
            flag_group {
              flag: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set {
             flag_group {
               flag: 'a'
             }
             flag_group {
               flag: 'b'
             }
          }
        }
    """)
    second = make_toolchain("""
       feature {
          name: 'feature'
          flag_set {
            flag_group {
              flag: 'b'
            }
            flag_group {
              flag: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set {
            flag_group {
              flag: 'b'
            }
            flag_group {
              flag: 'a'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_preserves_flags_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              flag: 'flag1'
              flag: 'flag2'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              flag: 'flag1'
              flag: 'flag2'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              flag: 'flag2'
              flag: 'flag1'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              flag: 'flag2'
              flag: 'flag1'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_iterate_over_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              iterate_over: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              iterate_over: 'a'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              iterate_over: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              iterate_over: 'b'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_expand_if_true_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_true: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_true: 'a'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_true: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_true: 'b'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_expand_if_false_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_false: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_false: 'a'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_false: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_false: 'b'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_expand_if_all_available_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_all_available: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_all_available: 'a'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_all_available: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_all_available: 'b'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_expand_if_none_available_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_none_available: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_none_available: 'a'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_none_available: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_none_available: 'b'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_expand_if_all_available_ignores_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_all_available: 'a'
              expand_if_all_available: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_all_available: 'a'
              expand_if_all_available: 'b'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_all_available: 'b'
              expand_if_all_available: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_all_available: 'b'
              expand_if_all_available: 'a'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_group_expand_if_none_available_ignores_order(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_none_available: 'a'
              expand_if_none_available: 'b'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_none_available: 'a'
              expand_if_none_available: 'b'
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_none_available: 'b'
              expand_if_none_available: 'a'
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_none_available: 'b'
              expand_if_none_available: 'a'
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_flag_group_expand_if_equal_differs(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_equal {
                variable: 'first'
                value: 'val'
              }
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_equal {
                variable: 'first'
                value: 'val'
              }
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              expand_if_equal {
                variable: 'second'
                value: 'val'
              }
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              expand_if_equal {
                variable: 'second'
                value: 'val'
              }
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_flag_group_flag_groups_differ(self):
    first = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              flag_group {
                flag: 'a'
                flag: 'b'
              }
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              flag_group {
                flag: 'a'
                flag: 'b'
              }
            }
          }
        }
    """)
    second = make_toolchain("""
        feature {
          name: 'feature'
          flag_set{
            flag_group {
              flag_group {
                flag: 'b'
                flag: 'a'
              }
            }
          }
        }
        action_config {
          config_name: 'config'
          flag_set{
            flag_group {
              flag_group {
                flag: 'b'
                flag: 'a'
              }
            }
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Feature 'feature' differs before and after", mock_stdout.getvalue()
      )
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_configs_not_ordered(self):
    first = make_toolchain("""
        action_config {
          config_name: 'action1'
        }
        action_config {
          config_name: 'action2'
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'action2'
        }
        action_config {
          config_name: 'action1'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("Action configs not in right order", mock_stdout.getvalue())

  def test_action_configs_missing(self):
    first = make_toolchain("""
        action_config {
          config_name: 'action1'
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'action2'
        }
    """)
    error_only_first = (
        "* List before change contains entries for the "
        "following action_configs that the list after the "
        "change doesn't:\n[action1]\n"
    )
    error_only_second = (
        "* List after change contains entries for the "
        "following action_configs that the list before the "
        "change doesn't:\n[action2]\n"
    )
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(error_only_first, mock_stdout.getvalue())
      self.assertIn(error_only_second, mock_stdout.getvalue())

  def test_action_config_enabled(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          enabled: true
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          enabled: false
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_config_action_name(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          action_name: 'config1'
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          action_name: 'config2'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_config_tool_tool_path_differs(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            tool_path: 'path1'
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            tool_path: 'path2'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_config_tool_execution_requirements_differ(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            execution_requirement: 'a'
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            execution_requirement: 'b'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_config_tool_execution_requirements_ignores_order(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            execution_requirement: 'a'
            execution_requirement: 'b'
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            execution_requirement: 'b'
            execution_requirement: 'a'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_action_config_implies_differs(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          implies: 'a'
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          implies: 'b'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_action_config_implies_preserves_order(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          implies: 'a'
          implies: 'b'
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          implies: 'b'
          implies: 'a'
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn(
          "* Action config 'config' differs before and after",
          mock_stdout.getvalue(),
      )

  def test_unused_tool_path(self):
    first = make_toolchain("""
        tool_path {
          name: "empty"
          path: ""
        }
    """)
    second = make_toolchain("""
        tool_path {
          name: "empty"
          path: "NOT_USED"
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())

  def test_unused_tool_path_in_tool(self):
    first = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            tool_path: ''
          }
        }
    """)
    second = make_toolchain("""
        action_config {
          config_name: 'config'
          tool {
            tool_path: 'NOT_USED'
          }
        }
    """)
    mock_stdout = StringIO()
    with mock.patch("sys.stdout", mock_stdout):
      compare_ctoolchains(first, second)
      self.assertIn("No difference", mock_stdout.getvalue())


if __name__ == "__main__":
  unittest.main()
