normalize_version
-----------------

    The exact output can differ lots for various setuptools versions.
    setuptools 7 and lower:
      None  -> ('*final',)
      '1.1' -> ('00000001', '00000001', '*final')
    setuptools 8 and higher:
      None  -> <LegacyVersion('')>
      '1.1' -> <Version('1.1')>
    Workaround is to turn the result into a tuple.  But that makes
    pkg_resources complain when using setuptools 8 or higher:

      RuntimeWarning: You have iterated over the result of
      pkg_resources.parse_version. This is a legacy behavior which is
      inconsistent with the new version class introduced in setuptools
      8.0. In most cases, conversion to a tuple is unnecessary. For
      comparison of versions, sort the Version instances directly. If
      you have another use case requiring the tuple, please file a bug
      with the setuptools project describing that need.

    In our case the only important part is that sorting works
    reliably, which is what we check here.

    >>> from Products.GenericSetup.upgrade import normalize_version
    >>> normalize_version(None) < normalize_version('1.1')
    True
    >>> normalize_version('1.1') > normalize_version(None)
    True
    >>> normalize_version('unknown') < normalize_version('1.1')
    True
    >>> normalize_version('1.0') < normalize_version('1.1')
    True
    >>> normalize_version(('1', '1')) == normalize_version('1.1')
    True


_version_matches_all
-------------------

    >>> from Products.GenericSetup.upgrade import _version_matches_all
    >>> _version_matches_all(None)
    True
    >>> _version_matches_all('all')
    True
    >>> _version_matches_all('unknown')
    True
    >>> _version_matches_all('')
    False
    >>> _version_matches_all('1.0')
    False
    >>> _version_matches_all(('all', ))
    True
    >>> _version_matches_all(('1', '0'))
    False


_version_matches
---------------

    >>> from Products.GenericSetup.upgrade import _version_matches
    >>> _version_matches(None, '1.0', '2.0')
    True
    >>> _version_matches('0.5', '1.0', '2.0')
    True
    >>> _version_matches('0.5', '1.0', '2.0', strict=True)
    False
    >>> _version_matches('1.0', '1.0', '2.0', strict=True)
    True
    >>> _version_matches('1.5', '1.0', '2.0')
    False
    >>> _version_matches('1.0', '1.0', '0.5')
    False
    >>> _version_matches('1.0', '1.0', 'unknown')
    True


UpgradeEntity
-------------

    >>> from Products.GenericSetup.upgrade import _extractStepInfo
    >>> from Products.GenericSetup.upgrade import UpgradeEntity
    >>> tool = object()
    >>> def true_checker(tool): return True
    >>> def false_checker(tool): return False

    with no restrictions: all -> all, no checker
    --------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '*', '*', 'DESC')
        >>> e.source is None
        True
        >>> e.dest is None
        True

        all <> unknown <> all

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True
            >>> e.versionMatch('unknown')
            True
            >>> e.isProposed(tool, 'unknown')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, 'unknown'))
            True

        all <> 1.0 <> all

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        all <> 2.0 <> all

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True

    with version restriction: all -> 2.0, no checker
    ------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '*', '2.0', 'DESC')
        >>> e.source is None
        True
        >>> e.dest
        ('2', '0')

        all <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True
            >>> e.versionMatch('unknown')
            True
            >>> e.isProposed(tool, 'unknown')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, 'unknown'))
            True

        all <> 1.0 < 2.0

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        all <> 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with version restriction: 1.0 -> 2.0, no checker
    ------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.0', '2.0', 'DESC')
        >>> e.source
        ('1', '0')
        >>> e.dest
        ('2', '0')

        1.0 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True
            >>> e.versionMatch('unknown')
            True
            >>> e.isProposed(tool, 'unknown')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, 'unknown'))
            True

        1.0 == 1.0 < 2.0

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.0 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with version restriction: 1.1 -> 2.0, no checker
    ------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.1', '2.0', 'DESC')
        >>> e.source
        ('1', '1')
        >>> e.dest
        ('2', '0')

        1.1 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True
            >>> e.versionMatch('unknown')
            True
            >>> e.isProposed(tool, 'unknown')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, 'unknown'))
            True

        1.1 > 1.0 < 2.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.1 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with version restriction: 2.0 -> 3.0, no checker
    ------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '2.0', '3.0', 'DESC')
        >>> e.source
        ('2', '0')
        >>> e.dest
        ('3', '0')

        2.0 <> unknown <> 3.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True
            >>> e.versionMatch('unknown')
            True
            >>> e.isProposed(tool, 'unknown')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, 'unknown'))
            True

        2.0 > 1.0 < 3.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        2.0 == 2.0 < 3.0

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True

    with checker restriction: all -> all, true checker
    --------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '*', '*', 'DESC', true_checker)

        all <> unknown <> all

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        all <> 1.0 <> all

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        all <> 2.0 <> all

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True

    with checker restriction: all -> all, false checker
    ---------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '*', '*', 'DESC', false_checker)

        all <> unknown <> all

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        all <> 1.0 <> all

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        all <> 2.0 <> all

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True

    with combined restrictions: 1.0 -> 2.0, true checker
    ----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.0', '2.0', 'DESC', true_checker)

        1.0 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        1.0 == 1.0 < 2.0

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.0 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with combined restrictions: 1.1 -> 2.0, true checker
    ----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.1', '2.0', 'DESC', true_checker)

        1.1 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        1.1 > 1.0 < 2.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.1 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with combined restrictions: 2.0 -> 3.0, true checker
    ----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '2.0', '3.0', 'DESC', true_checker)

        2.0 <> unknown <> 3.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        2.0 > 1.0 < 3.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        2.0 == 2.0 < 3.0

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            True
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True

    with combined restrictions: 1.0 -> 2.0, false checker
    -----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.0', '2.0', 'DESC', false_checker)

        1.0 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        1.0 == 1.0 < 2.0

            >>> e.versionMatch('1.0')
            True
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.0 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with combined restrictions: 1.1 -> 2.0, false checker
    -----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '1.1', '2.0', 'DESC', false_checker)

        1.1 <> unknown <> 2.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        1.1 > 1.0 < 2.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        1.1 < 2.0 == 2.0

            >>> e.versionMatch('2.0')
            False
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            False

    with combined restrictions: 2.0 -> 3.0, false checker
    -----------------------------------------------------

        >>> e = UpgradeEntity('TITLE', 'PROFILE', '2.0', '3.0', 'DESC', false_checker)

        2.0 <> unknown <> 3.0

            >>> e.versionMatch(None)
            True
            >>> e.isProposed(tool, None)
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, None))
            True

        2.0 > 1.0 < 3.0

            >>> e.versionMatch('1.0')
            False
            >>> e.isProposed(tool, '1.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '1.0'))
            True

        2.0 == 2.0 < 3.0

            >>> e.versionMatch('2.0')
            True
            >>> e.isProposed(tool, '2.0')
            False
            >>> bool(_extractStepInfo(tool, 'ID', e, '2.0'))
            True
