class TestDirectoryMetaDirective(UpgradeTestCase):

    def setUp(self):
        super(TestDirectoryMetaDirective, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_upgrade_steps_are_registered(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_action'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 2, 2, 8))
                                  .named('remove_action'))

        with self.package_created():
            self.assert_upgrades([
                    {'source': ('10000000000000',),
                     'dest': ('20110101080000',),
                     'title': u'Add action.'},

                    {'source': ('20110101080000',),
                     'dest': ('20110202080000',),
                     'title': u'Remove action.'}])

    def test_first_source_version_is_last_regulare_upgrade_step(self):
        self.profile.with_upgrade(Builder('plone upgrade step')
                                  .upgrading('1000', to='1001')
                                  .titled(u'Register foo utility.'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_action'))

        with self.package_created():
            self.assert_upgrades([
                    {'source': ('1000',),
                     'dest': ('1001',),
                     'title': u'Register foo utility.'},

                    {'source': ('1001',),
                     'dest': ('20110101080000',),
                     'title': u'Add action.'}])

    def test_registers_migration_generic_setup_profile_foreach_step(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_an_action'))

        with self.package_created() as package:
            upgrade_path = package.package_path.joinpath(
                'upgrades', '20110101080000_add_an_action')
            self.assert_profile(
                {'id': 'the.package.upgrades:default-upgrade-20110101080000',
                 'title': 'Upgrade the.package:default ' + \
                     'to 20110101080000: Add an action.',
                 'description': '',
                 'path': upgrade_path,
                 'product': 'the.package.upgrades',
                 'type': EXTENSION,
                 'for': IMigratingPloneSiteRoot})

    def test_package_modules_is_not_corrupted(self):
        # Regression: when the upgrade-step:directory directive is used from
        # the package-directory with a relative path (directory="upgrades"),
        # it corrupted the sys.modules entry of the package.

        package_builder = (
            Builder('python package')
            .named('other.package')
            .at_path(self.layer['temp_directory'])
            .with_file('__init__.py', 'PACKAGE = "package root"')

            .with_directory('profiles/default')
            .with_zcml_node('genericsetup:registerProfile',
                            name='default',
                            title='other.package:default',
                            directory='profiles/default',
                            provides='Products.GenericSetup.interfaces.EXTENSION')

            .with_directory('upgrades')
            .with_file('upgrades/__init__.py', 'PACKAGE = "upgrades package"')
            .with_zcml_include('ftw.upgrade', file='meta.zcml')
            .with_zcml_node('upgrade-step:directory',
                            profile='other.package:default',
                            directory='upgrades'))

        with create(package_builder).zcml_loaded(self.layer['configurationContext']):
            import other.package
            self.assertEquals('package root', other.package.PACKAGE)

    def test_profile_must_be_registed_before_registering_upgrade_directory(self):
        package_builder = (Builder('python package')
                           .named('other.package')
                           .at_path(self.layer['temp_directory'])
                           .with_zcml_include('ftw.upgrade', file='meta.zcml')
                           .with_zcml_node('upgrade-step:directory',
                                           profile='other.package:default',
                                           directory='.'))

        with create(package_builder) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'The profile "other.package:default" needs to be registered'
            ' before registering its upgrade step directory.',
            str(cm.exception).splitlines()[0])

    def test_profile_version_is_set_to_latest_profile_version(self):
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 2, 2, 8)))

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile(
                {'id': u'the.package:default',
                 'title': u'the.package',
                 'description': u'',
                 'ftw.upgrade:dependencies': None,
                 'path': profile_path,
                 'version': '20110202080000',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

    def test_profile_version_is_set_to_latest_old_school_profile_version(self):
        self.profile.with_upgrade(Builder('plone upgrade step')
                                  .upgrading('1000', to='1001')
                                  .titled(u'Register foo utility.'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 2, 2, 8)))

        package = create(self.package)
        # Remove upgrade-step directory upgrade in order to have the
        # manually created upgrade step as last version
        # but still declaring an upgrade-step:directory:
        package.package_path.joinpath(
            'upgrades', '20110202080000_upgrade').rmtree()

        profile_path = package.package_path.joinpath('profiles', 'default')
        self.assertNotIn('<version',
                         profile_path.joinpath('metadata.xml').text())

        with package.zcml_loaded(self.layer['configurationContext']):
            from ftw.upgrade.directory.zcml import find_start_version
            find_start_version(u'the.package:default')
            self.assert_profile(
                {'id': u'the.package:default',
                 'title': u'the.package',
                 'description': u'',
                 'ftw.upgrade:dependencies': None,
                 'path': str(profile_path),
                 'version': '1001',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

    def test_version_set_to_default_when_no_upgrades_defined(self):
        upgrades = self.package.package.get_subpackage('upgrades')
        upgrades.with_zcml_include('ftw.upgrade', file='meta.zcml')
        upgrades.with_zcml_node('upgrade-step:directory',
                                profile='the.package:default',
                                directory='.')

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile(
                {'id': u'the.package:default',
                 'title': u'the.package',
                 'description': u'',
                 'ftw.upgrade:dependencies': None,
                 'path': profile_path,
                 'version': u'10000000000000',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

    def test_profile_must_not_have_a_metadata_version_defined(self):
        self.profile.with_fs_version('1000')
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))

        with create(self.package) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'Registering an upgrades directory for "the.package:default" requires'
            ' this profile to not define a version in its metadata.xml.'
            ' The version is automatically set to the latest upgrade.',
            str(cm.exception).splitlines()[0])

    def test_declaring_upgrades_dependency(self):
        self.package.with_profile(
            Builder('genericsetup profile')
            .named('bar')
            .with_upgrade(
                Builder('ftw upgrade step')
                .to(datetime(2010, 1, 1, 1, 1))
                .with_zcml_directory_options(
                    soft_dependencies="the.package:baz")))

        self.package.with_profile(
            Builder('genericsetup profile')
            .named('foo')
            .with_upgrade(
                Builder('ftw upgrade step')
                .to(datetime(2010, 1, 1, 1, 1))
                .with_zcml_directory_options(
                    soft_dependencies="the.package:bar the.package:baz")))

        self.package.with_profile(
            Builder('genericsetup profile')
            .named('baz')
            .with_upgrade(
                Builder('ftw upgrade step')
                .to(datetime(2010, 1, 1, 1, 1))
                .with_zcml_directory_options(
                    soft_dependencies="the.package:default")))

        with self.package_created() as package:
            self.assert_profile(
                {'id': u'the.package:foo',
                 'title': u'the.package',
                 'description': u'',
                 'ftw.upgrade:dependencies': [u'the.package:bar',
                                              u'the.package:baz'],
                 'path': package.package_path.joinpath('profiles', 'foo'),
                 'version': u'20100101010100',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

            portal_setup = getToolByName(self.portal, 'portal_setup')
            self.assertEquals(
                [u'the.package:default',
                 u'the.package:baz',
                 u'the.package:bar',
                 u'the.package:foo'],
                filter(lambda profile_id: profile_id.startswith('the.package:'),
                       get_sorted_profile_ids(portal_setup)))

    def test_handler_step_provides_interfaces_implemented_by_upgrade_step_class(self):
        code = '\n'.join((
            'from ftw.upgrade import UpgradeStep',
            'from ftw.upgrade.tests.test_directory_meta_directive import IFoo',
            'from zope.interface import implementer',
            '',
            '@implementer(IFoo)',
            'class Foo(UpgradeStep):',
            '    pass'))

        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 1))
                                  .with_code(code))

        with self.package_created():
            portal_setup = getToolByName(self.portal, 'portal_setup')
            steps = listUpgradeSteps(portal_setup, 'the.package:default', '10000000000000')
            self.assertEquals(1, len(steps))
            self.assertItemsEqual(
                (IRecordableHandler,
                 IUpgradeStep,
                 IFoo),
                tuple(providedBy(steps[0]['step'].handler)))
            self.assertTrue(steps[0]['step'].handler.handler)

    def assert_upgrades(self, expected):
        upgrades = self.portal_setup.listUpgrades('the.package:default')
        got = [dict((key, value) for (key, value) in step.items()
                    if key in ('source', 'dest', 'title'))
               for step in upgrades]
        self.maxDiff = None
        self.assertItemsEqual(expected, got)

    def assert_profile(self, expected):
        self.assertTrue(
            self.portal_setup.profileExists(expected['id']),
            'Profile "{0}" does not exist. Profiles: {1}'.format(
                expected['id'],
                [profile['id'] for profile in self.portal_setup.listProfileInfo()]))

        got = self.portal_setup.getProfileInfo(expected['id']).copy()

        # Ignore pre_handler and post_handler, only available in Plone >= 4.3.8
        got.pop('pre_handler', None)
        got.pop('post_handler', None)

        self.maxDiff = None
        self.assertDictEqual(expected, got)
Esempio n. 2
0
class TestUpgradeStepBuilder(UpgradeTestCase):

    def setUp(self):
        super(TestUpgradeStepBuilder, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_upgrade_step_directory_and_file_is_created(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1))
                                  .named('migrate file content type'))

        with self.package_created() as package:
            upgrade_path = package.package_path.joinpath(
                'upgrades', '20110101000000_migrate_file_content_type')
            self.assertTrue(upgrade_path.isdir(),
                            'Upgrade directory was not created {0}'.format(upgrade_path))
            self.assertMultiLineEqual(
                '\n'.join(('from ftw.upgrade import UpgradeStep',
                           '',
                           '',
                           'class MigrateFileContentType(UpgradeStep):',
                           '    """Migrate file content type.',
                           '    """',
                           '',
                           '    def __call__(self):',
                           '        self.install_upgrade_profile()',
                           '')),
                upgrade_path.joinpath('upgrade.py').text())

    def test_executing_upgrade_step_with_custom_code(self):
        class AddExcludeFromNavIndex(UpgradeStep):
            def __call__(self):
                self.catalog_add_index('excludeFromNav', 'KeywordIndex')

        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1))
                                  .calling(AddExcludeFromNavIndex))

        catalog = getToolByName(self.portal, 'portal_catalog')
        with self.package_created():
            self.install_profile('the.package:default', '0')
            self.assertNotIn('excludeFromNav', catalog.indexes(),
                             'Index excludeFromNav already exists.')
            self.install_profile_upgrades('the.package:default')
            self.assertIn('excludeFromNav', catalog.indexes(),
                          'Index excludeFromNav was not created.')

    def test_add_files_and_directories_to_profile(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step')
            .to(datetime(2011, 1, 1))
            .with_file('foo.txt', 'FOO')
            .with_directory('bar')
            .with_file('baz/baz.txt', 'BAZ', makedirs=True))

        with self.package_created() as package:
            upgrade_path = package.package_path.joinpath('upgrades',
                                                         '20110101000000_upgrade')
            self.assertTrue(upgrade_path.isdir(),
                            'Upgrade directory was not created {0}'.format(upgrade_path))

            self.assertEqual('FOO', upgrade_path.joinpath('foo.txt').text())
            self.assertTrue(upgrade_path.joinpath('bar').isdir(),
                            'directory "bar" was not created.')
            self.assertEqual('BAZ', upgrade_path.joinpath('baz', 'baz.txt').text())

    def test_importing_upgrade_step_with_import_profile_files(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step')
            .to(datetime(2011, 1, 1))
            .with_file('properties.xml', self.asset('foo-property.xml')))

        with self.package_created():
            self.install_profile('the.package:default', '0')
            self.assertFalse(self.portal.getProperty('foo'),
                             'Expected property "foo" to not yet exist.')
            self.install_profile_upgrades('the.package:default')
            self.assertEqual('bar',
                             self.portal.getProperty('foo'),
                             'Property "foo" was not created.')
class TestDirectoryMetaDirective(UpgradeTestCase):

    def setUp(self):
        super(TestDirectoryMetaDirective, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_upgrade_steps_are_registered(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_action'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 2, 2, 8))
                                  .named('remove_action'))

        with self.package_created():
            self.assert_upgrades([
                    {'source': ('10000000000000',),
                     'dest': ('20110101080000',),
                     'title': u'Add action.'},

                    {'source': ('20110101080000',),
                     'dest': ('20110202080000',),
                     'title': u'Remove action.'}])

    def test_first_source_version_is_last_regulare_upgrade_step(self):
        self.profile.with_upgrade(Builder('plone upgrade step')
                                  .upgrading('1000', to='1001')
                                  .titled('Register foo utility.'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_action'))

        with self.package_created():
            self.assert_upgrades([
                    {'source': ('1000',),
                     'dest': ('1001',),
                     'title': u'Register foo utility.'},

                    {'source': ('1001',),
                     'dest': ('20110101080000',),
                     'title': u'Add action.'}])

    def test_registers_migration_generic_setup_profile_foreach_step(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add_an_action'))

        with self.package_created() as package:
            upgrade_path = package.package_path.joinpath(
                'upgrades', '20110101080000_add_an_action')
            self.assert_profile(
                {'id': 'the.package.upgrades:default-upgrade-20110101080000',
                 'title': 'Upgrade the.package:default ' + \
                     'to 20110101080000: Add an action.',
                 'description': '',
                 'path': upgrade_path,
                 'product': 'the.package.upgrades',
                 'type': EXTENSION,
                 'for': IMigratingPloneSiteRoot})

    def test_package_modules_is_not_corrupted(self):
        # Regression: when the upgrade-step:directory directive is used from
        # the package-directory with a relative path (directory="upgrades"),
        # it corrupted the sys.modules entry of the package.

        package_builder = (
            Builder('python package')
            .named('other.package')
            .at_path(self.layer['temp_directory'])
            .with_file('__init__.py', 'PACKAGE = "package root"')

            .with_directory('profiles/default')
            .with_zcml_node('genericsetup:registerProfile',
                            name='default',
                            title='other.package:default',
                            directory='profiles/default',
                            provides='Products.GenericSetup.interfaces.EXTENSION')

            .with_directory('upgrades')
            .with_file('upgrades/__init__.py', 'PACKAGE = "upgrades package"')
            .with_zcml_include('ftw.upgrade', file='meta.zcml')
            .with_zcml_node('upgrade-step:directory',
                            profile='other.package:default',
                            directory='upgrades'))

        with create(package_builder).zcml_loaded(self.layer['configurationContext']):
            import other.package
            self.assertEquals('package root', other.package.PACKAGE)

    def test_profile_must_be_registed_before_registering_upgrade_directory(self):
        package_builder = (Builder('python package')
                           .named('other.package')
                           .at_path(self.layer['temp_directory'])
                           .with_zcml_include('ftw.upgrade', file='meta.zcml')
                           .with_zcml_node('upgrade-step:directory',
                                           profile='other.package:default',
                                           directory='.'))

        with create(package_builder) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'The profile "other.package:default" needs to be registered'
            ' before registering its upgrade step directory.',
            str(cm.exception).splitlines()[0])

    def test_profile_version_is_set_to_latest_profile_version(self):
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 2, 2, 8)))

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile(
                {'id': u'the.package:default',
                 'title': u'the.package',
                 'description': u'',
                 'path': profile_path,
                 'version': '20110202080000',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

    def test_version_set_to_default_when_no_upgrades_defined(self):
        upgrades = self.package.package.get_subpackage('upgrades')
        upgrades.with_zcml_include('ftw.upgrade', file='meta.zcml')
        upgrades.with_zcml_node('upgrade-step:directory',
                                profile='the.package:default',
                                directory='.')

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile(
                {'id': u'the.package:default',
                 'title': u'the.package',
                 'description': u'',
                 'path': profile_path,
                 'version': u'10000000000000',
                 'product': 'the.package',
                 'type': EXTENSION,
                 'for': None})

    def test_profile_must_not_have_a_metadata_version_defined(self):
        self.profile.with_fs_version('1000')
        self.profile.with_upgrade(Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))

        with create(self.package) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'Registering an upgrades directory for "the.package:default" requires'
            ' this profile to not define a version in its metadata.xml.'
            ' The version is automatically set to the latest upgrade.',
            str(cm.exception).splitlines()[0])

    def assert_upgrades(self, expected):
        upgrades = self.portal_setup.listUpgrades('the.package:default')
        got = [dict((key, value) for (key, value) in step.items()
                    if key in ('source', 'dest', 'title'))
               for step in upgrades]
        self.maxDiff = None
        self.assertItemsEqual(expected, got)

    def assert_profile(self, expected):
        self.assertTrue(
            self.portal_setup.profileExists(expected['id']),
            'Profile "{0}" does not exist. Profiles: {1}'.format(
                expected['id'],
                [profile['id'] for profile in self.portal_setup.listProfileInfo()]))

        got = self.portal_setup.getProfileInfo(expected['id'])
        self.maxDiff = None
        self.assertDictEqual(expected, got)
class TestDirectoryScanner(UpgradeTestCase):

    def setUp(self):
        super(TestDirectoryScanner, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_returns_chained_upgrade_infos(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add an action'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 2, 2, 8))
                                  .named('update the action'))
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 3, 3, 8))
                                  .named('remove the action'))

        with self.scanned() as upgrade_infos:
            map(lambda info: (info.__delitem__('path'),
                              info.__delitem__('callable')),
                upgrade_infos)

            self.maxDiff = None
            self.assertEqual(
                [{'source-version': None,
                  'target-version': '20110101080000',
                  'title': 'Add an action.'},

                 {'source-version': '20110101080000',
                  'target-version': '20110202080000',
                  'title': 'Update the action.'},

                 {'source-version': '20110202080000',
                  'target-version': '20110303080000',
                  'title': 'Remove the action.'}],

                upgrade_infos)

    def test_exception_raised_when_upgrade_has_no_code(self):
        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add action')
                                  .with_code(''))

        with create(self.package) as package:
            with self.assertRaises(UpgradeStepDefinitionError) as cm:
                self.scan(package)

        self.assertEqual(
            'The upgrade step 20110101080000_add_action has no upgrade class'
            ' in the upgrade.py module.',
            str(cm.exception))

    def test_exception_raised_when_multiple_upgrade_steps_detected(self):
        code = '\n'.join((
                'from ftw.upgrade import UpgradeStep',
                'class Foo(UpgradeStep): pass',
                'class Bar(UpgradeStep): pass'))

        self.profile.with_upgrade(Builder('ftw upgrade step')
                                  .to(datetime(2011, 1, 1, 8))
                                  .named('add action')
                                  .with_code(code))

        with create(self.package) as package:
            with self.assertRaises(UpgradeStepDefinitionError) as cm:
                self.scan(package)

        self.assertEqual(
            'The upgrade step 20110101080000_add_action has more than one upgrade'
            ' class in the upgrade.py module.',
            str(cm.exception))

    def test_does_not_fail_when_no_upgrades_present(self):
        self.package.with_zcml_include('ftw.upgrade', file='meta.zcml')
        self.package.with_zcml_node('upgrade-step:directory',
                                    profile='the.package:default',
                                    directory='.')

        with self.scanned() as upgrade_infos:
            self.assertEqual( [], upgrade_infos)

    @contextmanager
    def scanned(self):
        with create(self.package) as package:
            yield self.scan(package)

    def scan(self, package):
        upgrades = package.package_path.joinpath('upgrades')
        return Scanner('the.package.upgrades', upgrades).scan()
Esempio n. 5
0
class TestDirectoryMetaDirective(UpgradeTestCase):
    def setUp(self):
        super(TestDirectoryMetaDirective, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_upgrade_steps_are_registered(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1,
                                                    8)).named('add_action'))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 2, 2,
                                                    8)).named('remove_action'))

        with self.package_created():
            self.assert_upgrades([{
                'source': ('10000000000000', ),
                'dest': ('20110101080000', ),
                'title': u'Add action.'
            }, {
                'source': ('20110101080000', ),
                'dest': ('20110202080000', ),
                'title': u'Remove action.'
            }])

    def test_first_source_version_is_last_regulare_upgrade_step(self):
        self.profile.with_upgrade(
            Builder('plone upgrade step').upgrading(
                '1000', to='1001').titled(u'Register foo utility.'))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1,
                                                    8)).named('add_action'))

        with self.package_created():
            self.assert_upgrades([{
                'source': ('1000', ),
                'dest': ('1001', ),
                'title': u'Register foo utility.'
            }, {
                'source': ('1001', ),
                'dest': ('20110101080000', ),
                'title': u'Add action.'
            }])

    def test_registers_migration_generic_setup_profile_foreach_step(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1,
                                                    8)).named('add_an_action'))

        with self.package_created() as package:
            upgrade_path = package.package_path.joinpath(
                'upgrades', '20110101080000_add_an_action')
            self.assert_profile(
                {'id': 'the.package.upgrades:default-upgrade-20110101080000',
                 'title': 'Upgrade the.package:default ' + \
                     'to 20110101080000: Add an action.',
                 'description': '',
                 'path': upgrade_path,
                 'product': 'the.package.upgrades',
                 'type': EXTENSION,
                 'for': IMigratingPloneSiteRoot})

    def test_package_modules_is_not_corrupted(self):
        # Regression: when the upgrade-step:directory directive is used from
        # the package-directory with a relative path (directory="upgrades"),
        # it corrupted the sys.modules entry of the package.

        package_builder = (
            Builder('python package').named('other.package').at_path(
                self.layer['temp_directory']).with_file(
                    '__init__.py', 'PACKAGE = "package root"'
                ).with_directory('profiles/default').with_zcml_node(
                    'genericsetup:registerProfile',
                    name='default',
                    title='other.package:default',
                    directory='profiles/default',
                    provides='Products.GenericSetup.interfaces.EXTENSION').
            with_directory('upgrades').with_file(
                'upgrades/__init__.py',
                'PACKAGE = "upgrades package"').with_zcml_include(
                    'ftw.upgrade', file='meta.zcml').with_zcml_node(
                        'upgrade-step:directory',
                        profile='other.package:default',
                        directory='upgrades'))

        with create(package_builder).zcml_loaded(
                self.layer['configurationContext']):
            import other.package
            self.assertEquals('package root', other.package.PACKAGE)

    def test_profile_must_be_registed_before_registering_upgrade_directory(
            self):
        package_builder = (
            Builder('python package').named('other.package').at_path(
                self.layer['temp_directory']).with_zcml_include(
                    'ftw.upgrade', file='meta.zcml').with_zcml_node(
                        'upgrade-step:directory',
                        profile='other.package:default',
                        directory='.'))

        with create(package_builder) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'The profile "other.package:default" needs to be registered'
            ' before registering its upgrade step directory.',
            str(cm.exception).splitlines()[0])

    def test_profile_version_is_set_to_latest_profile_version(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 2, 2, 8)))

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile({
                'id': u'the.package:default',
                'title': u'the.package',
                'description': u'',
                'ftw.upgrade:dependencies': None,
                'path': profile_path,
                'version': '20110202080000',
                'product': 'the.package',
                'type': EXTENSION,
                'for': None
            })

    def test_profile_version_is_set_to_latest_old_school_profile_version(self):
        self.profile.with_upgrade(
            Builder('plone upgrade step').upgrading(
                '1000', to='1001').titled(u'Register foo utility.'))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 2, 2, 8)))

        package = create(self.package)
        # Remove upgrade-step directory upgrade in order to have the
        # manually created upgrade step as last version
        # but still declaring an upgrade-step:directory:
        package.package_path.joinpath('upgrades',
                                      '20110202080000_upgrade').rmtree()

        profile_path = package.package_path.joinpath('profiles', 'default')
        self.assertNotIn('<version',
                         profile_path.joinpath('metadata.xml').text())

        with package.zcml_loaded(self.layer['configurationContext']):
            from ftw.upgrade.directory.zcml import find_start_version
            find_start_version(u'the.package:default')
            self.assert_profile({
                'id': u'the.package:default',
                'title': u'the.package',
                'description': u'',
                'ftw.upgrade:dependencies': None,
                'path': str(profile_path),
                'version': '1001',
                'product': 'the.package',
                'type': EXTENSION,
                'for': None
            })

    def test_version_set_to_default_when_no_upgrades_defined(self):
        upgrades = self.package.package.get_subpackage('upgrades')
        upgrades.with_zcml_include('ftw.upgrade', file='meta.zcml')
        upgrades.with_zcml_node('upgrade-step:directory',
                                profile='the.package:default',
                                directory='.')

        with self.package_created() as package:
            profile_path = package.package_path.joinpath('profiles', 'default')
            self.assert_profile({
                'id': u'the.package:default',
                'title': u'the.package',
                'description': u'',
                'ftw.upgrade:dependencies': None,
                'path': profile_path,
                'version': u'10000000000000',
                'product': 'the.package',
                'type': EXTENSION,
                'for': None
            })

    def test_profile_must_not_have_a_metadata_version_defined(self):
        self.profile.with_fs_version('1000')
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1, 8)))

        with create(self.package) as package:
            with self.assertRaises(ConfigurationExecutionError) as cm:
                package.load_zcml(self.layer['configurationContext'])

        self.assertEqual(
            "<class 'ftw.upgrade.exceptions.UpgradeStepConfigurationError'>: "
            'Registering an upgrades directory for "the.package:default" requires'
            ' this profile to not define a version in its metadata.xml.'
            ' The version is automatically set to the latest upgrade.',
            str(cm.exception).splitlines()[0])

    def test_declaring_upgrades_dependency(self):
        self.package.with_profile(
            Builder('genericsetup profile').named('bar').with_upgrade(
                Builder('ftw upgrade step').to(datetime(
                    2010, 1, 1, 1, 1)).with_zcml_directory_options(
                        soft_dependencies="the.package:baz")))

        self.package.with_profile(
            Builder('genericsetup profile').named('foo').with_upgrade(
                Builder('ftw upgrade step').to(datetime(
                    2010, 1, 1, 1, 1)).with_zcml_directory_options(
                        soft_dependencies="the.package:bar the.package:baz")))

        self.package.with_profile(
            Builder('genericsetup profile').named('baz').with_upgrade(
                Builder('ftw upgrade step').to(datetime(
                    2010, 1, 1, 1, 1)).with_zcml_directory_options(
                        soft_dependencies="the.package:default")))

        with self.package_created() as package:
            self.assert_profile({
                'id':
                u'the.package:foo',
                'title':
                u'the.package',
                'description':
                u'',
                'ftw.upgrade:dependencies':
                [u'the.package:bar', u'the.package:baz'],
                'path':
                package.package_path.joinpath('profiles', 'foo'),
                'version':
                u'20100101010100',
                'product':
                'the.package',
                'type':
                EXTENSION,
                'for':
                None
            })

            portal_setup = getToolByName(self.portal, 'portal_setup')
            self.assertEquals(
                [
                    u'the.package:default', u'the.package:baz',
                    u'the.package:bar', u'the.package:foo'
                ],
                filter(
                    lambda profile_id: profile_id.startswith('the.package:'),
                    get_sorted_profile_ids(portal_setup)))

    def test_handler_step_provides_interfaces_implemented_by_upgrade_step_class(
            self):
        code = '\n'.join((
            'from ftw.upgrade import UpgradeStep',
            'from ftw.upgrade.tests.test_directory_meta_directive import IFoo',
            'from zope.interface import implementer', '', '@implementer(IFoo)',
            'class Foo(UpgradeStep):', '    pass'))

        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1,
                                                    1)).with_code(code))

        with self.package_created():
            portal_setup = getToolByName(self.portal, 'portal_setup')
            steps = listUpgradeSteps(portal_setup, 'the.package:default',
                                     '10000000000000')
            self.assertEquals(1, len(steps))
            self.assertItemsEqual((IRecordableHandler, IUpgradeStep, IFoo),
                                  tuple(providedBy(steps[0]['step'].handler)))
            self.assertTrue(steps[0]['step'].handler.handler)

    def assert_upgrades(self, expected):
        upgrades = self.portal_setup.listUpgrades('the.package:default')
        got = [
            dict((key, value) for (key, value) in step.items()
                 if key in ('source', 'dest', 'title')) for step in upgrades
        ]
        self.maxDiff = None
        self.assertItemsEqual(expected, got)

    def assert_profile(self, expected):
        self.assertTrue(
            self.portal_setup.profileExists(expected['id']),
            'Profile "{0}" does not exist. Profiles: {1}'.format(
                expected['id'], [
                    profile['id']
                    for profile in self.portal_setup.listProfileInfo()
                ]))

        got = self.portal_setup.getProfileInfo(expected['id']).copy()

        # Ignore pre_handler and post_handler, only available in Plone >= 4.3.8
        got.pop('pre_handler', None)
        got.pop('post_handler', None)

        self.maxDiff = None
        self.assertDictEqual(expected, got)
class TestDirectoryScanner(UpgradeTestCase):
    def setUp(self):
        super(TestDirectoryScanner, self).setUp()
        self.profile = Builder('genericsetup profile')
        self.package.with_profile(self.profile)

    def test_returns_chained_upgrade_infos(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(2011, 1, 1,
                                                    8)).named('add an action'))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(
                2011, 2, 2, 8)).named('update the action'))
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(
                2011, 3, 3, 8)).named('remove the action'))

        with self.scanned() as upgrade_infos:
            map(
                lambda info:
                (info.__delitem__('path'), info.__delitem__('callable')),
                upgrade_infos)

            self.maxDiff = None
            self.assertEqual([{
                'source-version': None,
                'target-version': '20110101080000',
                'title': 'Add an action.'
            }, {
                'source-version': '20110101080000',
                'target-version': '20110202080000',
                'title': 'Update the action.'
            }, {
                'source-version': '20110202080000',
                'target-version': '20110303080000',
                'title': 'Remove the action.'
            }], upgrade_infos)

    def test_exception_raised_when_upgrade_has_no_code(self):
        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(
                2011, 1, 1, 8)).named('add action').with_code(''))

        with create(self.package) as package:
            with self.assertRaises(UpgradeStepDefinitionError) as cm:
                self.scan(package)

        self.assertEqual(
            'The upgrade step 20110101080000_add_action has no upgrade class'
            ' in the upgrade.py module.', str(cm.exception))

    def test_exception_raised_when_multiple_upgrade_steps_detected(self):
        code = '\n'.join(
            ('from ftw.upgrade import UpgradeStep',
             'class Foo(UpgradeStep): pass', 'class Bar(UpgradeStep): pass'))

        self.profile.with_upgrade(
            Builder('ftw upgrade step').to(datetime(
                2011, 1, 1, 8)).named('add action').with_code(code))

        with create(self.package) as package:
            with self.assertRaises(UpgradeStepDefinitionError) as cm:
                self.scan(package)

        self.assertEqual(
            'The upgrade step 20110101080000_add_action has more than one upgrade'
            ' class in the upgrade.py module.', str(cm.exception))

    def test_does_not_fail_when_no_upgrades_present(self):
        self.package.with_zcml_include('ftw.upgrade', file='meta.zcml')
        self.package.with_zcml_node('upgrade-step:directory',
                                    profile='the.package:default',
                                    directory='.')

        with self.scanned() as upgrade_infos:
            self.assertEqual([], upgrade_infos)

    @contextmanager
    def scanned(self):
        with create(self.package) as package:
            yield self.scan(package)

    def scan(self, package):
        upgrades = package.package_path.joinpath('upgrades')
        return Scanner('the.package.upgrades', upgrades).scan()