Example #1
0
class TestDateFormatter(unittest.TestCase):
    '''Tests that the output of DateFormatter jinja filter is same as
    utils.strftime'''
    def setUp(self):
        # prepare a temp content and output folder
        self.temp_content = mkdtemp(prefix='pelicantests.')
        self.temp_output = mkdtemp(prefix='pelicantests.')

        # prepare a template file
        template_dir = os.path.join(self.temp_content, 'template')
        template_path = os.path.join(template_dir, 'source.html')
        os.makedirs(template_dir)
        with open(template_path, 'w') as template_file:
            template_file.write('date = {{ date|strftime("%A, %d %B %Y") }}')
        self.date = utils.SafeDatetime(2012, 8, 29)

    def tearDown(self):
        shutil.rmtree(self.temp_content)
        shutil.rmtree(self.temp_output)
        # reset locale to default
        locale.setlocale(locale.LC_ALL, '')

    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_french_strftime(self):
        # This test tries to reproduce an issue that
        # occurred with python3.3 under macos10 only
        if platform == 'win32':
            locale.setlocale(locale.LC_ALL, str('French'))
        else:
            locale.setlocale(locale.LC_ALL, str('fr_FR.UTF-8'))
        date = utils.SafeDatetime(2014, 8, 14)
        # we compare the lower() dates since macos10 returns
        # "Jeudi" for %A whereas linux reports "jeudi"
        self.assertEqual(
            u'jeudi, 14 août 2014',
            utils.strftime(date, date_format="%A, %d %B %Y").lower())
        df = utils.DateFormatter()
        self.assertEqual(u'jeudi, 14 août 2014',
                         df(date, date_format="%A, %d %B %Y").lower())
        # Let us now set the global locale to C:
        locale.setlocale(locale.LC_ALL, str('C'))
        # DateFormatter should still work as expected
        # since it is the whole point of DateFormatter
        # (This is where pre-2014/4/15 code fails on macos10)
        df_date = df(date, date_format="%A, %d %B %Y").lower()
        self.assertEqual(u'jeudi, 14 août 2014', df_date)

    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_french_locale(self):
        if platform == 'win32':
            locale_string = 'French'
        else:
            locale_string = 'fr_FR.UTF-8'
        settings = read_settings(
            override={
                'LOCALE': locale_string,
                'TEMPLATE_PAGES': {
                    'template/source.html': 'generated/file.html'
                }
            })

        generator = TemplatePagesGenerator({'date': self.date}, settings,
                                           self.temp_content, '',
                                           self.temp_output)
        generator.env.filters.update({'strftime': utils.DateFormatter()})

        writer = Writer(self.temp_output, settings=settings)
        generator.generate_output(writer)

        output_path = os.path.join(self.temp_output, 'generated', 'file.html')

        # output file has been generated
        self.assertTrue(os.path.exists(output_path))

        # output content is correct
        with utils.pelican_open(output_path) as output_file:
            self.assertEqual(output_file,
                             utils.strftime(self.date, 'date = %A, %d %B %Y'))

    @unittest.skipUnless(
        locale_available('tr_TR.UTF-8') or locale_available('Turkish'),
        'Turkish locale needed')
    def test_turkish_locale(self):
        if platform == 'win32':
            locale_string = 'Turkish'
        else:
            locale_string = 'tr_TR.UTF-8'
        settings = read_settings(
            override={
                'LOCALE': locale_string,
                'TEMPLATE_PAGES': {
                    'template/source.html': 'generated/file.html'
                }
            })

        generator = TemplatePagesGenerator({'date': self.date}, settings,
                                           self.temp_content, '',
                                           self.temp_output)
        generator.env.filters.update({'strftime': utils.DateFormatter()})

        writer = Writer(self.temp_output, settings=settings)
        generator.generate_output(writer)

        output_path = os.path.join(self.temp_output, 'generated', 'file.html')

        # output file has been generated
        self.assertTrue(os.path.exists(output_path))

        # output content is correct
        with utils.pelican_open(output_path) as output_file:
            self.assertEqual(output_file,
                             utils.strftime(self.date, 'date = %A, %d %B %Y'))
Example #2
0
class TestPelican(LoggedTestCase):
    # general functional testing for pelican. Basically, this test case tries
    # to run pelican in different situations and see how it behaves

    def setUp(self):
        super(TestPelican, self).setUp()
        self.temp_path = mkdtemp(prefix='pelicantests.')
        self.temp_cache = mkdtemp(prefix='pelican_cache.')
        self.maxDiff = None
        self.old_locale = locale.setlocale(locale.LC_ALL)
        locale.setlocale(locale.LC_ALL, str('C'))

    def tearDown(self):
        rmtree(self.temp_path)
        rmtree(self.temp_cache)
        locale.setlocale(locale.LC_ALL, self.old_locale)
        super(TestPelican, self).tearDown()

    def assertDirsEqual(self, left_path, right_path):
        out, err = subprocess.Popen(
            ['git', 'diff', '--no-ext-diff', '--exit-code',
             '-w', left_path, right_path],
            env={str('PAGER'): str('')},
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        ).communicate()

        def ignorable_git_crlf_errors(line):
            # Work around for running tests on Windows
            for msg in [
                    "LF will be replaced by CRLF",
                    "The file will have its original line endings"]:
                if msg in line:
                    return True
            return False
        if err:
            err = '\n'.join([l for l in err.decode('utf8').splitlines()
                             if not ignorable_git_crlf_errors(l)])
        assert not out, out
        assert not err, err

    def test_order_of_generators(self):
        # StaticGenerator must run last, so it can identify files that
        # were skipped by the other generators, and so static files can
        # have their output paths overridden by the {attach} link syntax.

        pelican = Pelican(settings=read_settings(path=None))
        generator_classes = pelican.get_generator_classes()

        self.assertTrue(
            generator_classes[-1] is StaticGenerator,
            "StaticGenerator must be the last generator, but it isn't!")
        self.assertIsInstance(
            generator_classes, collections.Sequence,
            "get_generator_classes() must return a Sequence to preserve order")

    def test_basic_generation_works(self):
        # when running pelican without settings, it should pick up the default
        # ones and generate correct output without raising any exception
        settings = read_settings(path=None, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'LOCALE': locale.normalize('en_US'),
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(
            self.temp_path, os.path.join(OUTPUT_PATH, 'basic'))
        self.assertLogCountEqual(
            count=1,
            msg="Unable to find.*skipping url replacement",
            level=logging.WARNING)

    def test_custom_generation_works(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(path=SAMPLE_CONFIG, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'LOCALE': locale.normalize('en_US'),
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(
            self.temp_path, os.path.join(OUTPUT_PATH, 'custom'))

    @unittest.skipUnless(locale_available('fr_FR.UTF-8') or
                         locale_available('French'), 'French locale needed')
    def test_custom_locale_generation_works(self):
        '''Test that generation with fr_FR.UTF-8 locale works'''
        if sys.platform == 'win32':
            our_locale = str('French')
        else:
            our_locale = str('fr_FR.UTF-8')

        settings = read_settings(path=SAMPLE_FR_CONFIG, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'LOCALE': our_locale,
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(
            self.temp_path, os.path.join(OUTPUT_PATH, 'custom_locale'))

    def test_theme_static_paths_copy(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(path=SAMPLE_CONFIG, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'THEME_STATIC_PATHS': [os.path.join(SAMPLES_PATH, 'very'),
                                   os.path.join(SAMPLES_PATH, 'kinda'),
                                   os.path.join(SAMPLES_PATH,
                                                'theme_standard')]
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        theme_output = os.path.join(self.temp_path, 'theme')
        extra_path = os.path.join(theme_output, 'exciting', 'new', 'files')

        for file in ['a_stylesheet', 'a_template']:
            self.assertTrue(os.path.exists(os.path.join(theme_output, file)))

        for file in ['wow!', 'boom!', 'bap!', 'zap!']:
            self.assertTrue(os.path.exists(os.path.join(extra_path, file)))

    def test_theme_static_paths_copy_single_file(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(path=SAMPLE_CONFIG, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'THEME_STATIC_PATHS': [os.path.join(SAMPLES_PATH,
                                                'theme_standard')]
        })

        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        theme_output = os.path.join(self.temp_path, 'theme')

        for file in ['a_stylesheet', 'a_template']:
            self.assertTrue(os.path.exists(os.path.join(theme_output, file)))

    def test_write_only_selected(self):
        """Test that only the selected files are written"""
        settings = read_settings(path=None, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'WRITE_SELECTED': [
                os.path.join(self.temp_path, 'oh-yeah.html'),
                os.path.join(self.temp_path, 'categories.html'),
            ],
            'LOCALE': locale.normalize('en_US'),
        })
        pelican = Pelican(settings=settings)
        logger = logging.getLogger()
        orig_level = logger.getEffectiveLevel()
        logger.setLevel(logging.INFO)
        mute(True)(pelican.run)()
        logger.setLevel(orig_level)
        self.assertLogCountEqual(
            count=2,
            msg="Writing .*",
            level=logging.INFO)

    def test_cyclic_intersite_links_no_warnings(self):
        settings = read_settings(path=None, override={
            'PATH': os.path.join(CURRENT_DIR, 'cyclic_intersite_links'),
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        # There are four different intersite links:
        # - one pointing to the second article from first and third
        # - one pointing to the first article from second and third
        # - one pointing to the third article from first and second
        # - one pointing to a nonexistent from each
        # If everything goes well, only the warning about the nonexistent
        # article should be printed. Only two articles are not sufficient,
        # since the first will always have _context['generated_content'] empty
        # (thus skipping the link resolving) and the second will always have it
        # non-empty, containing the first, thus always succeeding.
        self.assertLogCountEqual(
            count=1,
            msg="Unable to find '.*\\.rst', skipping url replacement.",
            level=logging.WARNING)

    def test_md_extensions_deprecation(self):
        """Test that a warning is issued if MD_EXTENSIONS is used"""
        settings = read_settings(path=None, override={
            'PATH': INPUT_PATH,
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
            'MD_EXTENSIONS': {},
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertLogCountEqual(
            count=1,
            msg="MD_EXTENSIONS is deprecated use MARKDOWN instead.",
            level=logging.WARNING)

    def test_parse_errors(self):
        # Verify that just an error is printed and the application doesn't
        # abort, exit or something.
        settings = read_settings(path=None, override={
            'PATH': os.path.abspath(os.path.join(CURRENT_DIR, 'parse_error')),
            'OUTPUT_PATH': self.temp_path,
            'CACHE_PATH': self.temp_cache,
        })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertLogCountEqual(
            count=1,
            msg="Could not process .*parse_error.rst",
            level=logging.ERROR)
Example #3
0
class TestUtils(LoggedTestCase):
    _new_attribute = 'new_value'

    @utils.deprecated_attribute(old='_old_attribute',
                                new='_new_attribute',
                                since=(3, 1, 0),
                                remove=(4, 1, 3))
    def _old_attribute():
        return None

    def test_deprecated_attribute(self):
        value = self._old_attribute
        self.assertEqual(value, self._new_attribute)
        self.assertLogCountEqual(
            count=1,
            msg=('_old_attribute has been deprecated since 3.1.0 and will be '
                 'removed by version 4.1.3.  Use _new_attribute instead'),
            level=logging.WARNING)

    def test_get_date(self):
        # valid ones
        date = utils.SafeDatetime(year=2012, month=11, day=22)
        date_hour = utils.SafeDatetime(year=2012,
                                       month=11,
                                       day=22,
                                       hour=22,
                                       minute=11)
        date_hour_z = utils.SafeDatetime(year=2012,
                                         month=11,
                                         day=22,
                                         hour=22,
                                         minute=11,
                                         tzinfo=pytz.timezone('UTC'))
        date_hour_est = utils.SafeDatetime(year=2012,
                                           month=11,
                                           day=22,
                                           hour=22,
                                           minute=11,
                                           tzinfo=pytz.timezone('EST'))
        date_hour_sec = utils.SafeDatetime(year=2012,
                                           month=11,
                                           day=22,
                                           hour=22,
                                           minute=11,
                                           second=10)
        date_hour_sec_z = utils.SafeDatetime(year=2012,
                                             month=11,
                                             day=22,
                                             hour=22,
                                             minute=11,
                                             second=10,
                                             tzinfo=pytz.timezone('UTC'))
        date_hour_sec_est = utils.SafeDatetime(year=2012,
                                               month=11,
                                               day=22,
                                               hour=22,
                                               minute=11,
                                               second=10,
                                               tzinfo=pytz.timezone('EST'))
        date_hour_sec_frac_z = utils.SafeDatetime(year=2012,
                                                  month=11,
                                                  day=22,
                                                  hour=22,
                                                  minute=11,
                                                  second=10,
                                                  microsecond=123000,
                                                  tzinfo=pytz.timezone('UTC'))
        dates = {
            '2012-11-22': date,
            '2012/11/22': date,
            '2012-11-22 22:11': date_hour,
            '2012/11/22 22:11': date_hour,
            '22-11-2012': date,
            '22/11/2012': date,
            '22.11.2012': date,
            '22.11.2012 22:11': date_hour,
            '2012-11-22T22:11Z': date_hour_z,
            '2012-11-22T22:11-0500': date_hour_est,
            '2012-11-22 22:11:10': date_hour_sec,
            '2012-11-22T22:11:10Z': date_hour_sec_z,
            '2012-11-22T22:11:10-0500': date_hour_sec_est,
            '2012-11-22T22:11:10.123Z': date_hour_sec_frac_z,
        }

        # examples from http://www.w3.org/TR/NOTE-datetime
        iso_8601_date = utils.SafeDatetime(year=1997, month=7, day=16)
        iso_8601_date_hour_tz = utils.SafeDatetime(year=1997,
                                                   month=7,
                                                   day=16,
                                                   hour=19,
                                                   minute=20,
                                                   tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_tz = utils.SafeDatetime(
            year=1997,
            month=7,
            day=16,
            hour=19,
            minute=20,
            second=30,
            tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_ms_tz = utils.SafeDatetime(
            year=1997,
            month=7,
            day=16,
            hour=19,
            minute=20,
            second=30,
            microsecond=450000,
            tzinfo=pytz.timezone('CET'))
        iso_8601 = {
            '1997-07-16': iso_8601_date,
            '1997-07-16T19:20+01:00': iso_8601_date_hour_tz,
            '1997-07-16T19:20:30+01:00': iso_8601_date_hour_sec_tz,
            '1997-07-16T19:20:30.45+01:00': iso_8601_date_hour_sec_ms_tz,
        }

        # invalid ones
        invalid_dates = ['2010-110-12', 'yay']

        for value, expected in dates.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for value, expected in iso_8601.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for item in invalid_dates:
            self.assertRaises(ValueError, utils.get_date, item)

    def test_slugify(self):

        samples = (
            ('this is a test', 'this-is-a-test'),
            ('this        is a test', 'this-is-a-test'),
            ('this → is ← a ↑ test', 'this-is-a-test'),
            ('this--is---a test', 'this-is-a-test'),
            ('unicode測試許功蓋,你看到了嗎?',
             'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'),
            ('大飯原発4号機、18日夜起動へ', 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),
        )

        settings = read_settings()
        subs = settings['SLUG_REGEX_SUBSTITUTIONS']

        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

        self.assertEqual(utils.slugify('Cat', regex_subs=subs), 'cat')
        self.assertEqual(
            utils.slugify('Cat', regex_subs=subs, preserve_case=False), 'cat')
        self.assertEqual(
            utils.slugify('Cat', regex_subs=subs, preserve_case=True), 'Cat')

    def test_slugify_substitute(self):

        samples = (
            ('C++ is based on C', 'cpp-is-based-on-c'),
            ('C+++ test C+ test', 'cpp-test-c-test'),
            ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'),
            ('c++-streams', 'cpp-streams'),
        )

        settings = read_settings()
        subs = [
            (r'C\+\+', 'CPP'),
            (r'C#', 'C-SHARP'),
        ] + settings['SLUG_REGEX_SUBSTITUTIONS']
        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

    def test_slugify_substitute_and_keeping_non_alphanum(self):

        samples = (
            ('Fedora QA', 'fedora.qa'),
            ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'),
            ('C++ is based on C', 'cpp is based on c'),
            ('C+++ test C+ test', 'cpp+ test c+ test'),
        )

        subs = [
            (r'Fedora QA', 'fedora.qa'),
            (r'c\+\+', 'cpp'),
        ]
        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

    def test_get_relative_path(self):

        samples = (
            (os.path.join('test', 'test.html'), os.pardir),
            (os.path.join('test', 'test',
                          'test.html'), os.path.join(os.pardir, os.pardir)),
            ('test.html', os.curdir),
            (os.path.join('/test', 'test.html'), os.pardir),
            (os.path.join('/test', 'test',
                          'test.html'), os.path.join(os.pardir, os.pardir)),
            ('/test.html', os.curdir),
        )

        for value, expected in samples:
            self.assertEqual(utils.get_relative_path(value), expected)

    def test_truncate_html_words(self):
        # Plain text.
        self.assertEqual(utils.truncate_html_words('short string', 20),
                         'short string')
        self.assertEqual(utils.truncate_html_words('word ' * 100, 20),
                         'word ' * 20 + '…')

        # Words enclosed or intervaled by HTML tags.
        self.assertEqual(
            utils.truncate_html_words('<p>' + 'word ' * 100 + '</p>', 20),
            '<p>' + 'word ' * 20 + '…</p>')
        self.assertEqual(
            utils.truncate_html_words(
                '<span\nstyle="\n…\n">' + 'word ' * 100 + '</span>', 20),
            '<span\nstyle="\n…\n">' + 'word ' * 20 + '…</span>')
        self.assertEqual(utils.truncate_html_words('<br>' + 'word ' * 100, 20),
                         '<br>' + 'word ' * 20 + '…')
        self.assertEqual(
            utils.truncate_html_words('<!-- comment -->' + 'word ' * 100, 20),
            '<!-- comment -->' + 'word ' * 20 + '…')

        # Words with hypens and apostrophes.
        self.assertEqual(utils.truncate_html_words("a-b " * 100, 20),
                         "a-b " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("it's " * 100, 20),
                         "it's " * 20 + '…')

        # Words with HTML entity references.
        self.assertEqual(utils.truncate_html_words("&eacute; " * 100, 20),
                         "&eacute; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("caf&eacute; " * 100, 20),
                         "caf&eacute; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&egrave;lite " * 100, 20),
                         "&egrave;lite " * 20 + '…')
        self.assertEqual(
            utils.truncate_html_words("cafeti&eacute;re " * 100, 20),
            "cafeti&eacute;re " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&int;dx " * 100, 20),
                         "&int;dx " * 20 + '…')

        # Words with HTML character references inside and outside
        # the ASCII range.
        self.assertEqual(utils.truncate_html_words("&#xe9; " * 100, 20),
                         "&#xe9; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&#x222b;dx " * 100, 20),
                         "&#x222b;dx " * 20 + '…')

        # Words with invalid or broken HTML references.
        self.assertEqual(utils.truncate_html_words('&invalid;', 20),
                         '&invalid;')
        self.assertEqual(utils.truncate_html_words('&#9999999999;', 20),
                         '&#9999999999;')
        self.assertEqual(utils.truncate_html_words('&#xfffffffff;', 20),
                         '&#xfffffffff;')
        self.assertEqual(utils.truncate_html_words('&mdash text', 20),
                         '&mdash text')
        self.assertEqual(utils.truncate_html_words('&#1234 text', 20),
                         '&#1234 text')
        self.assertEqual(utils.truncate_html_words('&#xabc text', 20),
                         '&#xabc text')

    def test_process_translations(self):
        fr_articles = []
        en_articles = []

        # create a bunch of articles
        # 0: no translation metadata
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay0',
                        title='Titre',
                        content='en français'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay0',
                        title='Title',
                        content='in english'))
        # 1: translation metadata on default lang
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay1',
                        title='Titre',
                        content='en français'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay1',
                        title='Title',
                        content='in english',
                        translation='true'))
        # 2: translation metadata not on default lang
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay2',
                        title='Titre',
                        content='en français',
                        translation='true'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay2',
                        title='Title',
                        content='in english'))
        # 3: back to default language detection if all items have the
        #    translation metadata
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay3',
                        title='Titre',
                        content='en français',
                        translation='yep'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay3',
                        title='Title',
                        content='in english',
                        translation='yes'))
        # 4-5: translation pairs with the same slug but different category
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay4',
                        title='Titre',
                        content='en français',
                        category='foo'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay4',
                        title='Title',
                        content='in english',
                        category='foo'))
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay4',
                        title='Titre',
                        content='en français',
                        category='bar'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay4',
                        title='Title',
                        content='in english',
                        category='bar'))

        # try adding articles in both orders
        for lang0_articles, lang1_articles in ((fr_articles, en_articles),
                                               (en_articles, fr_articles)):
            articles = lang0_articles + lang1_articles

            # test process_translations with falsy translation_id
            index, trans = utils.process_translations(articles,
                                                      translation_id=None)
            for i in range(6):
                for lang_articles in [en_articles, fr_articles]:
                    self.assertIn(lang_articles[i], index)
                    self.assertNotIn(lang_articles[i], trans)

            # test process_translations with simple and complex translation_id
            for translation_id in ['slug', {'slug', 'category'}]:
                index, trans = utils.process_translations(
                    articles, translation_id=translation_id)

                for a in [
                        en_articles[0], fr_articles[1], en_articles[2],
                        en_articles[3], en_articles[4], en_articles[5]
                ]:
                    self.assertIn(a, index)
                    self.assertNotIn(a, trans)

                for a in [
                        fr_articles[0], en_articles[1], fr_articles[2],
                        fr_articles[3], fr_articles[4], fr_articles[5]
                ]:
                    self.assertIn(a, trans)
                    self.assertNotIn(a, index)

                for i in range(6):
                    self.assertIn(en_articles[i], fr_articles[i].translations)
                    self.assertIn(fr_articles[i], en_articles[i].translations)

                for a_arts in [en_articles, fr_articles]:
                    for b_arts in [en_articles, fr_articles]:
                        if translation_id == 'slug':
                            self.assertIn(a_arts[4], b_arts[5].translations)
                            self.assertIn(a_arts[5], b_arts[4].translations)
                        elif translation_id == {'slug', 'category'}:
                            self.assertNotIn(a_arts[4], b_arts[5].translations)
                            self.assertNotIn(a_arts[5], b_arts[4].translations)

    def test_watchers(self):
        # Test if file changes are correctly detected
        # Make sure to handle not getting any files correctly.

        dirname = os.path.join(os.path.dirname(__file__), 'content')
        folder_watcher = utils.folder_watcher(dirname, ['rst'])

        path = os.path.join(dirname, 'article_with_metadata.rst')
        file_watcher = utils.file_watcher(path)

        # first check returns True
        self.assertEqual(next(folder_watcher), True)
        self.assertEqual(next(file_watcher), True)

        # next check without modification returns False
        self.assertEqual(next(folder_watcher), False)
        self.assertEqual(next(file_watcher), False)

        # after modification, returns True
        t = time.time()
        os.utime(path, (t, t))
        self.assertEqual(next(folder_watcher), True)
        self.assertEqual(next(file_watcher), True)

        # file watcher with None or empty path should return None
        self.assertEqual(next(utils.file_watcher('')), None)
        self.assertEqual(next(utils.file_watcher(None)), None)

        empty_path = os.path.join(os.path.dirname(__file__), 'empty')
        try:
            os.mkdir(empty_path)
            os.mkdir(os.path.join(empty_path, "empty_folder"))
            shutil.copy(__file__, empty_path)

            # if no files of interest, returns None
            watcher = utils.folder_watcher(empty_path, ['rst'])
            self.assertEqual(next(watcher), None)
        except OSError:
            self.fail("OSError Exception in test_files_changed test")
        finally:
            shutil.rmtree(empty_path, True)

    def test_clean_output_dir(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'clean_output')
        content = os.path.join(os.path.dirname(__file__), 'content')
        shutil.copytree(content, test_directory)
        utils.clean_output_dir(test_directory, retention)
        self.assertTrue(os.path.isdir(test_directory))
        self.assertListEqual([], os.listdir(test_directory))
        shutil.rmtree(test_directory)

    def test_clean_output_dir_not_there(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'does_not_exist')
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_clean_output_dir_is_file(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'this_is_a_file')
        f = open(test_directory, 'w')
        f.write('')
        f.close()
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_strftime(self):
        d = utils.SafeDatetime(2012, 8, 29)

        # simple formatting
        self.assertEqual(utils.strftime(d, '%d/%m/%y'), '29/08/12')
        self.assertEqual(utils.strftime(d, '%d/%m/%Y'), '29/08/2012')

        # RFC 3339
        self.assertEqual(utils.strftime(d, '%Y-%m-%dT%H:%M:%SZ'),
                         '2012-08-29T00:00:00Z')

        # % escaped
        self.assertEqual(utils.strftime(d, '%d%%%m%%%y'), '29%08%12')
        self.assertEqual(utils.strftime(d, '%d %% %m %% %y'), '29 % 08 % 12')
        # not valid % formatter
        self.assertEqual(utils.strftime(d, '10% reduction in %Y'),
                         '10% reduction in 2012')
        self.assertEqual(utils.strftime(d, '%10 reduction in %Y'),
                         '%10 reduction in 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Published in %d-%m-%Y'),
                         'Published in 29-08-2012')

        # with non-ascii text
        self.assertEqual(
            utils.strftime(d, '%d/%m/%Y Øl trinken beim Besäufnis'),
            '29/08/2012 Øl trinken beim Besäufnis')

        # alternative formatting options
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '29/8/12')
        self.assertEqual(utils.strftime(d, '%-H:%-M:%-S'), '0:0:0')

        d = utils.SafeDatetime(2012, 8, 9)
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '9/8/12')

    # test the output of utils.strftime in a different locale
    # Turkish locale
    @unittest.skipUnless(
        locale_available('tr_TR.UTF-8') or locale_available('Turkish'),
        'Turkish locale needed')
    def test_strftime_locale_dependent_turkish(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_ALL)

        if platform == 'win32':
            locale.setlocale(locale.LC_ALL, str('Turkish'))
        else:
            locale.setlocale(locale.LC_ALL, str('tr_TR.UTF-8'))

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012')
        self.assertEqual(utils.strftime(d, '%A, %d %B %Y'),
                         'Çarşamba, 29 Ağustos 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'),
                         'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012')

        # non-ascii format candidate (someone might pass it… for some reason)
        self.assertEqual(utils.strftime(d, '%Y yılında %üretim artışı'),
                         '2012 yılında %üretim artışı')

        # restore locale back
        locale.setlocale(locale.LC_ALL, old_locale)

    # test the output of utils.strftime in a different locale
    # French locale
    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_strftime_locale_dependent_french(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_ALL)

        if platform == 'win32':
            locale.setlocale(locale.LC_ALL, str('French'))
        else:
            locale.setlocale(locale.LC_ALL, str('fr_FR.UTF-8'))

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012')

        # depending on OS, the first letter is m or M
        self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi'))

        # with text
        self.assertEqual(utils.strftime(d, 'Écrit le %d %B %Y'),
                         'Écrit le 29 août 2012')

        # non-ascii format candidate (someone might pass it… for some reason)
        self.assertEqual(utils.strftime(d, '%écrits en %Y'), '%écrits en 2012')

        # restore locale back
        locale.setlocale(locale.LC_ALL, old_locale)

    def test_maybe_pluralize(self):
        self.assertEqual(utils.maybe_pluralize(0, 'Article', 'Articles'),
                         '0 Articles')
        self.assertEqual(utils.maybe_pluralize(1, 'Article', 'Articles'),
                         '1 Article')
        self.assertEqual(utils.maybe_pluralize(2, 'Article', 'Articles'),
                         '2 Articles')
Example #4
0
class TestPelican(LoggedTestCase):
    # general functional testing for pelican. Basically, this test case tries
    # to run pelican in different situations and see how it behaves

    def setUp(self):
        super(TestPelican, self).setUp()
        self.temp_path = mkdtemp(prefix='pelicantests.')
        self.temp_cache = mkdtemp(prefix='pelican_cache.')
        self.maxDiff = None
        self.old_locale = locale.setlocale(locale.LC_ALL)
        locale.setlocale(locale.LC_ALL, str('C'))

    def tearDown(self):
        rmtree(self.temp_path)
        rmtree(self.temp_cache)
        locale.setlocale(locale.LC_ALL, self.old_locale)
        super(TestPelican, self).tearDown()

    def assertFilesEqual(self, diff):
        msg = ("some generated files differ from the expected functional "
               "tests output.\n"
               "This is probably because the HTML generated files "
               "changed. If these changes are normal, please refer "
               "to docs/contribute.rst to update the expected "
               "output of the functional tests.")

        self.assertEqual(diff['left_only'], [], msg=msg)
        self.assertEqual(diff['right_only'], [], msg=msg)
        self.assertEqual(diff['diff_files'], [], msg=msg)

    def assertDirsEqual(self, left_path, right_path):
        out, err = subprocess.Popen([
            'git', 'diff', '--no-ext-diff', '--exit-code', '-w', left_path,
            right_path
        ],
                                    env={
                                        'PAGER': ''
                                    },
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE).communicate()
        assert not out, out
        assert not err, err

    def test_order_of_generators(self):
        # StaticGenerator must run last, so it can identify files that
        # were skipped by the other generators, and so static files can
        # have their output paths overridden by the {attach} link syntax.

        pelican = Pelican(settings=read_settings(path=None))
        generator_classes = pelican.get_generator_classes()

        self.assertTrue(
            generator_classes[-1] is StaticGenerator,
            "StaticGenerator must be the last generator, but it isn't!")
        self.assertIsInstance(
            generator_classes, collections.Sequence,
            "get_generator_classes() must return a Sequence to preserve order")

    def test_basic_generation_works(self):
        # when running pelican without settings, it should pick up the default
        # ones and generate correct output without raising any exception
        settings = read_settings(path=None,
                                 override={
                                     'PATH': INPUT_PATH,
                                     'OUTPUT_PATH': self.temp_path,
                                     'CACHE_PATH': self.temp_cache,
                                     'LOCALE': locale.normalize('en_US'),
                                 })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(self.temp_path,
                             os.path.join(OUTPUT_PATH, 'basic'))
        self.assertLogCountEqual(
            count=3,
            msg="Unable to find.*skipping url replacement",
            level=logging.WARNING)

    def test_custom_generation_works(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(path=SAMPLE_CONFIG,
                                 override={
                                     'PATH': INPUT_PATH,
                                     'OUTPUT_PATH': self.temp_path,
                                     'CACHE_PATH': self.temp_cache,
                                     'LOCALE': locale.normalize('en_US'),
                                 })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(self.temp_path,
                             os.path.join(OUTPUT_PATH, 'custom'))

    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_custom_locale_generation_works(self):
        '''Test that generation with fr_FR.UTF-8 locale works'''
        old_locale = locale.setlocale(locale.LC_TIME)

        if sys.platform == 'win32':
            our_locale = str('French')
        else:
            our_locale = str('fr_FR.UTF-8')

        settings = read_settings(path=SAMPLE_FR_CONFIG,
                                 override={
                                     'PATH': INPUT_PATH,
                                     'OUTPUT_PATH': self.temp_path,
                                     'CACHE_PATH': self.temp_cache,
                                     'LOCALE': our_locale,
                                 })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        self.assertDirsEqual(self.temp_path,
                             os.path.join(OUTPUT_PATH, 'custom_locale'))

    def test_theme_static_paths_copy(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(path=SAMPLE_CONFIG,
                                 override={
                                     'PATH':
                                     INPUT_PATH,
                                     'OUTPUT_PATH':
                                     self.temp_path,
                                     'CACHE_PATH':
                                     self.temp_cache,
                                     'THEME_STATIC_PATHS': [
                                         os.path.join(SAMPLES_PATH, 'very'),
                                         os.path.join(SAMPLES_PATH, 'kinda'),
                                         os.path.join(SAMPLES_PATH,
                                                      'theme_standard')
                                     ]
                                 })
        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        theme_output = os.path.join(self.temp_path, 'theme')
        extra_path = os.path.join(theme_output, 'exciting', 'new', 'files')

        for file in ['a_stylesheet', 'a_template']:
            self.assertTrue(os.path.exists(os.path.join(theme_output, file)))

        for file in ['wow!', 'boom!', 'bap!', 'zap!']:
            self.assertTrue(os.path.exists(os.path.join(extra_path, file)))

    def test_theme_static_paths_copy_single_file(self):
        # the same thing with a specified set of settings should work
        settings = read_settings(
            path=SAMPLE_CONFIG,
            override={
                'PATH':
                INPUT_PATH,
                'OUTPUT_PATH':
                self.temp_path,
                'CACHE_PATH':
                self.temp_cache,
                'THEME_STATIC_PATHS':
                [os.path.join(SAMPLES_PATH, 'theme_standard')]
            })

        pelican = Pelican(settings=settings)
        mute(True)(pelican.run)()
        theme_output = os.path.join(self.temp_path, 'theme')

        for file in ['a_stylesheet', 'a_template']:
            self.assertTrue(os.path.exists(os.path.join(theme_output, file)))

    def test_write_only_selected(self):
        """Test that only the selected files are written"""
        settings = read_settings(path=None,
                                 override={
                                     'PATH':
                                     INPUT_PATH,
                                     'OUTPUT_PATH':
                                     self.temp_path,
                                     'CACHE_PATH':
                                     self.temp_cache,
                                     'WRITE_SELECTED': [
                                         os.path.join(self.temp_path,
                                                      'oh-yeah.html'),
                                         os.path.join(self.temp_path,
                                                      'categories.html'),
                                     ],
                                     'LOCALE':
                                     locale.normalize('en_US'),
                                 })
        pelican = Pelican(settings=settings)
        logger = logging.getLogger()
        orig_level = logger.getEffectiveLevel()
        logger.setLevel(logging.INFO)
        mute(True)(pelican.run)()
        logger.setLevel(orig_level)
        self.assertLogCountEqual(count=2, msg="Writing .*", level=logging.INFO)
Example #5
0
class TestUtils(LoggedTestCase):
    _new_attribute = 'new_value'

    @utils.deprecated_attribute(
        old='_old_attribute', new='_new_attribute',
        since=(3, 1, 0), remove=(4, 1, 3))
    def _old_attribute():
        return None

    def test_deprecated_attribute(self):
        value = self._old_attribute
        self.assertEqual(value, self._new_attribute)
        self.assertLogCountEqual(
            count=1,
            msg=('_old_attribute has been deprecated since 3.1.0 and will be '
                 'removed by version 4.1.3.  Use _new_attribute instead'),
            level=logging.WARNING)

    def test_get_date(self):
        # valid ones
        date = utils.SafeDatetime(year=2012, month=11, day=22)
        date_hour = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11)
        date_hour_z = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11,
            tzinfo=pytz.timezone('UTC'))
        date_hour_est = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11,
            tzinfo=pytz.timezone('EST'))
        date_hour_sec = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11, second=10)
        date_hour_sec_z = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11, second=10,
            tzinfo=pytz.timezone('UTC'))
        date_hour_sec_est = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11, second=10,
            tzinfo=pytz.timezone('EST'))
        date_hour_sec_frac_z = utils.SafeDatetime(
            year=2012, month=11, day=22, hour=22, minute=11, second=10,
            microsecond=123000, tzinfo=pytz.timezone('UTC'))
        dates = {
            '2012-11-22': date,
            '2012/11/22': date,
            '2012-11-22 22:11': date_hour,
            '2012/11/22 22:11': date_hour,
            '22-11-2012': date,
            '22/11/2012': date,
            '22.11.2012': date,
            '22.11.2012 22:11': date_hour,
            '2012-11-22T22:11Z': date_hour_z,
            '2012-11-22T22:11-0500': date_hour_est,
            '2012-11-22 22:11:10': date_hour_sec,
            '2012-11-22T22:11:10Z': date_hour_sec_z,
            '2012-11-22T22:11:10-0500': date_hour_sec_est,
            '2012-11-22T22:11:10.123Z': date_hour_sec_frac_z,
            }

        # examples from http://www.w3.org/TR/NOTE-datetime
        iso_8601_date = utils.SafeDatetime(year=1997, month=7, day=16)
        iso_8601_date_hour_tz = utils.SafeDatetime(
            year=1997, month=7, day=16, hour=19, minute=20,
            tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_tz = utils.SafeDatetime(
            year=1997, month=7, day=16, hour=19, minute=20, second=30,
            tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_ms_tz = utils.SafeDatetime(
            year=1997, month=7, day=16, hour=19, minute=20, second=30,
            microsecond=450000, tzinfo=pytz.timezone('CET'))
        iso_8601 = {
            '1997-07-16': iso_8601_date,
            '1997-07-16T19:20+01:00': iso_8601_date_hour_tz,
            '1997-07-16T19:20:30+01:00': iso_8601_date_hour_sec_tz,
            '1997-07-16T19:20:30.45+01:00': iso_8601_date_hour_sec_ms_tz,
        }

        # invalid ones
        invalid_dates = ['2010-110-12', 'yay']


        for value, expected in dates.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for value, expected in iso_8601.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for item in invalid_dates:
            self.assertRaises(ValueError, utils.get_date, item)

    def test_slugify(self):

        samples = (('this is a test', 'this-is-a-test'),
                   ('this        is a test', 'this-is-a-test'),
                   ('this → is ← a ↑ test', 'this-is-a-test'),
                   ('this--is---a test', 'this-is-a-test'),
                   ('unicode測試許功蓋,你看到了嗎?',
                    'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'),
                   ('大飯原発4号機、18日夜起動へ',
                    'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),)

        for value, expected in samples:
            self.assertEqual(utils.slugify(value), expected)

    def test_slugify_substitute(self):

        samples = (('C++ is based on C', 'cpp-is-based-on-c'),
                   ('C+++ test C+ test', 'cpp-test-c-test'),
                   ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'),
                   ('c++-streams', 'cpp-streams'),)

        subs = (('C++', 'CPP'), ('C#', 'C-SHARP'))
        for value, expected in samples:
            self.assertEqual(utils.slugify(value, subs), expected)

    def test_get_relative_path(self):

        samples = ((os.path.join('test', 'test.html'), os.pardir),
                   (os.path.join('test', 'test', 'test.html'),
                    os.path.join(os.pardir, os.pardir)),
                   ('test.html', os.curdir),
                   (os.path.join('/test', 'test.html'), os.pardir),
                   (os.path.join('/test', 'test', 'test.html'),
                    os.path.join(os.pardir, os.pardir)),
                   ('/test.html', os.curdir),)

        for value, expected in samples:
            self.assertEqual(utils.get_relative_path(value), expected)

    def test_process_translations(self):
        # create a bunch of articles
        # 1: no translation metadata
        fr_article1 = get_article(lang='fr', slug='yay', title='Un titre',
                                  content='en français')
        en_article1 = get_article(lang='en', slug='yay', title='A title',
                                  content='in english')
        # 2: reverse which one is the translation thanks to metadata
        fr_article2 = get_article(lang='fr', slug='yay2', title='Un titre',
                                  content='en français')
        en_article2 = get_article(lang='en', slug='yay2', title='A title',
                                  content='in english',
                                  extra_metadata={'translation': 'true'})
        # 3: back to default language detection if all items have the
        #    translation metadata
        fr_article3 = get_article(lang='fr', slug='yay3', title='Un titre',
                                  content='en français',
                                  extra_metadata={'translation': 'yep'})
        en_article3 = get_article(lang='en', slug='yay3', title='A title',
                                  content='in english',
                                  extra_metadata={'translation': 'yes'})

        articles = [fr_article1, en_article1, fr_article2, en_article2,
                    fr_article3, en_article3]

        index, trans = utils.process_translations(articles)

        self.assertIn(en_article1, index)
        self.assertIn(fr_article1, trans)
        self.assertNotIn(en_article1, trans)
        self.assertNotIn(fr_article1, index)

        self.assertIn(fr_article2, index)
        self.assertIn(en_article2, trans)
        self.assertNotIn(fr_article2, trans)
        self.assertNotIn(en_article2, index)

        self.assertIn(en_article3, index)
        self.assertIn(fr_article3, trans)
        self.assertNotIn(en_article3, trans)
        self.assertNotIn(fr_article3, index)

    def test_watchers(self):
        # Test if file changes are correctly detected
        # Make sure to handle not getting any files correctly.

        dirname = os.path.join(os.path.dirname(__file__), 'content')
        folder_watcher = utils.folder_watcher(dirname, ['rst'])

        path = os.path.join(dirname, 'article_with_metadata.rst')
        file_watcher = utils.file_watcher(path)

        # first check returns True
        self.assertEqual(next(folder_watcher), True)
        self.assertEqual(next(file_watcher), True)

        # next check without modification returns False
        self.assertEqual(next(folder_watcher), False)
        self.assertEqual(next(file_watcher), False)

        # after modification, returns True
        t = time.time()
        os.utime(path, (t, t))
        self.assertEqual(next(folder_watcher), True)
        self.assertEqual(next(file_watcher), True)

        # file watcher with None or empty path should return None
        self.assertEqual(next(utils.file_watcher('')), None)
        self.assertEqual(next(utils.file_watcher(None)), None)

        empty_path = os.path.join(os.path.dirname(__file__), 'empty')
        try:
            os.mkdir(empty_path)
            os.mkdir(os.path.join(empty_path, "empty_folder"))
            shutil.copy(__file__, empty_path)

            # if no files of interest, returns None
            watcher = utils.folder_watcher(empty_path, ['rst'])
            self.assertEqual(next(watcher), None)
        except OSError:
            self.fail("OSError Exception in test_files_changed test")
        finally:
            shutil.rmtree(empty_path, True)

    def test_clean_output_dir(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'clean_output')
        content = os.path.join(os.path.dirname(__file__), 'content')
        shutil.copytree(content, test_directory)
        utils.clean_output_dir(test_directory, retention)
        self.assertTrue(os.path.isdir(test_directory))
        self.assertListEqual([], os.listdir(test_directory))
        shutil.rmtree(test_directory)

    def test_clean_output_dir_not_there(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'does_not_exist')
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_clean_output_dir_is_file(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'this_is_a_file')
        f = open(test_directory, 'w')
        f.write('')
        f.close()
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_strftime(self):
        d = utils.SafeDatetime(2012, 8, 29)

        # simple formatting
        self.assertEqual(utils.strftime(d, '%d/%m/%y'), '29/08/12')
        self.assertEqual(utils.strftime(d, '%d/%m/%Y'), '29/08/2012')

        # RFC 3339
        self.assertEqual(utils.strftime(d, '%Y-%m-%dT%H:%M:%SZ'),'2012-08-29T00:00:00Z')

        # % escaped
        self.assertEqual(utils.strftime(d, '%d%%%m%%%y'), '29%08%12')
        self.assertEqual(utils.strftime(d, '%d %% %m %% %y'), '29 % 08 % 12')
        # not valid % formatter
        self.assertEqual(utils.strftime(d, '10% reduction in %Y'),
                         '10% reduction in 2012')
        self.assertEqual(utils.strftime(d, '%10 reduction in %Y'),
                         '%10 reduction in 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Published in %d-%m-%Y'),
                         'Published in 29-08-2012')

        # with non-ascii text
        self.assertEqual(utils.strftime(d, '%d/%m/%Y Øl trinken beim Besäufnis'),
                         '29/08/2012 Øl trinken beim Besäufnis')

        # alternative formatting options
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '29/8/12')
        self.assertEqual(utils.strftime(d, '%-H:%-M:%-S'), '0:0:0')

        d = utils.SafeDatetime(2012, 8, 9)
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '9/8/12')


    # test the output of utils.strftime in a different locale
    # Turkish locale
    @unittest.skipUnless(locale_available('tr_TR.UTF-8') or
                         locale_available('Turkish'),
                         'Turkish locale needed')
    def test_strftime_locale_dependent_turkish(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_TIME)

        if platform == 'win32':
            locale.setlocale(locale.LC_TIME, str('Turkish'))
        else:
            locale.setlocale(locale.LC_TIME, str('tr_TR.UTF-8'))

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012')
        self.assertEqual(utils.strftime(d, '%A, %d %B %Y'),
                         'Çarşamba, 29 Ağustos 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'),
            'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012')

        # non-ascii format candidate (someone might pass it... for some reason)
        self.assertEqual(utils.strftime(d, '%Y yılında %üretim artışı'),
            '2012 yılında %üretim artışı')

        # restore locale back
        locale.setlocale(locale.LC_TIME, old_locale)


    # test the output of utils.strftime in a different locale
    # French locale
    @unittest.skipUnless(locale_available('fr_FR.UTF-8') or
                         locale_available('French'),
                         'French locale needed')
    def test_strftime_locale_dependent_french(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_TIME)

        if platform == 'win32':
            locale.setlocale(locale.LC_TIME, str('French'))
        else:
            locale.setlocale(locale.LC_TIME, str('fr_FR.UTF-8'))

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012')

        # depending on OS, the first letter is m or M
        self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi'))

        # with text
        self.assertEqual(utils.strftime(d, 'Écrit le %d %B %Y'),
            'Écrit le 29 août 2012')

        # non-ascii format candidate (someone might pass it... for some reason)
        self.assertEqual(utils.strftime(d, '%écrits en %Y'),
            '%écrits en 2012')

        # restore locale back
        locale.setlocale(locale.LC_TIME, old_locale)
Example #6
0
class TestDateFormatter(unittest.TestCase):
    '''Tests that the output of DateFormatter jinja filter is same as
    utils.strftime'''
    def setUp(self):
        # prepare a temp content and output folder
        self.temp_content = mkdtemp(prefix='pelicantests.')
        self.temp_output = mkdtemp(prefix='pelicantests.')

        # prepare a template file
        template_dir = os.path.join(self.temp_content, 'template')
        template_path = os.path.join(template_dir, 'source.html')
        os.makedirs(template_dir)
        with open(template_path, 'w') as template_file:
            template_file.write('date = {{ date|strftime("%A, %d %B %Y") }}')
        self.date = datetime.date(2012, 8, 29)

    def tearDown(self):
        shutil.rmtree(self.temp_content)
        shutil.rmtree(self.temp_output)
        # reset locale to default
        locale.setlocale(locale.LC_ALL, '')

    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_french_locale(self):
        settings = read_settings(
            override={
                'LOCALE': locale.normalize('fr_FR.UTF-8'),
                'TEMPLATE_PAGES': {
                    'template/source.html': 'generated/file.html'
                }
            })

        generator = TemplatePagesGenerator({'date': self.date}, settings,
                                           self.temp_content, '',
                                           self.temp_output)
        generator.env.filters.update({'strftime': utils.DateFormatter()})

        writer = Writer(self.temp_output, settings=settings)
        generator.generate_output(writer)

        output_path = os.path.join(self.temp_output, 'generated', 'file.html')

        # output file has been generated
        self.assertTrue(os.path.exists(output_path))

        # output content is correct
        with utils.pelican_open(output_path) as output_file:
            self.assertEqual(output_file,
                             utils.strftime(self.date, 'date = %A, %d %B %Y'))

    @unittest.skipUnless(
        locale_available('tr_TR.UTF-8') or locale_available('Turkish'),
        'Turkish locale needed')
    def test_turkish_locale(self):
        settings = read_settings(
            override={
                'LOCALE': locale.normalize('tr_TR.UTF-8'),
                'TEMPLATE_PAGES': {
                    'template/source.html': 'generated/file.html'
                }
            })

        generator = TemplatePagesGenerator({'date': self.date}, settings,
                                           self.temp_content, '',
                                           self.temp_output)
        generator.env.filters.update({'strftime': utils.DateFormatter()})

        writer = Writer(self.temp_output, settings=settings)
        generator.generate_output(writer)

        output_path = os.path.join(self.temp_output, 'generated', 'file.html')

        # output file has been generated
        self.assertTrue(os.path.exists(output_path))

        # output content is correct
        with utils.pelican_open(output_path) as output_file:
            self.assertEqual(output_file,
                             utils.strftime(self.date, 'date = %A, %d %B %Y'))
Example #7
0
class TestUtils(LoggedTestCase):
    _new_attribute = 'new_value'

    @utils.deprecated_attribute(old='_old_attribute',
                                new='_new_attribute',
                                since=(3, 1, 0),
                                remove=(4, 1, 3))
    def _old_attribute():
        return None

    def test_deprecated_attribute(self):
        value = self._old_attribute
        self.assertEqual(value, self._new_attribute)
        self.assertLogCountEqual(
            count=1,
            msg=('_old_attribute has been deprecated since 3.1.0 and will be '
                 'removed by version 4.1.3.  Use _new_attribute instead'),
            level=logging.WARNING)

    def test_get_date(self):
        # valid ones
        date = utils.SafeDatetime(year=2012, month=11, day=22)
        date_hour = utils.SafeDatetime(year=2012,
                                       month=11,
                                       day=22,
                                       hour=22,
                                       minute=11)
        date_hour_z = utils.SafeDatetime(year=2012,
                                         month=11,
                                         day=22,
                                         hour=22,
                                         minute=11,
                                         tzinfo=pytz.timezone('UTC'))
        date_hour_est = utils.SafeDatetime(year=2012,
                                           month=11,
                                           day=22,
                                           hour=22,
                                           minute=11,
                                           tzinfo=pytz.timezone('EST'))
        date_hour_sec = utils.SafeDatetime(year=2012,
                                           month=11,
                                           day=22,
                                           hour=22,
                                           minute=11,
                                           second=10)
        date_hour_sec_z = utils.SafeDatetime(year=2012,
                                             month=11,
                                             day=22,
                                             hour=22,
                                             minute=11,
                                             second=10,
                                             tzinfo=pytz.timezone('UTC'))
        date_hour_sec_est = utils.SafeDatetime(year=2012,
                                               month=11,
                                               day=22,
                                               hour=22,
                                               minute=11,
                                               second=10,
                                               tzinfo=pytz.timezone('EST'))
        date_hour_sec_frac_z = utils.SafeDatetime(year=2012,
                                                  month=11,
                                                  day=22,
                                                  hour=22,
                                                  minute=11,
                                                  second=10,
                                                  microsecond=123000,
                                                  tzinfo=pytz.timezone('UTC'))
        dates = {
            '2012-11-22': date,
            '2012/11/22': date,
            '2012-11-22 22:11': date_hour,
            '2012/11/22 22:11': date_hour,
            '22-11-2012': date,
            '22/11/2012': date,
            '22.11.2012': date,
            '22.11.2012 22:11': date_hour,
            '2012-11-22T22:11Z': date_hour_z,
            '2012-11-22T22:11-0500': date_hour_est,
            '2012-11-22 22:11:10': date_hour_sec,
            '2012-11-22T22:11:10Z': date_hour_sec_z,
            '2012-11-22T22:11:10-0500': date_hour_sec_est,
            '2012-11-22T22:11:10.123Z': date_hour_sec_frac_z,
        }

        # examples from http://www.w3.org/TR/NOTE-datetime
        iso_8601_date = utils.SafeDatetime(year=1997, month=7, day=16)
        iso_8601_date_hour_tz = utils.SafeDatetime(year=1997,
                                                   month=7,
                                                   day=16,
                                                   hour=19,
                                                   minute=20,
                                                   tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_tz = utils.SafeDatetime(
            year=1997,
            month=7,
            day=16,
            hour=19,
            minute=20,
            second=30,
            tzinfo=pytz.timezone('CET'))
        iso_8601_date_hour_sec_ms_tz = utils.SafeDatetime(
            year=1997,
            month=7,
            day=16,
            hour=19,
            minute=20,
            second=30,
            microsecond=450000,
            tzinfo=pytz.timezone('CET'))
        iso_8601 = {
            '1997-07-16': iso_8601_date,
            '1997-07-16T19:20+01:00': iso_8601_date_hour_tz,
            '1997-07-16T19:20:30+01:00': iso_8601_date_hour_sec_tz,
            '1997-07-16T19:20:30.45+01:00': iso_8601_date_hour_sec_ms_tz,
        }

        # invalid ones
        invalid_dates = ['2010-110-12', 'yay']

        for value, expected in dates.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for value, expected in iso_8601.items():
            self.assertEqual(utils.get_date(value), expected, value)

        for item in invalid_dates:
            self.assertRaises(ValueError, utils.get_date, item)

    def test_slugify(self):

        samples = (
            ('this is a test', 'this-is-a-test'),
            ('this        is a test', 'this-is-a-test'),
            ('this → is ← a ↑ test', 'this-is-a-test'),
            ('this--is---a test', 'this-is-a-test'),
            ('unicode測試許功蓋,你看到了嗎?',
             'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'),
            ('大飯原発4号機、18日夜起動へ', 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),
        )

        settings = read_settings()
        subs = settings['SLUG_REGEX_SUBSTITUTIONS']

        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

        self.assertEqual(utils.slugify('Cat', regex_subs=subs), 'cat')
        self.assertEqual(
            utils.slugify('Cat', regex_subs=subs, preserve_case=False), 'cat')
        self.assertEqual(
            utils.slugify('Cat', regex_subs=subs, preserve_case=True), 'Cat')

    def test_slugify_use_unicode(self):

        samples = (('this is a test', 'this-is-a-test'),
                   ('this        is a test', 'this-is-a-test'),
                   ('this → is ← a ↑ test',
                    'this-is-a-test'), ('this--is---a test', 'this-is-a-test'),
                   ('unicode測試許功蓋,你看到了嗎?', 'unicode測試許功蓋你看到了嗎'), ('Çığ',
                                                                  'çığ'))

        settings = read_settings()
        subs = settings['SLUG_REGEX_SUBSTITUTIONS']

        for value, expected in samples:
            self.assertEqual(
                utils.slugify(value, regex_subs=subs, use_unicode=True),
                expected)

        # check with preserve case
        for value, expected in samples:
            self.assertEqual(
                utils.slugify('Çığ',
                              regex_subs=subs,
                              preserve_case=True,
                              use_unicode=True), 'Çığ')

        # check normalization
        samples = (('大飯原発4号機、18日夜起動へ', '大飯原発4号機18日夜起動へ'),
                   ('\N{LATIN SMALL LETTER C}\N{COMBINING CEDILLA}',
                    '\N{LATIN SMALL LETTER C WITH CEDILLA}'))
        for value, expected in samples:
            self.assertEqual(
                utils.slugify(value, regex_subs=subs, use_unicode=True),
                expected)

    def test_slugify_substitute(self):

        samples = (
            ('C++ is based on C', 'cpp-is-based-on-c'),
            ('C+++ test C+ test', 'cpp-test-c-test'),
            ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'),
            ('c++-streams', 'cpp-streams'),
        )

        settings = read_settings()
        subs = [
            (r'C\+\+', 'CPP'),
            (r'C#', 'C-SHARP'),
        ] + settings['SLUG_REGEX_SUBSTITUTIONS']
        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

    def test_slugify_substitute_and_keeping_non_alphanum(self):

        samples = (
            ('Fedora QA', 'fedora.qa'),
            ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'),
            ('C++ is based on C', 'cpp is based on c'),
            ('C+++ test C+ test', 'cpp+ test c+ test'),
        )

        subs = [
            (r'Fedora QA', 'fedora.qa'),
            (r'c\+\+', 'cpp'),
        ]
        for value, expected in samples:
            self.assertEqual(utils.slugify(value, regex_subs=subs), expected)

    def test_get_relative_path(self):

        samples = (
            (os.path.join('test', 'test.html'), os.pardir),
            (os.path.join('test', 'test',
                          'test.html'), os.path.join(os.pardir, os.pardir)),
            ('test.html', os.curdir),
            (os.path.join('/test', 'test.html'), os.pardir),
            (os.path.join('/test', 'test',
                          'test.html'), os.path.join(os.pardir, os.pardir)),
            ('/test.html', os.curdir),
        )

        for value, expected in samples:
            self.assertEqual(utils.get_relative_path(value), expected)

    def test_truncate_html_words(self):
        # Plain text.
        self.assertEqual(utils.truncate_html_words('short string', 20),
                         'short string')
        self.assertEqual(utils.truncate_html_words('word ' * 100, 20),
                         'word ' * 20 + '…')

        # Plain text with Unicode content.
        self.assertEqual(
            utils.truncate_html_words(
                '我愿意这样,朋友——我独自远行,不但没有你,\
                 并且再没有别的影在黑暗里。', 12), '我愿意这样,朋友——我独自远行' + ' …')
        self.assertEqual(
            utils.truncate_html_words(
                'Ты мелькнула, ты предстала, Снова сердце задрожало,', 3),
            'Ты мелькнула, ты' + ' …')
        self.assertEqual(
            utils.truncate_html_words('Trong đầm gì đẹp bằng sen', 4),
            'Trong đầm gì đẹp' + ' …')

        # Words enclosed or intervaled by HTML tags.
        self.assertEqual(
            utils.truncate_html_words('<p>' + 'word ' * 100 + '</p>', 20),
            '<p>' + 'word ' * 20 + '…</p>')
        self.assertEqual(
            utils.truncate_html_words(
                '<span\nstyle="\n…\n">' + 'word ' * 100 + '</span>', 20),
            '<span\nstyle="\n…\n">' + 'word ' * 20 + '…</span>')
        self.assertEqual(utils.truncate_html_words('<br>' + 'word ' * 100, 20),
                         '<br>' + 'word ' * 20 + '…')
        self.assertEqual(
            utils.truncate_html_words('<!-- comment -->' + 'word ' * 100, 20),
            '<!-- comment -->' + 'word ' * 20 + '…')

        # Words with hypens and apostrophes.
        self.assertEqual(utils.truncate_html_words("a-b " * 100, 20),
                         "a-b " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("it's " * 100, 20),
                         "it's " * 20 + '…')

        # Words with HTML entity references.
        self.assertEqual(utils.truncate_html_words("&eacute; " * 100, 20),
                         "&eacute; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("caf&eacute; " * 100, 20),
                         "caf&eacute; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&egrave;lite " * 100, 20),
                         "&egrave;lite " * 20 + '…')
        self.assertEqual(
            utils.truncate_html_words("cafeti&eacute;re " * 100, 20),
            "cafeti&eacute;re " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&int;dx " * 100, 20),
                         "&int;dx " * 20 + '…')

        # Words with HTML character references inside and outside
        # the ASCII range.
        self.assertEqual(utils.truncate_html_words("&#xe9; " * 100, 20),
                         "&#xe9; " * 20 + '…')
        self.assertEqual(utils.truncate_html_words("&#x222b;dx " * 100, 20),
                         "&#x222b;dx " * 20 + '…')

        # Words with invalid or broken HTML references.
        self.assertEqual(utils.truncate_html_words('&invalid;', 20),
                         '&invalid;')
        self.assertEqual(utils.truncate_html_words('&#9999999999;', 20),
                         '&#9999999999;')
        self.assertEqual(utils.truncate_html_words('&#xfffffffff;', 20),
                         '&#xfffffffff;')
        self.assertEqual(utils.truncate_html_words('&mdash text', 20),
                         '&mdash text')
        self.assertEqual(utils.truncate_html_words('&#1234 text', 20),
                         '&#1234 text')
        self.assertEqual(utils.truncate_html_words('&#xabc text', 20),
                         '&#xabc text')

    def test_process_translations(self):
        fr_articles = []
        en_articles = []

        # create a bunch of articles
        # 0: no translation metadata
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay0',
                        title='Titre',
                        content='en français'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay0',
                        title='Title',
                        content='in english'))
        # 1: translation metadata on default lang
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay1',
                        title='Titre',
                        content='en français'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay1',
                        title='Title',
                        content='in english',
                        translation='true'))
        # 2: translation metadata not on default lang
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay2',
                        title='Titre',
                        content='en français',
                        translation='true'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay2',
                        title='Title',
                        content='in english'))
        # 3: back to default language detection if all items have the
        #    translation metadata
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay3',
                        title='Titre',
                        content='en français',
                        translation='yep'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay3',
                        title='Title',
                        content='in english',
                        translation='yes'))
        # 4-5: translation pairs with the same slug but different category
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay4',
                        title='Titre',
                        content='en français',
                        category='foo'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay4',
                        title='Title',
                        content='in english',
                        category='foo'))
        fr_articles.append(
            get_article(lang='fr',
                        slug='yay4',
                        title='Titre',
                        content='en français',
                        category='bar'))
        en_articles.append(
            get_article(lang='en',
                        slug='yay4',
                        title='Title',
                        content='in english',
                        category='bar'))

        # try adding articles in both orders
        for lang0_articles, lang1_articles in ((fr_articles, en_articles),
                                               (en_articles, fr_articles)):
            articles = lang0_articles + lang1_articles

            # test process_translations with falsy translation_id
            index, trans = utils.process_translations(articles,
                                                      translation_id=None)
            for i in range(6):
                for lang_articles in [en_articles, fr_articles]:
                    self.assertIn(lang_articles[i], index)
                    self.assertNotIn(lang_articles[i], trans)

            # test process_translations with simple and complex translation_id
            for translation_id in ['slug', {'slug', 'category'}]:
                index, trans = utils.process_translations(
                    articles, translation_id=translation_id)

                for a in [
                        en_articles[0], fr_articles[1], en_articles[2],
                        en_articles[3], en_articles[4], en_articles[5]
                ]:
                    self.assertIn(a, index)
                    self.assertNotIn(a, trans)

                for a in [
                        fr_articles[0], en_articles[1], fr_articles[2],
                        fr_articles[3], fr_articles[4], fr_articles[5]
                ]:
                    self.assertIn(a, trans)
                    self.assertNotIn(a, index)

                for i in range(6):
                    self.assertIn(en_articles[i], fr_articles[i].translations)
                    self.assertIn(fr_articles[i], en_articles[i].translations)

                for a_arts in [en_articles, fr_articles]:
                    for b_arts in [en_articles, fr_articles]:
                        if translation_id == 'slug':
                            self.assertIn(a_arts[4], b_arts[5].translations)
                            self.assertIn(a_arts[5], b_arts[4].translations)
                        elif translation_id == {'slug', 'category'}:
                            self.assertNotIn(a_arts[4], b_arts[5].translations)
                            self.assertNotIn(a_arts[5], b_arts[4].translations)

    def test_filesystemwatcher(self):
        def create_file(name, content):
            with open(name, 'w') as f:
                f.write(content)

        # disable logger filter
        from pelican.utils import logger
        logger.disable_filter()

        # create a temp "project" dir
        root = mkdtemp()
        content_path = os.path.join(root, 'content')
        static_path = os.path.join(root, 'content', 'static')
        config_file = os.path.join(root, 'config.py')
        theme_path = os.path.join(root, 'mytheme')

        # populate
        os.mkdir(content_path)
        os.mkdir(theme_path)
        create_file(
            config_file, 'PATH = "content"\n'
            'THEME = "mytheme"\n'
            'STATIC_PATHS = ["static"]')

        t = time.time() - 1000  # make sure it's in the "past"
        os.utime(config_file, (t, t))
        settings = read_settings(config_file)

        watcher = utils.FileSystemWatcher(config_file, Readers, settings)
        # should get a warning for static not not existing
        self.assertLogCountEqual(1, 'Watched path does not exist: .*static')

        # create it and update config
        os.mkdir(static_path)
        watcher.update_watchers(settings)
        # no new warning
        self.assertLogCountEqual(1, 'Watched path does not exist: .*static')

        # get modified values
        modified = watcher.check()
        # empty theme and content should raise warnings
        self.assertLogCountEqual(1, 'No valid files found in content')
        self.assertLogCountEqual(1, 'Empty theme folder. Using `basic` theme')

        self.assertIsNone(modified['content'])  # empty
        self.assertIsNone(modified['theme'])  # empty
        self.assertIsNone(modified['[static]static'])  # empty
        self.assertTrue(modified['settings'])  # modified, first time

        # add a content, add file to theme and check again
        create_file(os.path.join(content_path, 'article.md'), 'Title: test\n'
                    'Date: 01-01-2020')

        create_file(os.path.join(theme_path, 'dummy'), 'test')

        modified = watcher.check()
        # no new warning
        self.assertLogCountEqual(1, 'No valid files found in content')
        self.assertLogCountEqual(1, 'Empty theme folder. Using `basic` theme')

        self.assertIsNone(modified['[static]static'])  # empty
        self.assertFalse(modified['settings'])  # not modified
        self.assertTrue(modified['theme'])  # modified
        self.assertTrue(modified['content'])  # modified

        # change config, remove static path
        create_file(
            config_file, 'PATH = "content"\n'
            'THEME = "mytheme"\n'
            'STATIC_PATHS = []')

        settings = read_settings(config_file)
        watcher.update_watchers(settings)

        modified = watcher.check()
        self.assertNotIn('[static]static', modified)  # should be gone
        self.assertTrue(modified['settings'])  # modified
        self.assertFalse(modified['content'])  # not modified
        self.assertFalse(modified['theme'])  # not modified

        # cleanup
        logger.enable_filter()
        shutil.rmtree(root)

    def test_clean_output_dir(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'clean_output')
        content = os.path.join(os.path.dirname(__file__), 'content')
        shutil.copytree(content, test_directory)
        utils.clean_output_dir(test_directory, retention)
        self.assertTrue(os.path.isdir(test_directory))
        self.assertListEqual([], os.listdir(test_directory))
        shutil.rmtree(test_directory)

    def test_clean_output_dir_not_there(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'does_not_exist')
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_clean_output_dir_is_file(self):
        retention = ()
        test_directory = os.path.join(os.path.dirname(__file__),
                                      'this_is_a_file')
        f = open(test_directory, 'w')
        f.write('')
        f.close()
        utils.clean_output_dir(test_directory, retention)
        self.assertFalse(os.path.exists(test_directory))

    def test_strftime(self):
        d = utils.SafeDatetime(2012, 8, 29)

        # simple formatting
        self.assertEqual(utils.strftime(d, '%d/%m/%y'), '29/08/12')
        self.assertEqual(utils.strftime(d, '%d/%m/%Y'), '29/08/2012')

        # RFC 3339
        self.assertEqual(utils.strftime(d, '%Y-%m-%dT%H:%M:%SZ'),
                         '2012-08-29T00:00:00Z')

        # % escaped
        self.assertEqual(utils.strftime(d, '%d%%%m%%%y'), '29%08%12')
        self.assertEqual(utils.strftime(d, '%d %% %m %% %y'), '29 % 08 % 12')
        # not valid % formatter
        self.assertEqual(utils.strftime(d, '10% reduction in %Y'),
                         '10% reduction in 2012')
        self.assertEqual(utils.strftime(d, '%10 reduction in %Y'),
                         '%10 reduction in 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Published in %d-%m-%Y'),
                         'Published in 29-08-2012')

        # with non-ascii text
        self.assertEqual(
            utils.strftime(d, '%d/%m/%Y Øl trinken beim Besäufnis'),
            '29/08/2012 Øl trinken beim Besäufnis')

        # alternative formatting options
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '29/8/12')
        self.assertEqual(utils.strftime(d, '%-H:%-M:%-S'), '0:0:0')

        d = utils.SafeDatetime(2012, 8, 9)
        self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '9/8/12')

        d = utils.SafeDatetime(2021, 1, 8)
        self.assertEqual(utils.strftime(d, '%G - %-V - %u'), '2021 - 1 - 5')

    # test the output of utils.strftime in a different locale
    # Turkish locale
    @unittest.skipUnless(
        locale_available('tr_TR.UTF-8') or locale_available('Turkish'),
        'Turkish locale needed')
    def test_strftime_locale_dependent_turkish(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_ALL)

        if platform == 'win32':
            locale.setlocale(locale.LC_ALL, 'Turkish')
        else:
            locale.setlocale(locale.LC_ALL, 'tr_TR.UTF-8')

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012')
        self.assertEqual(utils.strftime(d, '%A, %d %B %Y'),
                         'Çarşamba, 29 Ağustos 2012')

        # with text
        self.assertEqual(utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'),
                         'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012')

        # non-ascii format candidate (someone might pass it… for some reason)
        self.assertEqual(utils.strftime(d, '%Y yılında %üretim artışı'),
                         '2012 yılında %üretim artışı')

        # restore locale back
        locale.setlocale(locale.LC_ALL, old_locale)

    # test the output of utils.strftime in a different locale
    # French locale
    @unittest.skipUnless(
        locale_available('fr_FR.UTF-8') or locale_available('French'),
        'French locale needed')
    def test_strftime_locale_dependent_french(self):
        # store current locale
        old_locale = locale.setlocale(locale.LC_ALL)

        if platform == 'win32':
            locale.setlocale(locale.LC_ALL, 'French')
        else:
            locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8')

        d = utils.SafeDatetime(2012, 8, 29)

        # simple
        self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012')

        # depending on OS, the first letter is m or M
        self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi'))

        # with text
        self.assertEqual(utils.strftime(d, 'Écrit le %d %B %Y'),
                         'Écrit le 29 août 2012')

        # non-ascii format candidate (someone might pass it… for some reason)
        self.assertEqual(utils.strftime(d, '%écrits en %Y'), '%écrits en 2012')

        # restore locale back
        locale.setlocale(locale.LC_ALL, old_locale)

    def test_maybe_pluralize(self):
        self.assertEqual(utils.maybe_pluralize(0, 'Article', 'Articles'),
                         '0 Articles')
        self.assertEqual(utils.maybe_pluralize(1, 'Article', 'Articles'),
                         '1 Article')
        self.assertEqual(utils.maybe_pluralize(2, 'Article', 'Articles'),
                         '2 Articles')