#!/usr/bin/env python
# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.

import cStringIO
import logging
import os
import sys
import tempfile
import unittest

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(
    __file__.decode(sys.getfilesystemencoding()))))
sys.path.insert(0, ROOT_DIR)
sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party'))

import isolate_format
from depot_tools import auto_stub
from depot_tools import fix_encoding
from utils import file_path


# Access to a protected member XXX of a client class
# pylint: disable=W0212


FAKE_DIR = (
    u'z:\\path\\to\\non_existing'
    if sys.platform == 'win32' else u'/path/to/non_existing')


class IsolateFormatTest(auto_stub.TestCase):
  def test_unknown_key(self):
    try:
      isolate_format.verify_variables({'foo': [],})
      self.fail()
    except AssertionError:
      pass

  def test_unknown_var(self):
    try:
      isolate_format.verify_condition({'variables': {'foo': [],}}, {})
      self.fail()
    except AssertionError:
      pass

  def test_eval_content(self):
    try:
      # Intrinsics are not available.
      isolate_format.eval_content('map(str, [1, 2])')
      self.fail()
    except NameError:
      pass

  def test_load_isolate_as_config_empty(self):
    expected = {
      (): {
        'isolate_dir': FAKE_DIR,
      },
    }
    self.assertEqual(
        expected,
        isolate_format.load_isolate_as_config(FAKE_DIR, {}, None).flatten())

  def test_load_isolate_as_config(self):
    value = {
      'conditions': [
        ['OS=="amiga" or OS=="atari" or OS=="coleco" or OS=="dendy"', {
          'variables': {
            'files': ['a', 'b', 'touched'],
          },
        }],
        ['OS=="atari"', {
          'variables': {
            'files': ['c', 'd', 'touched_a', 'x'],
            'command': ['echo', 'Hello World'],
            'read_only': 2,
          },
        }],
        ['OS=="amiga" or OS=="coleco" or OS=="dendy"', {
          'variables': {
            'files': ['e', 'f', 'touched_e', 'x'],
            'command': ['echo', 'You should get an Atari'],
          },
        }],
        ['OS=="amiga"', {
          'variables': {
            'files': ['g'],
            'read_only': 1,
          },
        }],
        ['OS=="amiga" or OS=="atari" or OS=="dendy"', {
          'variables': {
            'files': ['h'],
          },
        }],
      ],
    }
    expected = {
      (None,): {
        'isolate_dir': FAKE_DIR,
      },
      ('amiga',): {
        'files': ['a', 'b', 'e', 'f', 'g', 'h', 'touched', 'touched_e', 'x'],
        'command': ['echo', 'You should get an Atari'],
        'isolate_dir': FAKE_DIR,
        'read_only': 1,
      },
      ('atari',): {
        'files': ['a', 'b', 'c', 'd', 'h', 'touched', 'touched_a', 'x'],
        'command': ['echo', 'Hello World'],
        'isolate_dir': FAKE_DIR,
        'read_only': 2,
      },
      ('coleco',): {
        'files': ['a', 'b', 'e', 'f', 'touched', 'touched_e', 'x'],
        'command': ['echo', 'You should get an Atari'],
        'isolate_dir': FAKE_DIR,
      },
      ('dendy',): {
        'files': ['a', 'b', 'e', 'f', 'h', 'touched', 'touched_e', 'x'],
        'command': ['echo', 'You should get an Atari'],
        'isolate_dir': FAKE_DIR,
      },
    }
    self.assertEqual(
        expected, isolate_format.load_isolate_as_config(
            FAKE_DIR, value, None).flatten())

  def test_load_isolate_as_config_duplicate_command(self):
    value = {
      'variables': {
        'command': ['rm', '-rf', '/'],
      },
      'conditions': [
        ['OS=="atari"', {
          'variables': {
            'command': ['echo', 'Hello World'],
          },
        }],
      ],
    }
    try:
      isolate_format.load_isolate_as_config(FAKE_DIR, value, None)
      self.fail()
    except AssertionError:
      pass

  def test_load_isolate_as_config_no_variable(self):
    value = {
      'variables': {
        'command': ['echo', 'You should get an Atari'],
        'files': ['a', 'b', 'touched'],
        'read_only': 1,
      },
    }
    # The key is the empty tuple, since there is no variable to bind to.
    expected = {
      (): {
        'command': ['echo', 'You should get an Atari'],
        'files': ['a', 'b', 'touched'],
        'isolate_dir': FAKE_DIR,
        'read_only': 1,
      },
    }
    self.assertEqual(
        expected, isolate_format.load_isolate_as_config(
            FAKE_DIR, value, None).flatten())

  def test_merge_two_empty(self):
    # Flat stay flat. Pylint is confused about union() return type.
    # pylint: disable=E1103
    actual = isolate_format.Configs(None, ()).union(
        isolate_format.load_isolate_as_config(FAKE_DIR, {}, None)).union(
            isolate_format.load_isolate_as_config(FAKE_DIR, {}, None))
    expected = {
      (): {
        'isolate_dir': FAKE_DIR,
      },
    }
    self.assertEqual(expected, actual.flatten())

  def test_load_two_conditions(self):
    linux = {
      'conditions': [
        ['OS=="linux"', {
          'variables': {
            'files': [
              'file_linux',
              'file_common',
            ],
          },
        }],
      ],
    }
    mac = {
      'conditions': [
        ['OS=="mac"', {
          'variables': {
            'files': [
              'file_mac',
              'file_common',
            ],
          },
        }],
      ],
    }
    expected = {
      (None,): {
        'isolate_dir': FAKE_DIR,
      },
      ('linux',): {
        'files': ['file_common', 'file_linux'],
        'isolate_dir': FAKE_DIR,
      },
      ('mac',): {
        'files': ['file_common', 'file_mac'],
        'isolate_dir': FAKE_DIR,
      },
    }
    # Pylint is confused about union() return type.
    # pylint: disable=E1103
    configs = isolate_format.Configs(None, ()).union(
        isolate_format.load_isolate_as_config(FAKE_DIR, linux, None)).union(
            isolate_format.load_isolate_as_config(FAKE_DIR, mac, None)
        ).flatten()
    self.assertEqual(expected, configs)

  def test_load_three_conditions(self):
    linux = {
      'conditions': [
        ['OS=="linux" and chromeos==1', {
          'variables': {
            'files': [
              'file_linux',
              'file_common',
            ],
          },
        }],
      ],
    }
    mac = {
      'conditions': [
        ['OS=="mac" and chromeos==0', {
          'variables': {
            'files': [
              'file_mac',
              'file_common',
            ],
          },
        }],
      ],
    }
    win = {
      'conditions': [
        ['OS=="win" and chromeos==0', {
          'variables': {
            'files': [
              'file_win',
              'file_common',
            ],
          },
        }],
      ],
    }
    expected = {
      (None, None): {
        'isolate_dir': FAKE_DIR,
      },
      ('linux', 1): {
        'files': ['file_common', 'file_linux'],
        'isolate_dir': FAKE_DIR,
      },
      ('mac', 0): {
        'files': ['file_common', 'file_mac'],
        'isolate_dir': FAKE_DIR,
      },
      ('win', 0): {
        'files': ['file_common', 'file_win'],
        'isolate_dir': FAKE_DIR,
      },
    }
    # Pylint is confused about union() return type.
    # pylint: disable=E1103
    configs = isolate_format.Configs(None, ()).union(
        isolate_format.load_isolate_as_config(FAKE_DIR, linux, None)).union(
            isolate_format.load_isolate_as_config(FAKE_DIR, mac, None)).union(
                isolate_format.load_isolate_as_config(FAKE_DIR, win, None))
    self.assertEqual(expected, configs.flatten())

  def test_safe_index(self):
    self.assertEqual(1, isolate_format._safe_index(('a', 'b'), 'b'))
    self.assertEqual(None, isolate_format._safe_index(('a', 'b'), 'c'))

  def test_get_map_keys(self):
    self.assertEqual(
        (0, None, 1), isolate_format._get_map_keys(('a', 'b', 'c'), ('a', 'c')))

  def test_map_keys(self):
    self.assertEqual(
        ('a', None, 'c'),
        isolate_format._map_keys((0, None, 1), ('a', 'c')))

  def test_load_multi_variables(self):
    # Load an .isolate with different condition on different variables.
    data = {
      'conditions': [
        ['OS=="abc"', {
          'variables': {
            'command': ['bar'],
          },
        }],
        ['CHROMEOS=="1"', {
          'variables': {
            'command': ['foo'],
          },
        }],
      ],
    }
    configs = isolate_format.load_isolate_as_config(FAKE_DIR, data, None)
    self.assertEqual(('CHROMEOS', 'OS'), configs.config_variables)
    flatten = dict((k, v.flatten()) for k, v in configs._by_config.iteritems())
    expected = {
      (None, None): {
        'isolate_dir': FAKE_DIR,
      },
      (None, 'abc'): {
        'command': ['bar'],
        'isolate_dir': FAKE_DIR,
      },
      ('1', None): {
        'command': ['foo'],
        'isolate_dir': FAKE_DIR,
      },
      # TODO(maruel): It is a conflict.
      ('1', 'abc'): {
        'command': ['bar'],
        'isolate_dir': FAKE_DIR,
      },
    }
    self.assertEqual(expected, flatten)

  def test_union_multi_variables(self):
    data1 = {
      'conditions': [
        ['OS=="abc"', {
          'variables': {
            'command': ['bar'],
          },
        }],
      ],
    }
    data2 = {
      'conditions': [
        ['CHROMEOS=="1"', {
          'variables': {
            'command': ['foo'],
          },
        }],
      ],
    }
    configs1 = isolate_format.load_isolate_as_config(FAKE_DIR, data1, None)
    configs2 = isolate_format.load_isolate_as_config(FAKE_DIR, data2, None)
    configs = configs1.union(configs2)
    self.assertEqual(('CHROMEOS', 'OS'), configs.config_variables)
    flatten = dict((k, v.flatten()) for k, v in configs._by_config.iteritems())
    expected = {
      (None, None): {
        'isolate_dir': FAKE_DIR,
      },
      (None, 'abc'): {
        'command': ['bar'],
        'isolate_dir': FAKE_DIR,
      },
      ('1', None): {
        'command': ['foo'],
        'isolate_dir': FAKE_DIR,
      },
    }
    self.assertEqual(expected, flatten)

  def test_ConfigSettings_union(self):
    lhs_values = {}
    rhs_values = {'files': ['data/', 'test/data/']}
    lhs = isolate_format.ConfigSettings(lhs_values, '/src/net/third_party/nss')
    rhs = isolate_format.ConfigSettings(rhs_values, '/src/base')
    out = lhs.union(rhs)
    expected = {
      'files': ['data/', 'test/data/'],
      'isolate_dir': '/src/base',
    }
    self.assertEqual(expected, out.flatten())

  def test_configs_comment(self):
    # Pylint is confused with isolate_format.union() return type.
    # pylint: disable=E1103
    configs = isolate_format.load_isolate_as_config(
            FAKE_DIR, {}, '# Yo dawg!\n# Chill out.\n').union(
        isolate_format.load_isolate_as_config(FAKE_DIR, {}, None))
    self.assertEqual('# Yo dawg!\n# Chill out.\n', configs.file_comment)

    configs = isolate_format.load_isolate_as_config(FAKE_DIR, {}, None).union(
        isolate_format.load_isolate_as_config(
            FAKE_DIR, {}, '# Yo dawg!\n# Chill out.\n'))
    self.assertEqual('# Yo dawg!\n# Chill out.\n', configs.file_comment)

    # Only keep the first one.
    configs = isolate_format.load_isolate_as_config(
        FAKE_DIR, {}, '# Yo dawg!\n').union(
            isolate_format.load_isolate_as_config(
                FAKE_DIR, {}, '# Chill out.\n'))
    self.assertEqual('# Yo dawg!\n', configs.file_comment)

  def test_extract_comment(self):
    self.assertEqual(
        '# Foo\n# Bar\n', isolate_format.extract_comment('# Foo\n# Bar\n{}'))
    self.assertEqual('', isolate_format.extract_comment('{}'))

  def _test_pretty_print_impl(self, value, expected):
    actual = cStringIO.StringIO()
    isolate_format.pretty_print(value, actual)
    self.assertEqual(expected.splitlines(), actual.getvalue().splitlines())

  def test_pretty_print_empty(self):
    self._test_pretty_print_impl({}, '{\n}\n')

  def test_pretty_print_mid_size(self):
    value = {
      'variables': {
        'files': [
          'file1',
          'file2',
        ],
      },
      'conditions': [
        ['OS==\"foo\"', {
          'variables': {
            'files': [
              'dir1/',
              'dir2/',
              'file3',
              'file4',
            ],
            'command': ['python', '-c', 'print "H\\i\'"'],
            'read_only': 2,
          },
        }],
        ['OS==\"bar\"', {
          'variables': {},
        }],
      ],
    }
    isolate_format.verify_root(value, {})
    # This is an .isolate format.
    expected = (
        "{\n"
        "  'variables': {\n"
        "    'files': [\n"
        "      'file1',\n"
        "      'file2',\n"
        "    ],\n"
        "  },\n"
        "  'conditions': [\n"
        "    ['OS==\"foo\"', {\n"
        "      'variables': {\n"
        "        'command': [\n"
        "          'python',\n"
        "          '-c',\n"
        "          'print \"H\\i\'\"',\n"
        "        ],\n"
        "        'files': [\n"
        "          'dir1/',\n"
        "          'dir2/',\n"
        "          'file3',\n"
        "          'file4',\n"
        "        ],\n"
        "        'read_only': 2,\n"
        "      },\n"
        "    }],\n"
        "    ['OS==\"bar\"', {\n"
        "      'variables': {\n"
        "      },\n"
        "    }],\n"
        "  ],\n"
        "}\n")
    self._test_pretty_print_impl(value, expected)

  def test_convert_old_to_new_else(self):
    isolate_with_else_clauses = {
      'conditions': [
        ['OS=="mac"', {
          'variables': {'foo': 'bar'},
        }, {
          'variables': {'x': 'y'},
        }],
      ],
    }
    with self.assertRaises(isolate_format.IsolateError):
      isolate_format.load_isolate_as_config(
          FAKE_DIR, isolate_with_else_clauses, None)

  def test_match_configs(self):
    expectations = [
        (
          ('OS=="win"', ('OS',), [('win',), ('mac',), ('linux',)]),
          [('win',)],
        ),
        (
          (
            '(foo==1 or foo==2) and bar=="b"',
            ['foo', 'bar'],
            [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')],
          ),
          [(1, 'b'), (2, 'b')],
        ),
        (
          (
            'bar=="b"',
            ['foo', 'bar'],
            [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')],
          ),
          # TODO(maruel): When a free variable match is found, it should not
          # list all the bounded values in addition. The problem is when an
          # intersection of two different bound variables that are tested singly
          # in two different conditions.
          [(1, 'b'), (2, 'b'), (None, 'b')],
        ),
        (
          (
            'foo==1 or bar=="b"',
            ['foo', 'bar'],
            [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')],
          ),
          # TODO(maruel): (None, 'b') would match.
          # It is hard in this case to realize that each of the variables 'foo'
          # and 'bar' can be unbounded in a specific case.
          [(1, 'a'), (1, 'b'), (2, 'b'), (1, None)],
        ),
    ]
    for data, expected in expectations:
      self.assertEqual(expected, isolate_format.match_configs(*data))

  def test_load_with_globals(self):
    values = {
      'variables': {
        'files': [
          'file_common',
        ],
      },
      'conditions': [
        ['OS=="linux"', {
          'variables': {
            'files': [
              'file_linux',
            ],
            'read_only': 1,
          },
        }],
        ['OS=="mac" or OS=="win"', {
          'variables': {
            'files': [
              'file_non_linux',
            ],
            'read_only': 0,
          },
        }],
      ],
    }
    expected = {
      (None,): {
        'files': [
          'file_common',
        ],
        'isolate_dir': FAKE_DIR,
      },
      ('linux',): {
        'files': [
          'file_linux',
        ],
        'isolate_dir': FAKE_DIR,
        'read_only': 1,
      },
      ('mac',): {
        'files': [
          'file_non_linux',
        ],
        'isolate_dir': FAKE_DIR,
        'read_only': 0,
      },
      ('win',): {
        'files': [
          'file_non_linux',
        ],
        'isolate_dir': FAKE_DIR,
        'read_only': 0,
      },
    }
    actual = isolate_format.load_isolate_as_config(FAKE_DIR, values, None)
    self.assertEqual(expected, actual.flatten())

  def test_and_or_bug(self):
    a = {
      'conditions': [
        ['use_x11==0', {
          'variables': {
            'command': ['foo', 'x11=0'],
          },
        }],
        ['OS=="linux" and chromeos==0', {
          'variables': {
            'command': ['foo', 'linux'],
            },
          }],
        ],
      }

    def load_included_isolate(isolate_dir, _isolate_path):
      return isolate_format.load_isolate_as_config(isolate_dir, a, None)
    self.mock(isolate_format, 'load_included_isolate', load_included_isolate)

    b = {
      'conditions': [
        ['use_x11==1', {
          'variables': {
            'command': ['foo', 'x11=1'],
          },
        }],
      ],
      'includes': [
        'a',
      ],
    }
    variables = {'use_x11': 1, 'OS': 'linux', 'chromeos': 0}
    config = isolate_format.load_isolate_for_config('/', str(b), variables)
    self.assertEqual((['foo', 'x11=1'], [], None, '/'), config)
    variables = {'use_x11': 0, 'OS': 'linux', 'chromeos': 0}
    config = isolate_format.load_isolate_for_config('/', str(b), variables)
    self.assertEqual(([], [], None, '/'), config)


class IsolateFormatTmpDirTest(unittest.TestCase):
  def setUp(self):
    super(IsolateFormatTmpDirTest, self).setUp()
    self.tempdir = tempfile.mkdtemp(prefix=u'isolate_')

  def tearDown(self):
    try:
      file_path.rmtree(self.tempdir)
    finally:
      super(IsolateFormatTmpDirTest, self).tearDown()

  def test_load_with_includes(self):
    included_isolate = {
      'variables': {
        'files': [
          'file_common',
        ],
      },
      'conditions': [
        ['OS=="linux"', {
          'variables': {
            'files': [
              'file_linux',
            ],
            'read_only': 1,
          },
        }],
        ['OS=="mac" or OS=="win"', {
          'variables': {
            'files': [
              'file_non_linux',
            ],
            'read_only': 0,
          },
        }],
      ],
    }
    with open(os.path.join(self.tempdir, 'included.isolate'), 'wb') as f:
      isolate_format.pretty_print(included_isolate, f)
    values = {
      'includes': ['included.isolate'],
      'variables': {
        'files': [
          'file_less_common',
        ],
      },
      'conditions': [
        ['OS=="mac"', {
          'variables': {
            'files': [
              'file_mac',
            ],
            'read_only': 2,
          },
        }],
      ],
    }
    actual = isolate_format.load_isolate_as_config(self.tempdir, values, None)

    expected = {
      (None,): {
        'files': [
          'file_common',
          'file_less_common',
        ],
        'isolate_dir': self.tempdir,
      },
      ('linux',): {
        'files': [
          'file_linux',
        ],
        'isolate_dir': self.tempdir,
        'read_only': 1,
      },
      ('mac',): {
        'files': [
          'file_mac',
          'file_non_linux',
        ],
        'isolate_dir': self.tempdir,
        'read_only': 2,
      },
      ('win',): {
        'files': [
          'file_non_linux',
        ],
        'isolate_dir': self.tempdir,
        'read_only': 0,
      },
    }
    self.assertEqual(expected, actual.flatten())

  def test_load_with_includes_with_commands(self):
    # This one is messy. Check that isolate_dir is the expected value. To
    # achieve this, put the .isolate files into subdirectories.
    dir_1 = os.path.join(self.tempdir, '1')
    dir_3 = os.path.join(self.tempdir, '3')
    dir_3_2 = os.path.join(self.tempdir, '3', '2')
    os.mkdir(dir_1)
    os.mkdir(dir_3)
    os.mkdir(dir_3_2)

    isolate1 = {
      'conditions': [
        ['OS=="amiga" or OS=="win"', {
          'variables': {
            'command': [
              'foo', 'amiga_or_win',
            ],
          },
        }],
        ['OS=="linux"', {
          'variables': {
            'command': [
              'foo', 'linux',
            ],
            'files': [
              'file_linux',
            ],
          },
        }],
        ['OS=="mac" or OS=="win"', {
          'variables': {
            'files': [
              'file_non_linux',
            ],
          },
        }],
      ],
    }
    isolate2 = {
      'conditions': [
        ['OS=="linux" or OS=="mac"', {
          'variables': {
            'command': [
              'foo', 'linux_or_mac',
            ],
            'files': [
              'other/file',
            ],
          },
        }],
      ],
    }
    # Do not define command in isolate3, otherwise commands in the other
    # included .isolated will be ignored.
    isolate3 = {
      'includes': [
        '../1/isolate1.isolate',
        '2/isolate2.isolate',
      ],
      'conditions': [
        ['OS=="amiga"', {
          'variables': {
            'files': [
              'file_amiga',
            ],
          },
        }],
        ['OS=="mac"', {
          'variables': {
            'files': [
              'file_mac',
            ],
          },
        }],
      ],
    }
    # No need to write isolate3.
    with open(os.path.join(dir_1, 'isolate1.isolate'), 'wb') as f:
      isolate_format.pretty_print(isolate1, f)
    with open(os.path.join(dir_3_2, 'isolate2.isolate'), 'wb') as f:
      isolate_format.pretty_print(isolate2, f)

    # The 'isolate_dir' are important, they are what will be used when
    # definining the final isolate_dir to use to run the command in the
    # .isolated file.
    actual = isolate_format.load_isolate_as_config(dir_3, isolate3, None)
    expected = {
      (None,): {
        # TODO(maruel): See TODO in ConfigSettings.flatten().
        # TODO(maruel): If kept, in this case dir_3 should be selected.
        'isolate_dir': dir_1,
      },
      ('amiga',): {
        'command': ['foo', 'amiga_or_win'],
        'files': [
          # Note that the file was rebased from isolate1. This is important,
          # isolate1 represent the canonical root path because it is the one
          # that defined the command.
          '../3/file_amiga',
        ],
        'isolate_dir': dir_1,
      },
      ('linux',): {
        # Last included takes precedence. *command comes from isolate2*, so
        # it becomes the canonical root, so reference to file from isolate1 is
        # via '../../1'.
        'command': ['foo', 'linux_or_mac'],
        'files': [
          '../../1/file_linux',
          'other/file',
        ],
        'isolate_dir': dir_3_2,
      },
      ('mac',): {
        'command': ['foo', 'linux_or_mac'],
        'files': [
          '../../1/file_non_linux',
          '../file_mac',
          'other/file',
        ],
        'isolate_dir': dir_3_2,
      },
      ('win',): {
        # command comes from isolate1.
        'command': ['foo', 'amiga_or_win'],
        'files': [
          # While this may be surprising, this is because the command was
          # defined in isolate1, not isolate3.
          'file_non_linux',
        ],
        'isolate_dir': dir_1,
      },
    }
    self.assertEqual(expected, actual.flatten())

  def test_load_with_includes_with_commands_and_variables(self):
    # This one is the pinacle of fun. Check that isolate_dir is the expected
    # value. To achieve this, put the .isolate files into subdirectories.
    dir_1 = os.path.join(self.tempdir, '1')
    dir_3 = os.path.join(self.tempdir, '3')
    dir_3_2 = os.path.join(self.tempdir, '3', '2')
    os.mkdir(dir_1)
    os.mkdir(dir_3)
    os.mkdir(dir_3_2)

    isolate1 = {
      'conditions': [
        ['OS=="amiga" or OS=="win"', {
          'variables': {
            'command': [
              'foo', 'amiga_or_win', '<(PATH)',
            ],
          },
        }],
        ['OS=="linux"', {
          'variables': {
            'command': [
              'foo', 'linux', '<(PATH)',
            ],
            'files': [
              '<(PATH)/file_linux',
            ],
          },
        }],
        ['OS=="mac" or OS=="win"', {
          'variables': {
            'files': [
              '<(PATH)/file_non_linux',
            ],
          },
        }],
      ],
    }
    isolate2 = {
      'conditions': [
        ['OS=="linux" or OS=="mac"', {
          'variables': {
            'command': [
              'foo', 'linux_or_mac', '<(PATH)',
            ],
            'files': [
              '<(PATH)/other/file',
            ],
          },
        }],
      ],
    }
    # Do not define command in isolate3, otherwise commands in the other
    # included .isolated will be ignored.
    isolate3 = {
      'includes': [
        '../1/isolate1.isolate',
        '2/isolate2.isolate',
      ],
      'conditions': [
        ['OS=="amiga"', {
          'variables': {
            'files': [
              '<(PATH)/file_amiga',
            ],
          },
        }],
        ['OS=="mac"', {
          'variables': {
            'files': [
              '<(PATH)/file_mac',
            ],
          },
        }],
      ],
    }
    # No need to write isolate3.
    with open(os.path.join(dir_1, 'isolate1.isolate'), 'wb') as f:
      isolate_format.pretty_print(isolate1, f)
    with open(os.path.join(dir_3_2, 'isolate2.isolate'), 'wb') as f:
      isolate_format.pretty_print(isolate2, f)

    # The 'isolate_dir' are important, they are what will be used when
    # definining the final isolate_dir to use to run the command in the
    # .isolated file.
    actual = isolate_format.load_isolate_as_config(dir_3, isolate3, None)
    expected = {
      (None,): {
        'isolate_dir': dir_1,
      },
      ('amiga',): {
        'command': ['foo', 'amiga_or_win', '<(PATH)'],
        'files': [
          '<(PATH)/file_amiga',
        ],
        'isolate_dir': dir_1,
      },
      ('linux',): {
        # Last included takes precedence. *command comes from isolate2*, so
        # it becomes the canonical root, so reference to file from isolate1 is
        # via '../../1'.
        'command': ['foo', 'linux_or_mac', '<(PATH)'],
        'files': [
          '<(PATH)/file_linux',
          '<(PATH)/other/file',
        ],
        'isolate_dir': dir_3_2,
      },
      ('mac',): {
        'command': ['foo', 'linux_or_mac', '<(PATH)'],
        'files': [
          '<(PATH)/file_mac',
          '<(PATH)/file_non_linux',
          '<(PATH)/other/file',
        ],
        'isolate_dir': dir_3_2,
      },
      ('win',): {
        # command comes from isolate1.
        'command': ['foo', 'amiga_or_win', '<(PATH)'],
        'files': [
          '<(PATH)/file_non_linux',
        ],
        'isolate_dir': dir_1,
      },
    }
    self.assertEqual(expected, actual.flatten())


if __name__ == '__main__':
  fix_encoding.fix_encoding()
  logging.basicConfig(
      level=logging.DEBUG if '-v' in sys.argv else logging.ERROR,
      format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s')
  if '-v' in sys.argv:
    unittest.TestCase.maxDiff = None
  unittest.main()
