# Copyright (c) 2012 OpenStack Foundation.
#
#    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.

from concurrent import futures
import os.path
import queue
import random
import re
import sys
import threading
import time
from unittest import mock

import ddt
import netaddr
from neutron_lib import constants
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from osprofiler import profiler
import testtools

from neutron.common import utils
from neutron.tests import base
from neutron.tests.unit import tests


class _PortRange:
    """A linked list of port ranges."""
    def __init__(self, base, prev_ref=None):
        self.base = base
        self.mask = 0xffff
        self.prev_ref = prev_ref

    @property
    def possible_mask_base(self):
        return self.base & (self.mask << 1)

    @property
    def can_merge(self):
        return (self.prev_ref and
                self.possible_mask_base == self.prev_ref.possible_mask_base and
                self.mask == self.prev_ref.mask)

    def shake(self):
        """Try to merge ranges created earlier.

        If previous number in a list can be merged with current item under
        common mask, it's merged. Then it continues to do the same with the
        rest of the list.
        """
        while self.can_merge:
            self.mask <<= 1
            self.base = self.prev_ref.base
            if self.prev_ref:
                self.prev_ref = self.prev_ref.prev_ref

    def __str__(self):
        return _hex_format(self.base, self.mask)

    def get_list(self):
        if self.prev_ref:
            return self.prev_ref.get_list() + [str(self)]
        return [str(self)]


def _hex_str(num):
    return format(num, '#06x')


def _hex_format(port, mask):
    if mask != 0xffff:
        return f"{_hex_str(port)}/{_hex_str(0xffff & mask)}"
    return _hex_str(port)


def _port_rule_masking(port_min, port_max):
    current = None
    for num in range(port_min, port_max + 1):
        port_range = _PortRange(num, prev_ref=current)
        port_range.shake()
        current = port_range
    return current.get_list()


class TestExceptionLogger(base.BaseTestCase):
    def test_normal_call(self):
        result = "Result"

        @utils.exception_logger()
        def func():
            return result

        self.assertEqual(result, func())

    def test_raise(self):
        result = "Result"

        @utils.exception_logger()
        def func():
            raise RuntimeError(result)

        self.assertRaises(RuntimeError, func)

    def test_spawn_normal(self):
        result = "Result"
        logger = mock.Mock()

        @utils.exception_logger(logger=logger)
        def func(ret):
            ret.append(result)

        ret_value = []
        _thread = threading.Thread(target=func, args=(ret_value, ))
        _thread.start()
        _thread.join()
        self.assertEqual(result, ret_value[0])
        self.assertFalse(logger.called)

    def test_spawn_raise(self):
        result = "Result"
        logger = mock.Mock()

        @utils.exception_logger(logger=logger)
        def func():
            raise RuntimeError(result)

        _thread = threading.Thread(target=func)
        _thread.start()
        self.assertTrue(logger.called)

    def test_pool_spawn_normal(self):
        logger = mock.Mock()
        calls = mock.Mock()

        @utils.exception_logger(logger=logger)
        def func(i):
            calls(i)

        with futures.ThreadPoolExecutor(max_workers=4) as executor:
            fs = [executor.submit(func, i) for i in range(4)]
            all(f.done() for f in fs)

        calls.assert_has_calls([mock.call(0), mock.call(1),
                                mock.call(2), mock.call(3)],
                               any_order=True)
        self.assertFalse(logger.called)

    def test_pool_spawn_raise(self):
        logger = mock.Mock()
        calls = mock.Mock()

        @utils.exception_logger(logger=logger)
        def func(i):
            if i == 2:
                raise RuntimeError(2)
            calls(i)

        with futures.ThreadPoolExecutor(max_workers=4) as executor:
            fs = [executor.submit(func, i) for i in range(4)]
            all(f.done() for f in fs)

        calls.assert_has_calls([mock.call(0), mock.call(1), mock.call(3)],
                               any_order=True)
        self.assertTrue(logger.called)

    def test_wait_until_true(self):

        class FalseException(Exception):
            pass

        self.assertRaises(
            FalseException,
            utils.wait_until_true,
            lambda: False, timeout=1, exception=FalseException)


class TestDvrServices(base.BaseTestCase):

    def _test_is_dvr_serviced(self, device_owner, expected):
        self.assertEqual(expected, utils.is_dvr_serviced(device_owner))

    def test_is_dvr_serviced_with_dhcp_port(self):
        self._test_is_dvr_serviced(constants.DEVICE_OWNER_DHCP, True)

    def test_is_dvr_serviced_with_vm_port(self):
        self._test_is_dvr_serviced(constants.DEVICE_OWNER_COMPUTE_PREFIX, True)


class TestFipServices(base.BaseTestCase):

    def _test_is_fip_serviced(self, device_owner, expected):
        self.assertEqual(expected, utils.is_fip_serviced(device_owner))

    def test_is_fip_serviced_with_dhcp_port(self):
        self._test_is_fip_serviced(constants.DEVICE_OWNER_DHCP, False)

    def test_is_fip_serviced_with_vm_port(self):
        self._test_is_fip_serviced(constants.DEVICE_OWNER_COMPUTE_PREFIX, True)


class TestIpToCidr(base.BaseTestCase):
    def test_ip_to_cidr_ipv4_default(self):
        self.assertEqual('15.1.2.3/32', utils.ip_to_cidr('15.1.2.3'))

    def test_ip_to_cidr_ipv4_prefix(self):
        self.assertEqual('15.1.2.3/24', utils.ip_to_cidr('15.1.2.3', 24))

    def test_ip_to_cidr_ipv4_netaddr(self):
        ip_address = netaddr.IPAddress('15.1.2.3')
        self.assertEqual('15.1.2.3/32', utils.ip_to_cidr(ip_address))

    def test_ip_to_cidr_ipv4_bad_prefix(self):
        self.assertRaises(netaddr.core.AddrFormatError,
                          utils.ip_to_cidr, '15.1.2.3', 33)

    def test_ip_to_cidr_ipv6_default(self):
        self.assertEqual('::1/128', utils.ip_to_cidr('::1'))

    def test_ip_to_cidr_ipv6_prefix(self):
        self.assertEqual('::1/64', utils.ip_to_cidr('::1', 64))

    def test_ip_to_cidr_ipv6_bad_prefix(self):
        self.assertRaises(netaddr.core.AddrFormatError,
                          utils.ip_to_cidr, '2000::1', 129)


class TestCidrIsHost(base.BaseTestCase):
    def test_is_cidr_host_ipv4(self):
        self.assertTrue(utils.is_cidr_host('15.1.2.3/32'))

    def test_is_cidr_host_ipv4_not_cidr(self):
        self.assertRaises(ValueError,
                          utils.is_cidr_host,
                          '15.1.2.3')

    def test_is_cidr_host_ipv6(self):
        self.assertTrue(utils.is_cidr_host('2000::1/128'))

    def test_is_cidr_host_ipv6_netaddr(self):
        net = netaddr.IPNetwork("2000::1")
        self.assertTrue(utils.is_cidr_host(net))

    def test_is_cidr_host_ipv6_32(self):
        self.assertFalse(utils.is_cidr_host('2000::1/32'))

    def test_is_cidr_host_ipv6_not_cidr(self):
        self.assertRaises(ValueError,
                          utils.is_cidr_host,
                          '2000::1')

    def test_is_cidr_host_ipv6_not_cidr_netaddr(self):
        ip_address = netaddr.IPAddress("2000::3")
        self.assertRaises(ValueError,
                          utils.is_cidr_host,
                          ip_address)


class TestIpVersionFromInt(base.BaseTestCase):
    def test_ip_version_from_int_ipv4(self):
        self.assertEqual(constants.IPv4,
                         utils.ip_version_from_int(constants.IP_VERSION_4))

    def test_ip_version_from_int_ipv6(self):
        self.assertEqual(constants.IPv6,
                         utils.ip_version_from_int(constants.IP_VERSION_6))

    def test_ip_version_from_int_illegal_int(self):
        self.assertRaises(ValueError,
                          utils.ip_version_from_int,
                          8)


class TestIsVersionGreaterEqual(base.BaseTestCase):
    def test_is_version_greater_equal_greater(self):
        self.assertTrue(utils.is_version_greater_equal('1.6.2', '1.6.0'))

    def test_is_version_greater_equal_equal(self):
        self.assertTrue(utils.is_version_greater_equal('1.6.2', '1.6.2'))

    def test_is_version_greater_equal_less(self):
        self.assertFalse(utils.is_version_greater_equal('1.6.0', '1.6.2'))


class TestDelayedStringRenderer(base.BaseTestCase):
    def test_call_deferred_until_str(self):
        my_func = mock.MagicMock(return_value='Brie cheese!')
        delayed = utils.DelayedStringRenderer(my_func, 1, 2, key_arg=44)
        self.assertFalse(my_func.called)
        string = "Type: %s" % delayed
        my_func.assert_called_once_with(1, 2, key_arg=44)
        self.assertEqual("Type: Brie cheese!", string)

    def test_not_called_with_low_log_level(self):
        LOG = logging.getLogger(__name__)
        # make sure we return logging to previous level
        current_log_level = LOG.logger.getEffectiveLevel()
        self.addCleanup(LOG.logger.setLevel, current_log_level)

        my_func = mock.MagicMock()
        delayed = utils.DelayedStringRenderer(my_func)

        # set to warning so we shouldn't be logging debug messages
        LOG.logger.setLevel(logging.logging.WARNING)
        LOG.debug("Hello %s", delayed)
        self.assertFalse(my_func.called)

        # but it should be called with the debug level
        LOG.logger.setLevel(logging.logging.DEBUG)
        LOG.debug("Hello %s", delayed)
        self.assertTrue(my_func.called)


class TestPortRuleMasking(base.BaseTestCase):
    def test_port_rule_wrong_input(self):
        with testtools.ExpectedException(ValueError):
            utils.port_rule_masking(12, 5)

    def compare_port_ranges_results(self, port_min, port_max):
        observed = utils.port_rule_masking(port_min, port_max)
        expected = _port_rule_masking(port_min, port_max)
        self.assertCountEqual(expected, observed)

    def test_port_rule_masking_random_ranges(self):
        # calling randint a bunch of times is really slow
        randports = sorted(random.sample(range(1, 65536), 2000))
        port_max = 0
        for i in randports:
            port_min = port_max
            port_max = i
            self.compare_port_ranges_results(port_min, port_max)

    def test_port_rule_masking_edge_cases(self):
        # (port_min, port_max) tuples
        TESTING_DATA = [
            (5, 12),
            (20, 130),
            (4501, 33057),
            (0, 65535),
            (22, 22),
            (5001, 5001),
            (0, 7),
            (8, 15),
            (1, 127),
        ]
        for port_min, port_max in TESTING_DATA:
            self.compare_port_ranges_results(port_min, port_max)


class TestExcDetails(base.BaseTestCase):

    def test_attach_exc_details(self):
        e = Exception()
        utils.attach_exc_details(e, 'details')
        self.assertEqual('details', utils.extract_exc_details(e))

    def test_attach_exc_details_with_interpolation(self):
        e = Exception()
        utils.attach_exc_details(e, 'details: %s', 'foo')
        self.assertEqual('details: foo', utils.extract_exc_details(e))

    def test_attach_exc_details_with_None_interpolation(self):
        e = Exception()
        utils.attach_exc_details(e, 'details: %s', None)
        self.assertEqual(
            'details: %s' % str(None), utils.extract_exc_details(e))

    def test_attach_exc_details_with_multiple_interpolation(self):
        e = Exception()
        utils.attach_exc_details(
            e, 'details: %s, %s', ('foo', 'bar'))
        self.assertEqual('details: foo, bar', utils.extract_exc_details(e))

    def test_attach_exc_details_with_dict_interpolation(self):
        e = Exception()
        utils.attach_exc_details(
            e, 'details: %(foo)s, %(bar)s', {'foo': 'foo', 'bar': 'bar'})
        self.assertEqual('details: foo, bar', utils.extract_exc_details(e))

    def test_extract_exc_details_no_details_attached(self):
        self.assertIsInstance(
            utils.extract_exc_details(Exception()), str)


@ddt.ddt
class ImportModulesRecursivelyTestCase(base.BaseTestCase):

    @ddt.data('/', r'\\')
    def test_recursion(self, separator):
        expected_modules = (
            'neutron.tests.unit.tests.example.dir.example_module',
            'neutron.tests.unit.tests.example.dir.subdir.example_module',
        )
        for module in expected_modules:
            sys.modules.pop(module, None)

        topdir = re.sub(r'[/\\]+', separator, os.path.dirname(tests.__file__))
        modules = utils.import_modules_recursively(topdir)
        for module in expected_modules:
            self.assertIn(module, modules)
            self.assertIn(module, sys.modules)


class TestThrottler(base.BaseTestCase):
    def test_throttler(self):
        threshold = 1
        orig_function = mock.Mock()
        # Add this magic name as it's required by functools
        orig_function.__name__ = 'mock_func'
        throttled_func = utils.throttler(threshold)(orig_function)

        throttled_func()

        sleep = utils.time.sleep

        def sleep_mock(amount_to_sleep):
            sleep(amount_to_sleep)
            self.assertGreaterEqual(threshold, amount_to_sleep)

        with mock.patch.object(utils.time, "sleep",
                               side_effect=sleep_mock):
            throttled_func()

        self.assertEqual(2, orig_function.call_count)

        lock_with_timer = throttled_func.__closure__[1].cell_contents
        timestamp = lock_with_timer.timestamp - threshold
        lock_with_timer.timestamp = timestamp

        throttled_func()

        self.assertEqual(3, orig_function.call_count)
        self.assertLess(timestamp, lock_with_timer.timestamp)

    def test_method_docstring_is_preserved(self):
        class Klass:
            @utils.throttler()
            def method(self):
                """Docstring"""

        self.assertEqual("Docstring", Klass.method.__doc__)

    def test_method_still_callable(self):
        class Klass:
            @utils.throttler()
            def method(self):
                pass

        obj = Klass()
        obj.method()


class BaseUnitConversionTest:

    def test_bytes_to_bits(self):
        test_values = [
            (0, 0),  # 0 bytes should be 0 bits
            (1, 8)   # 1 byte should be 8 bits
        ]
        for input_bytes, expected_bits in test_values:
            self.assertEqual(
                expected_bits, utils.bytes_to_bits(input_bytes)
            )


class TestSIUnitConversions(BaseUnitConversionTest, base.BaseTestCase):

    base_unit = constants.SI_BASE

    def test_bits_to_kilobits(self):
        test_values = [
            (0, 0),  # 0 bites should be 0 kilobites
            (1, 1),  # 1 bit should be 1 kilobit
            (999, 1),  # 999 bits should be 1 kilobit
            (1000, 1),  # 1000 bits should be 1 kilobit
            (1001, 2)   # 1001 bits should be 2 kilobits
        ]
        for input_bits, expected_kilobits in test_values:
            self.assertEqual(
                expected_kilobits,
                utils.bits_to_kilobits(input_bits, self.base_unit)
            )


class TestIECUnitConversions(BaseUnitConversionTest, base.BaseTestCase):

    base_unit = constants.IEC_BASE

    def test_bits_to_kilobits(self):
        test_values = [
            (0, 0),  # 0 bites should be 0 kilobites
            (1, 1),  # 1 bit should be 1 kilobit
            (1023, 1),  # 1023 bits should be 1 kilobit
            (1024, 1),  # 1024 bits should be 1 kilobit
            (1025, 2)   # 1025 bits should be 2 kilobits
        ]
        for input_bits, expected_kilobits in test_values:
            self.assertEqual(
                expected_kilobits,
                utils.bits_to_kilobits(input_bits, self.base_unit)
            )


class TestRpBandwidthValidator(base.BaseTestCase):

    def setUp(self):
        super().setUp()
        self.device_name_set = {'ens4', 'ens7'}
        self.valid_rp_bandwidths = {
            'ens7': {'egress': 10000, 'ingress': 10000}
        }
        self.not_valid_rp_bandwidth = {
            'ens8': {'egress': 10000, 'ingress': 10000}
        }

    def test_validate_rp_bandwidth_with_device_names(self):
        try:
            utils.validate_rp_bandwidth(self.valid_rp_bandwidths,
                                        self.device_name_set)
        except ValueError:
            self.fail("validate_rp_bandwidth failed to validate %s" %
                      self.valid_rp_bandwidths)

        self.assertRaises(ValueError, utils.validate_rp_bandwidth,
                          self.not_valid_rp_bandwidth, self.device_name_set)


class SpawnWithOrWithoutProfilerTestCase(base.BaseTestCase):

    def _compare_profilers_in_parent_and_in_child(self, init_profiler):

        q = queue.Queue()

        def is_profiler_initialized(where):
            # Instead of returning a single boolean add information so we can
            # identify which thread produced the result without depending on
            # queue order.
            return {where: bool(profiler.get())}

        def thread_with_no_leaked_profiler():
            if init_profiler:
                profiler.init(hmac_key='fake secret')

            utils.spawn_n(lambda: q.put(is_profiler_initialized('in-child')))
            q.put(is_profiler_initialized('in-parent'))

        # Make sure in parent we start with an uninitialized profiler by
        # spawning a new thread. Otherwise the unit test runner thread may
        # leak an initialized profiler from one test to another.
        _thread = threading.Thread(target=thread_with_no_leaked_profiler)
        _thread.start()

        # In order to have some global protection against leaking initialized
        # profilers neutron.test.base.BaseTestCase.setup() also calls
        # addCleanup(profiler.clean)

        # Merge the results independently of queue order.
        results = {}
        results.update(q.get())
        results.update(q.get())

        self.assertEqual(
            {'in-parent': init_profiler,
             'in-child': init_profiler},
            results)

    def test_spawn_with_profiler(self):
        self._compare_profilers_in_parent_and_in_child(init_profiler=True)

    def test_spawn_without_profiler(self):
        self._compare_profilers_in_parent_and_in_child(init_profiler=False)


@utils.SingletonDecorator
class _TestSingletonClass1:
    def __init__(self, variable):
        self.variable = variable


@utils.SingletonDecorator
class _TestSingletonClass2:
    def __init__(self, variable):
        self.variable = variable


class SingletonDecoratorTestCase(base.BaseTestCase):
    def setUp(self):
        super().setUp()
        self.instance_1 = self.instance_2 = None
        self.addCleanup(self._destroy_instances)

    def _destroy_instances(self):
        try:
            del self.instance_1
        except AttributeError:
            pass
        try:
            del self.instance_2
        except AttributeError:
            pass

    def test_singleton_instance_class(self):
        self.instance_1 = _TestSingletonClass1('value1')
        self.instance_2 = _TestSingletonClass1('other_value_that_is_not_set')
        self.assertIs(self.instance_1, self.instance_2)
        self.assertEqual('value1', self.instance_2.variable)

        # If one reference is deleted, the other variable (instance_2) is still
        # referenced to the same object and the singleton decorator keeps it.
        del self.instance_1
        self.instance_1 = _TestSingletonClass1('again_value_that_is_not_set')
        self.assertIs(self.instance_1, self.instance_2)
        self.assertEqual('value1', self.instance_2.variable)

    def test_singleton_several_independent_classes(self):
        self.instance_1 = _TestSingletonClass1('value1')
        self.instance_2 = _TestSingletonClass2('value2')
        self.assertIsNot(self.instance_1, self.instance_2)
        self.assertEqual('value1', self.instance_1.variable)
        self.assertEqual('value2', self.instance_2.variable)

    def test_singleton_instance_deletion(self):
        self.instance_1 = _TestSingletonClass1('value1')
        self.instance_2 = _TestSingletonClass2('value2')
        self.assertIsNotNone(self.instance_1)
        self.assertIsNotNone(self.instance_2)

        # Delete instance 1, new value "value1_b" set in __init__()
        del self.instance_1
        self.instance_1 = _TestSingletonClass1('value1_b')
        self.instance_2 = _TestSingletonClass2('value2_b')
        self.assertIsNotNone(self.instance_1)
        self.assertIsNotNone(self.instance_2)
        self.assertEqual('value1_b', self.instance_1.variable)
        self.assertEqual('value2', self.instance_2.variable)

        # Delete instance 2, new value "value2_c" set in __init__()
        del self.instance_2
        self.instance_1 = _TestSingletonClass1('value1_c')
        self.instance_2 = _TestSingletonClass2('value2_c')
        self.assertIsNotNone(self.instance_1)
        self.assertIsNotNone(self.instance_2)
        self.assertEqual('value1_b', self.instance_1.variable)
        self.assertEqual('value2_c', self.instance_2.variable)


class DisableNotificationTestCase(base.BaseTestCase):

    @utils.disable_notifications
    def sample_method(self):
        raise AttributeError()

    def test_notification_rpc_workers_lt_one(self):
        cfg.CONF.set_override('rpc_workers', 0)
        self.assertIsNone(self.sample_method())

    def test_notification_rpc_workers_none(self):
        cfg.CONF.set_override('rpc_workers', None)
        self.assertRaises(AttributeError, self.sample_method)

    def test_notification_rpc_workers_one(self):
        cfg.CONF.set_override('rpc_workers', 1)
        self.assertRaises(AttributeError, self.sample_method)


class SignatureTestCase(base.BaseTestCase):

    def test_sign_instance_id(self):
        conf = mock.Mock()
        conf.metadata_proxy_shared_secret = 'secret'
        self.assertEqual(
            '773ba44693c7553d6ee20f61ea5d2757a9a4f4a44d2841ae4e95b52e4cd62db4',
            utils.sign_instance_id(conf, 'foo')
        )


class ParsePermittedEthertypesTestCase(base.BaseTestCase):

    @mock.patch.object(utils, 'LOG')
    def test_parse_permitted_ethertypes(self, mock_log):
        permitted_ethertypes = ['0x1234',
                                '0x1234',
                                '0x5678',
                                '0XCAfe',
                                '1112',
                                '0x123R']
        ret = utils.parse_permitted_ethertypes(permitted_ethertypes)
        self.assertEqual({'0x1234', '0x5678', '0xcafe'}, ret)
        calls = [mock.call('Custom ethertype %s is repeated', '0x1234'),
                 mock.call('Custom ethertype %s is not a hexadecimal number.',
                           '1112'),
                 mock.call('Custom ethertype %s is not a hexadecimal number.',
                           '0x123R'),
                 ]
        mock_log.warning.assert_has_calls(calls)


class StringMapTestCase(base.BaseTestCase):
    data = {'a': 1, 'b': None, 'c': 'hi'}

    def test_stringmap(self):
        expected = {'a': '1', 'b': '', 'c': 'hi'}

        self.assertEqual(expected, utils.stringmap(self.data))

    def test_stringmap_custom_default(self):
        self.assertEqual('None', utils.stringmap(self.data, 'None')['b'])


class ThreadPoolExecutorWithBlockTestCase(base.BaseTestCase):

    def setUp(self):
        super().setUp()
        self._queue = queue.Queue()

    def _check_values(self, expected_values):
        # Retrieve all stored values
        stored_values = []
        while not self._queue.empty():
            stored_values.append(self._queue.get())

        for expected_value in expected_values:
            self.assertIn(expected_value, stored_values)

    def _add_tasks(self, tpool, num_tasks):
        expected_values = []
        for idx in range(num_tasks):
            value1 = 'value1_%s' % idx
            value2 = 'value2_%s' % idx
            values = (value1, value2)
            expected_values.append(values)
            tpool.submit(self._task, self._queue, value1, value2=value2)
        return expected_values

    @staticmethod
    def _task(_queue, value1, value2=None):
        _queue.put((value1, value2))
        time.sleep(1)

    def test_simple_execution(self):
        max_workers = 5
        t1 = timeutils.utcnow()
        with utils.ThreadPoolExecutorWithBlock(max_workers=max_workers) as \
                tpool:
            expected_values = self._add_tasks(tpool, max_workers * 3)

        utils.wait_until_true(lambda: self._queue.qsize() == max_workers * 3,
                              timeout=5, sleep=0.1)
        t2 = timeutils.utcnow()
        diff_seconds = (t2 - t1).total_seconds()
        # NOTE(ralonsoh): we can't expect a 3 seconds sharp time gap, but at
        # least the execution is contained in this interval. If each task takes
        # 1 second, 5 threads can be executed in parallel and 15 workers are
        # required, that will take at least 3 seconds and should complete in
        # 6 seconds (2x).
        self.assertTrue(3 < diff_seconds < 6)

        self._check_values(expected_values)

    def test_simple_execution_fast_exit(self):
        max_workers = 5
        t1 = timeutils.utcnow()
        with utils.ThreadPoolExecutorWithBlock(max_workers=max_workers) as \
                tpool:
            expected_values = self._add_tasks(tpool, max_workers)
            tpool.shutdown(wait=False)
            self._add_tasks(tpool, max_workers)

        utils.wait_until_true(lambda: self._queue.qsize() == max_workers,
                              timeout=5, sleep=0.1)
        t2 = timeutils.utcnow()
        diff_seconds = (t2 - t1).total_seconds()
        # Similar to above, execution will take at least 1 second and should
        # complete in 2 seconds (2x).
        self.assertTrue(1 < diff_seconds < 2)
        self._check_values(expected_values)


class TestFindUniqueSequence(base.BaseTestCase):
    def test_empty_groups(self):
        # Empty input should return empty list
        result = utils.find_unique_sequence([])
        self.assertEqual([], result)

    def test_single_group_single_value(self):
        # Single group with one value
        result = utils.find_unique_sequence([['a']])
        self.assertEqual(['a'], result)

    def test_single_group_multiple_values(self):
        # Single group with multiple values - returns first value
        result = utils.find_unique_sequence([['a', 'b', 'c']])
        self.assertEqual(['a'], result)

    def test_multiple_groups_no_overlap(self):
        # Multiple groups with distinct values - straightforward case
        result = utils.find_unique_sequence([['a'], ['b'], ['c']])
        self.assertEqual(['a', 'b', 'c'], result)

    def test_multiple_groups_with_overlap_solvable(self):
        # Groups with overlapping values but a solution exists
        result = utils.find_unique_sequence(
            [['a', 'b'], ['b', 'c'], ['c', 'a']])
        self.assertEqual(['a', 'b', 'c'], result)

    def test_backtracking_required(self):
        # First choice in group 1 would conflict, requiring backtrack
        # Group 0 picks 'a', group 1 must pick 'b' (not 'a'),
        # group 2 must pick 'c'
        result = utils.find_unique_sequence([['a'], ['a', 'b'], ['b', 'c']])
        self.assertEqual(['a', 'b', 'c'], result)

    def test_no_solution_all_same_value(self):
        # All groups have only the same value - no valid sequence
        result = utils.find_unique_sequence([['a'], ['a'], ['a']])
        self.assertIsNone(result)

    def test_no_solution_insufficient_unique_values(self):
        # Not enough unique values across groups
        result = utils.find_unique_sequence(
            [['a', 'b'], ['a', 'b'], ['a', 'b']])
        self.assertIsNone(result)

    def test_complex_backtracking(self):
        # Requires multiple levels of backtracking
        # If we pick 'x' first, then 'y', we're stuck at group 2
        # Need to backtrack and try different combinations
        groups = [['x', 'a'], ['y', 'x', 'b'], ['x', 'y', 'c']]
        result = utils.find_unique_sequence(groups)
        # Verify result is valid: one per group, all unique
        self.assertIsNotNone(result)
        self.assertEqual(3, len(result))
        self.assertEqual(3, len(set(result)))
        for i, val in enumerate(result):
            self.assertIn(val, groups[i])

    def test_result_preserves_group_order(self):
        # Result should have one element from each group in order
        groups = [['1', '2'], ['3', '4'], ['5', '6']]
        result = utils.find_unique_sequence(groups)
        self.assertEqual(3, len(result))
        self.assertIn(result[0], groups[0])
        self.assertIn(result[1], groups[1])
        self.assertIn(result[2], groups[2])

    def test_partial_overlap(self):
        # Some groups share values, but solution exists
        groups = [['a', 'b', 'c'], ['b', 'c', 'd'], ['c', 'd', 'e']]
        result = utils.find_unique_sequence(groups)
        self.assertIsNotNone(result)
        self.assertEqual(3, len(result))
        self.assertEqual(3, len(set(result)))

    def test_single_empty_group_in_list(self):
        # One of the groups is empty - no solution possible
        result = utils.find_unique_sequence([['a'], [], ['c']])
        self.assertIsNone(result)
