diff options
| author | 2021-02-03 19:17:50 -0500 | |
|---|---|---|
| committer | 2021-02-03 19:17:50 -0500 | |
| commit | 475d074fd74425efbe783fad08f97f2df0c4909f (patch) | |
| tree | 2acdae53999b3c74b716efa4edb5b40311fa356a /tests | |
| parent | cd502d52787f666fff3254d7d7e7578930c813c2 (diff) | |
| parent | 3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (diff) | |
Update upstream source from tag 'upstream/8.1.2'
Update to upstream version '8.1.2'
with Debian dir e5e966a9e6010ef70618dc9a61558fa4db35aceb
Diffstat (limited to 'tests')
76 files changed, 4733 insertions, 3441 deletions
diff --git a/tests/README.rst b/tests/README.rst deleted file mode 100644 index cdf5c8a..0000000 --- a/tests/README.rst +++ /dev/null @@ -1,105 +0,0 @@ -.. title: The Nikola Test Suite -.. slug: tests -.. date: 2012/03/30 23:00 - -The Nikola Test Suite -===================== - -Nikola, like many software projects, has a test suite. There are over 100 -tests. - -Tests (in alphabetical order) ------------------------------ - -* ``test_command_import_wordpress`` tests the WordPress importer for - Nikola. -* ``test_command_init`` checks whether new sites are created properly via the - ``init`` command. -* ``test_compile_markdown`` exercises the Markdown compiler plugin of Nikola. -* ``test_integration`` are used to validate that sites actually build. -* ``test_locale`` tests the locale support of Nikola. -* ``test_plugin_importing`` checks three basic plugins to know whether they - get imported properly. -* ``test_rss_feeds`` asserts that RSS created by Nikola is sane. -* ``test_rst_compiler`` exercises the reStructuredText compiler plugin of - Nikola. -* ``test_scheduling`` performs tests on post scheduling rules. -* ``test_utils`` test various Nikola utilities. - -Requirements to run the tests ------------------------------ - -You need: - -* ``pip install -r requirements-tests.txt`` -* a few minutes’ time -* appropriate locale settings - -How to set the locale for Nikola tests? ---------------------------------------- - -For testing nikola needs to specify two languages, each one with a supported locale. By default, the test suite uses ``en`` and ``pl`` as languages, and their respective default locale for them. - -The choice of Polish is due to having one locale to generate instead of 20 (Spanish) and you can happily ignore it — just set the language–locale pairs by exporting two shell variables, for example:: - - export NIKOLA_LOCALE_DEFAULT=en,en_US.utf8 - export NIKOLA_LOCALE_OTHER=pl,pl_PL.utf8 - -In Windows that would be:: - - set NIKOLA_LOCALE_DEFAULT=en,English - set NIKOLA_LOCALE_OTHER=pl,Polish - -Replace the part before the comma with a Nikola translation selector (see ``nikola/conf.py.in`` for details), and the part after the comma with an *installed* glibc locale. - -To check if the desired locale is supported in your host you can, in a python console:: - - import locale - locale.setlocale(locale.LC_ALL, 'locale_name') - # for example, 'en_US.utf8' (posix) 'English' (windows) - # if it does not traceback, then python can use that locale - -Alternatively, if you have some disk space to spare, you can install -the two default locales. Here is how to do that in Ubuntu:: - - sudo apt-get install language-pack-en language-pack-pl - - -How to execute the tests ------------------------- - -The command to execute tests is:: - - doit coverage - -Note that Travis does not use this command — and as such, differences between the two may appear. - -In Windows you want to drop the doctests parts, they fail over trivial differences in OS details. - -It is also recommended to run ``nikola help`` to see if Nikola actually -works. - -If you are committing code, make sure to run ``flake8 --ignore=E501 .`` to see if you comply with the PEP 8 style guide and do not have basic code mistakes (we ignore the 79-characters-per-line rule). - -In windows ignore the two flake8 diagnostics about messages_sl_si.py , they are artifacts of (symlinks + git + windows). - - -Travis CI ---------- - -We also run our tests on `Travis CI <https://travis-ci.org/>`_. -You can check the `current build status <https://travis-ci.org/getnikola/nikola>`_ there. - - -Writing tests -------------- - -* When adding new *.py files under tests/ , remember to include at the begining the lines:: - - # This code is so you can run the samples without installing the package, - # and should be before any import touching nikola, in any file under tests/ - import os - import sys - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - - Those lines allow to run the tests without installing nikola. diff --git a/tests/__init__.py b/tests/__init__.py index ed1acfa..4e8ada2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -# Tests for Nikola. +"""Tests for Nikola.""" diff --git a/tests/base.py b/tests/base.py deleted file mode 100644 index 2322c15..0000000 --- a/tests/base.py +++ /dev/null @@ -1,232 +0,0 @@ -# coding: utf8 -# Author: Rodrigo Bistolfi -# Date: 03/2013 - - -""" Base class for Nikola test cases """ - - -__all__ = ["BaseTestCase", "cd", "LocaleSupportInTesting"] - - -import os -import sys - - -from contextlib import contextmanager -import locale -import unittest - -import logbook - -import nikola.utils -import nikola.shortcodes -nikola.utils.LOGGER.handlers.append(logbook.TestHandler()) - -from yapsy.PluginManager import PluginManager -from nikola.plugin_categories import ( - Command, - Task, - LateTask, - TemplateSystem, - PageCompiler, - TaskMultiplier, - CompilerExtension, - MarkdownExtension, - RestExtension -) - -BaseTestCase = unittest.TestCase - -@contextmanager -def cd(path): - old_dir = os.getcwd() - os.chdir(path) - yield - os.chdir(old_dir) - - -class LocaleSupportInTesting(object): - """ - Nikola needs two pairs of valid (language, locale_n) to test multilingual sites. - - As languages of interest and installed OS support varies from host to host - we allow to specify two such pairs. - - A valid pair complies - 'languaje' one of the names of nikola translations ('en', 'es', ...) - 'locale_n' is a string that python accepts to set a locale, like in - import locale - locale.setlocale(locale.LC_ALL, str(locale_n)) - - You specify the custom pairs to use with two environment variables - NIKOLA_LOCALE_DEFAULT (lang and locale to use as nikola's DEFAULT_LANG) - NIKOLA_LOCALE_OTHER - - The value of the pair is lang (as in keys of Nikola's TRANSLATIONS), followed - by coma, followed by the locale. - """ - - @classmethod - def initialize(cls): - """Determines and diagnoses the two (lang, locale) pairs to use in testing - - While it only needs to run once at the beginning of the testing session, - calling multiple times is fine. - """ - if hasattr(cls, 'langlocales'): - return - defaults = { - 'posix': { - # non-windows defaults, must be two locales suported by .travis.yml - 'default': ("en", str("en_US.utf8")), - 'other': ("pl", str("pl_PL.utf8")), - }, - 'windows': { - # windows defaults - 'default': ("en", str("English")), - 'other': ("pl", str("Polish")), - }, - } - os_id = 'windows' if sys.platform == 'win32' else 'posix' - langlocales = {} - for suffix in ['other', 'default']: - try: - envar = 'NIKOLA_LOCALE_' + suffix.upper() - s = os.environ[envar] - parts = s.split(',') - lang = parts[0].strip() - try: - locale_n = str(parts[1].strip()) - locale.setlocale(locale.LC_ALL, locale_n) - except Exception: - msg = ("Environment variable {0} fails to specify a valid <lang>,<locale>." + - "Check your syntax, check that python supports that locale in your host.") - nikola.utils.LOGGER.error(msg.format(envar)) - sys.exit(1) - except KeyError: - lang, locale_n = defaults[os_id][suffix] - langlocales[suffix] = (lang, locale_n) - if (langlocales['default'][0] == langlocales['other'][0] or - langlocales['default'][1] == langlocales['other'][1]): # NOQA - # the mix of defaults and enviro is not good - msg = ('Locales for testing should differ in lang and locale, else ' + - 'the test would we weak. Check your environment settings for ' + - 'NIKOLA_LOCALE_DEFAULT and NIKOLA_LOCALE_OTHER') - nikola.utils.LOGGER.error(msg) - setattr(cls, 'langlocales', langlocales) - - @classmethod - def initialize_locales_for_testing(cls, variant): - """initializes nikola.utils.LocaleBorg""" - if not hasattr(cls, 'langlocales'): - cls.initialize() - default_lang = cls.langlocales['default'][0] - locales = {} - locales[default_lang] = cls.langlocales['default'][1] - if variant == 'unilingual': - pass - elif variant == 'bilingual': - locales[cls.langlocales['other'][0]] = cls.langlocales['other'][1] - else: - raise ValueError('Unknown locale variant') - nikola.utils.LocaleBorg.reset() - nikola.utils.LocaleBorg.initialize(locales, default_lang) - - -class FakePost(object): - - def __init__(self, title, slug): - self._title = title - self._slug = slug - self._meta = {'slug': slug} - self.default_lang = 'en' - self._depfile = {} - - def title(self): - return self._title - - def meta(self, key): - return self._meta[key] - - def permalink(self): - return '/posts/' + self._slug - - -class FakeSite(object): - def __init__(self): - self.template_system = self - self.invariant = False - self.config = { - 'DISABLED_PLUGINS': [], - 'EXTRA_PLUGINS': [], - 'DEFAULT_LANG': 'en', - 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'], - 'TRANSLATIONS_PATTERN': '{path}.{lang}.{ext}', - 'LISTINGS_FOLDERS': {'listings': 'listings'}, - } - self.EXTRA_PLUGINS = self.config['EXTRA_PLUGINS'] - self.plugin_manager = PluginManager(categories_filter={ - "Command": Command, - "Task": Task, - "LateTask": LateTask, - "TemplateSystem": TemplateSystem, - "PageCompiler": PageCompiler, - "TaskMultiplier": TaskMultiplier, - "CompilerExtension": CompilerExtension, - "MarkdownExtension": MarkdownExtension, - "RestExtension": RestExtension - }) - self.loghandlers = nikola.utils.STDERR_HANDLER # TODO remove on v8 - self.shortcode_registry = {} - self.plugin_manager.setPluginInfoExtension('plugin') - if sys.version_info[0] == 3: - places = [ - os.path.join(os.path.dirname(nikola.utils.__file__), 'plugins'), - ] - else: - places = [ - os.path.join(os.path.dirname(nikola.utils.__file__), nikola.utils.sys_encode('plugins')), - ] - self.plugin_manager.setPluginPlaces(places) - self.plugin_manager.collectPlugins() - self.compiler_extensions = self._activate_plugins_of_category("CompilerExtension") - - self.timeline = [ - FakePost(title='Fake post', - slug='fake-post') - ] - self.debug = True - self.rst_transforms = [] - self.post_per_input_file = {} - # This is to make plugin initialization happy - self.template_system = self - self.name = 'mako' - - def _activate_plugins_of_category(self, category): - """Activate all the plugins of a given category and return them.""" - # this code duplicated in nikola/nikola.py - plugins = [] - for plugin_info in self.plugin_manager.getPluginsOfCategory(category): - if plugin_info.name in self.config.get('DISABLED_PLUGINS'): - self.plugin_manager.removePluginFromCategory(plugin_info, category) - else: - self.plugin_manager.activatePluginByName(plugin_info.name) - plugin_info.plugin_object.set_site(self) - plugins.append(plugin_info) - return plugins - - def render_template(self, name, _, context): - return('<img src="IMG.jpg">') - - # this code duplicated in nikola/nikola.py - def register_shortcode(self, name, f): - """Register function f to handle shortcode "name".""" - if name in self.shortcode_registry: - nikola.utils.LOGGER.warn('Shortcode name conflict: %s', name) - return - self.shortcode_registry[name] = f - - def apply_shortcodes(self, data, *a, **kw): - """Apply shortcodes from the registry on data.""" - return nikola.shortcodes.apply_shortcodes(data, self.shortcode_registry, **kw) diff --git a/tests/conftest.py b/tests/conftest.py index 68309d5..56fe8bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,23 @@ import os import pytest -@pytest.yield_fixture(autouse=True) +@pytest.fixture(autouse=True) def ensure_chdir(): - x = os.getcwd() - yield - os.chdir(x) + old_dir = os.getcwd() + try: + yield + finally: + os.chdir(old_dir) + + +@pytest.fixture(scope="module") +def test_dir(): + """ + Absolute path to the directory with the tests. + """ + return os.path.abspath(os.path.dirname(__file__)) + + +@pytest.fixture(scope="session") +def default_locale() -> str: + return os.environ.get("NIKOLA_LOCALE_DEFAULT", "en") diff --git a/tests/data/1-nolinks.rst b/tests/data/1-nolinks.rst index 7f168fb..a644793 100644 --- a/tests/data/1-nolinks.rst +++ b/tests/data/1-nolinks.rst @@ -1,6 +1,7 @@ .. title: Welcome to Nikola .. slug: welcome-to-nikola .. date: 2012-03-30 23:00:00 UTC-03:00 +.. updated: 2018-08-10 06:54:00Z .. tags: nikola, python, demo, blog .. author: Roberto Alsina .. link: https://getnikola.com/ diff --git a/tests/data/metadata_extractors/f-html-1-compiler.html b/tests/data/metadata_extractors/f-html-1-compiler.html new file mode 100644 index 0000000..5e95c47 --- /dev/null +++ b/tests/data/metadata_extractors/f-html-1-compiler.html @@ -0,0 +1,7 @@ +<meta name="title" content="T: HTML, 1, compiler"> +<meta name="slug" content="s-html-1-compiler"> +<meta name="date" content="2017-07-01 00:00:00 UTC"> +<meta name="tags" content="meta,HTML,onefile,compiler"> + +Content line 1. +Content line 2.
\ No newline at end of file diff --git a/tests/data/metadata_extractors/f-ipynb-1-compiler.ipynb b/tests/data/metadata_extractors/f-ipynb-1-compiler.ipynb new file mode 100644 index 0000000..3f6d18f --- /dev/null +++ b/tests/data/metadata_extractors/f-ipynb-1-compiler.ipynb @@ -0,0 +1,31 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Content line 1.\nContent line 2." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "env": {}, + "language": "python", + "name": "python3" + }, + "nikola": { + "category": "", + "date": "2017-07-01 00:00:00 UTC", + "description": "", + "link": "", + "slug": "s-ipynb-1-compiler", + "tags": "meta,Jupyter Notebook,onefile,compiler", + "title": "T: Jupyter Notebook, 1, compiler", + "type": "text" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/data/metadata_extractors/f-markdown-1-compiler.md b/tests/data/metadata_extractors/f-markdown-1-compiler.md new file mode 100644 index 0000000..689bb0f --- /dev/null +++ b/tests/data/metadata_extractors/f-markdown-1-compiler.md @@ -0,0 +1,7 @@ +title: T: Markdown, 1, compiler +slug: s-markdown-1-compiler +date: 2017-07-01 00:00:00 UTC +tags: meta,Markdown,onefile,compiler + +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-markdown-1-nikola.md b/tests/data/metadata_extractors/f-markdown-1-nikola.md new file mode 100644 index 0000000..5b38c20 --- /dev/null +++ b/tests/data/metadata_extractors/f-markdown-1-nikola.md @@ -0,0 +1,13 @@ +<!-- +.. title: T: Markdown, 1, Nikola +.. slug: s-markdown-1-nikola +.. date: 2017-07-01 00:00:00 UTC +.. tags: meta,Markdown,onefile,Nikola +.. category: +.. link: +.. description: +.. type: text +--> + +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-markdown-2-nikola.md b/tests/data/metadata_extractors/f-markdown-2-nikola.md new file mode 100644 index 0000000..0bd667b --- /dev/null +++ b/tests/data/metadata_extractors/f-markdown-2-nikola.md @@ -0,0 +1,2 @@ +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-markdown-2-nikola.meta b/tests/data/metadata_extractors/f-markdown-2-nikola.meta new file mode 100644 index 0000000..c068ba6 --- /dev/null +++ b/tests/data/metadata_extractors/f-markdown-2-nikola.meta @@ -0,0 +1,7 @@ +.. title: T: Markdown, 2, Nikola +.. slug: s-markdown-2-nikola +.. date: 2017-07-01 00:00:00 UTC +.. tags: meta,Markdown,twofile,Nikola +.. link: +.. description: +.. type: text diff --git a/tests/data/metadata_extractors/f-rest-1-compiler.rst b/tests/data/metadata_extractors/f-rest-1-compiler.rst new file mode 100644 index 0000000..3b21c3f --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-1-compiler.rst @@ -0,0 +1,9 @@ +T: reST, 1, compiler +==================== + +:slug: s-rest-1-compiler +:Date: 2017-07-01 00:00:00 UTC +:tags: meta,reST,onefile,compiler + +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-1-nikola.rst b/tests/data/metadata_extractors/f-rest-1-nikola.rst new file mode 100644 index 0000000..14dede0 --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-1-nikola.rst @@ -0,0 +1,11 @@ +.. title: T: reST, 1, Nikola +.. slug: s-rest-1-nikola +.. date: 2017-07-01 00:00:00 UTC +.. tags: meta,reST,onefile,Nikola +.. category: +.. link: +.. description: +.. type: text + +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-1-toml.rst b/tests/data/metadata_extractors/f-rest-1-toml.rst new file mode 100644 index 0000000..0e2c4eb --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-1-toml.rst @@ -0,0 +1,8 @@ ++++ +title = "T: reST, 1, TOML" +slug = "s-rest-1-toml" +date = "2017-07-01 00:00:00 UTC" +tags = "meta,reST,onefile,TOML" ++++ +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-1-yaml.rst b/tests/data/metadata_extractors/f-rest-1-yaml.rst new file mode 100644 index 0000000..b904b35 --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-1-yaml.rst @@ -0,0 +1,8 @@ +--- +title: "T: reST, 1, YAML" +slug: s-rest-1-yaml +date: "2017-07-01 00:00:00 UTC" +tags: ["meta", "reST", "onefile", "YAML"] +--- +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-2-nikola.meta b/tests/data/metadata_extractors/f-rest-2-nikola.meta new file mode 100644 index 0000000..aeb6f49 --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-nikola.meta @@ -0,0 +1,7 @@ +.. title: T: reST, 2, Nikola +.. slug: s-rest-2-nikola +.. date: 2017-07-01 00:00:00 UTC +.. tags: meta,reST,twofile,Nikola +.. link: +.. description: +.. type: text diff --git a/tests/data/metadata_extractors/f-rest-2-nikola.rst b/tests/data/metadata_extractors/f-rest-2-nikola.rst new file mode 100644 index 0000000..0bd667b --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-nikola.rst @@ -0,0 +1,2 @@ +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-2-toml.meta b/tests/data/metadata_extractors/f-rest-2-toml.meta new file mode 100644 index 0000000..4235df9 --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-toml.meta @@ -0,0 +1,6 @@ ++++ +title = "T: reST, 2, TOML" +slug = "s-rest-2-toml" +date = "2017-07-01 00:00:00 UTC" +tags = "meta,reST,twofile,TOML" ++++ diff --git a/tests/data/metadata_extractors/f-rest-2-toml.rst b/tests/data/metadata_extractors/f-rest-2-toml.rst new file mode 100644 index 0000000..0bd667b --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-toml.rst @@ -0,0 +1,2 @@ +Content line 1. +Content line 2. diff --git a/tests/data/metadata_extractors/f-rest-2-yaml.meta b/tests/data/metadata_extractors/f-rest-2-yaml.meta new file mode 100644 index 0000000..87d83bc --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-yaml.meta @@ -0,0 +1,6 @@ +--- +title: "T: reST, 2, YAML" +slug: s-rest-2-yaml +date: "2017-07-01 00:00:00 UTC" +tags: ["meta", "reST", "twofile", "YAML"] +--- diff --git a/tests/data/metadata_extractors/f-rest-2-yaml.rst b/tests/data/metadata_extractors/f-rest-2-yaml.rst new file mode 100644 index 0000000..0bd667b --- /dev/null +++ b/tests/data/metadata_extractors/f-rest-2-yaml.rst @@ -0,0 +1,2 @@ +Content line 1. +Content line 2. diff --git a/tests/rss-2_0.xsd b/tests/data/rss-2_0.xsd index d7ddaee..d7ddaee 100644 --- a/tests/rss-2_0.xsd +++ b/tests/data/rss-2_0.xsd diff --git a/tests/data/test_config/conf.py b/tests/data/test_config/conf.py new file mode 100644 index 0000000..2a2d1b8 --- /dev/null +++ b/tests/data/test_config/conf.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import time + +BLOG_AUTHOR = "Your Name" +BLOG_TITLE = "Demo Site" +SITE_URL = "https://example.com/" +BLOG_EMAIL = "joe@demo.site" +BLOG_DESCRIPTION = "This is a demo site for Nikola." +DEFAULT_LANG = "en" +CATEGORY_ALLOW_HIERARCHIES = False +CATEGORY_OUTPUT_FLAT_HIERARCHY = False +HIDDEN_CATEGORIES = [] +HIDDEN_AUTHORS = ['Guest'] +LICENSE = "" + +CONTENT_FOOTER_FORMATS = { + DEFAULT_LANG: ( + (), + { + "email": BLOG_EMAIL, + "author": BLOG_AUTHOR, + "date": time.gmtime().tm_year, + "license": LICENSE + } + ) +} + +ADDITIONAL_METADATA = { + "ID": "conf" +} diff --git a/tests/data/test_config/config.with+illegal(module)name.characters.py b/tests/data/test_config/config.with+illegal(module)name.characters.py new file mode 100644 index 0000000..39a8aeb --- /dev/null +++ b/tests/data/test_config/config.with+illegal(module)name.characters.py @@ -0,0 +1,6 @@ +import conf + +globals().update(vars(conf)) +ADDITIONAL_METADATA = { + "ID": "illegal" +} diff --git a/tests/data/test_config/prod.py b/tests/data/test_config/prod.py new file mode 100644 index 0000000..3838827 --- /dev/null +++ b/tests/data/test_config/prod.py @@ -0,0 +1,6 @@ +import conf + +globals().update(vars(conf)) +ADDITIONAL_METADATA = { + "ID": "prod" +} diff --git a/tests/data/translated_titles/conf.py b/tests/data/translated_titles/conf.py index 8faa7a9..9e8a042 100644 --- a/tests/data/translated_titles/conf.py +++ b/tests/data/translated_titles/conf.py @@ -1,95 +1,16 @@ # -*- coding: utf-8 -*- - -from __future__ import unicode_literals import time - -# !! This is the configuration of Nikola. !!# -# !! You should edit it to your liking. !!# - - -# ! Some settings can be different in different languages. -# ! A comment stating (translatable) is used to denote those. -# ! There are two ways to specify a translatable setting: -# ! (a) BLOG_TITLE = "My Blog" -# ! (b) BLOG_TITLE = {"en": "My Blog", "es": "Mi Blog"} -# ! Option (a) is there for backwards compatibility and when you don't -# ! want that setting translated. -# ! Option (b) should be used for settings that are different in -# ! different languages. - - -# Data about this site BLOG_AUTHOR = "Your Name" # (translatable) BLOG_TITLE = "Demo Site" # (translatable) -# This is the main URL for your site. It will be used -# in a prominent link SITE_URL = "https://example.com/" -# This is the URL where nikola's output will be deployed. -# If not set, defaults to SITE_URL -# BASE_URL = "https://example.com/" BLOG_EMAIL = "joe@demo.site" BLOG_DESCRIPTION = "This is a demo site for Nikola." # (translatable) - -# Nikola is multilingual! -# -# Currently supported languages are: -# bg Bulgarian -# ca Catalan -# cs Czech [ALTERNATIVELY cz] -# de German -# el Greek [NOT gr!] -# en English -# eo Esperanto -# es Spanish -# et Estonian -# eu Basque -# fa Persian -# fi Finnish -# fr French -# hi Hindi -# hr Croatian -# it Italian -# ja Japanese [NOT jp!] -# nb Norwegian Bokmål -# nl Dutch -# pt_br Portuguese (Brasil) -# pl Polish -# ru Russian -# sl Slovenian [NOT sl_si!] -# tr Turkish (Turkey) [NOT tr_tr!] -# ur Urdu -# zh_cn Chinese (Simplified) -# -# If you want to use Nikola with a non-supported language you have to provide -# a module containing the necessary translations -# (cf. the modules at nikola/data/themes/base/messages/). -# If a specific post is not translated to a language, then the version -# in the default language will be shown instead. - -# What is the default language? DEFAULT_LANG = "en" - -# What other languages do you have? -# The format is {"translationcode" : "path/to/translation" } -# the path will be used as a prefix for the generated pages location TRANSLATIONS = { "en": "", "pl": "./pl", } - -# What will translated input files be named like? - -# If you have a page something.rst, then something.pl.rst will be considered -# its Polish translation. -# (in the above example: path == "something", ext == "rst", lang == "pl") -# this pattern is also used for metadata: -# something.meta -> something.pl.meta - TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}" - -# Links for the sidebar / navigation bar. -# You should provide a key-value pair for each used language. -# (the same way you would do with a (translatable) setting.) NAVIGATION_LINKS = { DEFAULT_LANG: ( ('/archive.html', 'Archives'), @@ -97,67 +18,14 @@ NAVIGATION_LINKS = { ('/rss.xml', 'RSS'), ), } - -# Below this point, everything is optional - -# While nikola can select a sensible locale for each language, -# sometimes explicit control can come handy. -# In this file we express locales in the string form that -# python's locales will accept in your OS, by example -# "en_US.utf8" in unix-like OS, "English_United States" in Windows. -# LOCALES = dict mapping language --> explicit locale for the languages -# in TRANSLATIONS. You can ommit one or more keys. -# LOCALE_FALLBACK = locale to use when an explicit locale is unavailable -# LOCALE_DEFAULT = locale to use for languages not mentioned in LOCALES; if -# not set the default Nikola mapping is used. - -# POSTS and PAGES contains (wildcard, destination, template) tuples. -# -# The wildcard is used to generate a list of reSt source files -# (whatever/thing.txt). -# -# That fragment could have an associated metadata file (whatever/thing.meta), -# and optionally translated files (example for spanish, with code "es"): -# whatever/thing.es.txt and whatever/thing.es.meta -# -# This assumes you use the default TRANSLATIONS_PATTERN. -# -# From those files, a set of HTML fragment files will be generated: -# cache/whatever/thing.html (and maybe cache/whatever/thing.html.es) -# -# These files are combinated with the template to produce rendered -# pages, which will be placed at -# output / TRANSLATIONS[lang] / destination / pagename.html -# -# where "pagename" is the "slug" specified in the metadata file. -# -# The difference between POSTS and PAGES is that POSTS are added -# to feeds and are considered part of a blog, while PAGES are -# just independent HTML pages. -# - POSTS = ( ("posts/*.rst", "posts", "post.tmpl"), ("posts/*.txt", "posts", "post.tmpl"), ) PAGES = ( - ("pages/*.rst", "pages", "story.tmpl"), - ("pages/*.txt", "pages", "story.tmpl"), + ("pages/*.rst", "pages", "page.tmpl"), + ("pages/*.txt", "pages", "page.tmpl"), ) - -# One or more folders containing files to be copied as-is into the output. -# The format is a dictionary of "source" "relative destination". -# Default is: -# FILES_FOLDERS = {'files': '' } -# Which means copy 'files' into 'output' - -# A mapping of languages to file-extensions that represent that language. -# Feel free to add or delete extensions to any list, but don't add any new -# compilers unless you write the interface for it yourself. -# -# 'rest' is reStructuredText -# 'markdown' is MarkDown -# 'html' assumes the file is html and just copies it COMPILERS = { "rest": ('.rst', '.txt'), "markdown": ('.md', '.mdown', '.markdown'), @@ -176,237 +44,10 @@ COMPILERS = { # with many of the others. # "pandoc": ('.rst', '.md', '.txt'), } - -# Create by default posts in one file format? -# Set to False for two-file posts, with separate metadata. -# ONE_FILE_POSTS = True - -# If this is set to True, the DEFAULT_LANG version will be displayed for -# untranslated posts. -# If this is set to False, then posts that are not translated to a language -# LANG will not be visible at all in the pages in that language. -# Formerly known as HIDE_UNTRANSLATED_POSTS (inverse) -# SHOW_UNTRANSLATED_POSTS = True - -# Paths for different autogenerated bits. These are combined with the -# translation paths. - -# Final locations are: -# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) -# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) -# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) -# TAG_PATH = "categories" - -# If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain -# the posts themselves. If set to False, it will be just a list of links. -# TAG_PAGES_ARE_INDEXES = True - -# Final location for the main blog page and sibling paginated pages is -# output / TRANSLATION[lang] / INDEX_PATH / index-*.html -# INDEX_PATH = "" - -# Create per-month archives instead of per-year -# CREATE_MONTHLY_ARCHIVE = False -# Create one large archive instead of per-year -# CREATE_SINGLE_ARCHIVE = False -# Final locations for the archives are: -# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME -# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html -# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / index.html -# ARCHIVE_PATH = "" -# ARCHIVE_FILENAME = "archive.html" - -# URLs to other posts/pages can take 3 forms: -# rel_path: a relative URL to the current page/post (default) -# full_path: a URL with the full path from the root -# absolute: a complete URL (that includes the SITE_URL) -# URL_TYPE = 'rel_path' - -# Final location for the blog main RSS feed is: -# output / TRANSLATION[lang] / RSS_PATH / rss.xml -# RSS_PATH = "" - -# Number of posts in RSS feeds -# FEED_LENGTH = 10 - -# Slug the Tag URL easier for users to type, special characters are -# often removed or replaced as well. -# SLUG_TAG_PATH = True - -# A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. -# -# A HTML file will be created in output/foo/from.html that redirects -# to the "/bar/to.html" URL. notice that the "from" side MUST be a -# relative URL. -# -# If you don't need any of these, just set to [] REDIRECTIONS = [] - -# Commands to execute to deploy. Can be anything, for example, -# you may use rsync: -# "rsync -rav --delete output/ joe@my.site:/srv/www/site" -# And then do a backup, or run `nikola ping` from the `ping` -# plugin (`nikola install_plugin ping`). -# To do manual deployment, set it to [] -# DEPLOY_COMMANDS = [] - -# Where the output site should be located -# If you don't use an absolute path, it will be considered as relative -# to the location of conf.py -# OUTPUT_FOLDER = 'output' - -# where the "cache" of partial generated content should be located -# default: 'cache' -# CACHE_FOLDER = 'cache' - -# Filters to apply to the output. -# A directory where the keys are either: a file extensions, or -# a tuple of file extensions. -# -# And the value is a list of commands to be applied in order. -# -# Each command must be either: -# -# A string containing a '%s' which will -# be replaced with a filename. The command *must* produce output -# in place. -# -# Or: -# -# A python callable, which will be called with the filename as -# argument. -# -# By default, there are no filters. -# -# Many filters are shipped with Nikola. A list is available in the manual: -# <https://getnikola.com/handbook.html#post-processing-filters> -# FILTERS = { -# ".jpg": ["jpegoptim --strip-all -m75 -v %s"], -# } - -# Expert setting! Create a gzipped copy of each generated file. Cheap server- -# side optimization for very high traffic sites or low memory servers. -# GZIP_FILES = False -# File extensions that will be compressed -# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml') -# Use an external gzip command? None means no. -# Example: GZIP_COMMAND = "pigz -k {filename}" -# GZIP_COMMAND = None -# Make sure the server does not return a "Accept-Ranges: bytes" header for -# files compressed by this option! OR make sure that a ranged request does not -# return partial content of another representation for these resources. Do not -# use this feature if you do not understand what this means. - -# Compiler to process LESS files. -# LESS_COMPILER = 'lessc' - -# A list of options to pass to the LESS compiler. -# Final command is: LESS_COMPILER LESS_OPTIONS file.less -# LESS_OPTIONS = [] - -# Compiler to process Sass files. -# SASS_COMPILER = 'sass' - -# A list of options to pass to the Sass compiler. -# Final command is: SASS_COMPILER SASS_OPTIONS file.s(a|c)ss -# SASS_OPTIONS = [] - -# ############################################################################# -# Image Gallery Options -# ############################################################################# - -# Galleries are folders in galleries/ -# Final location of galleries will be output / GALLERY_PATH / gallery_name -# GALLERY_PATH = "galleries" -# THUMBNAIL_SIZE = 180 -# MAX_IMAGE_SIZE = 1280 -# USE_FILENAME_AS_TITLE = True -# EXTRA_IMAGE_EXTENSIONS = [] -# -# If set to False, it will sort by filename instead. Defaults to True -# GALLERY_SORT_BY_DATE = True - -# ############################################################################# -# HTML fragments and diverse things that are used by the templates -# ############################################################################# - -# Data about post-per-page indexes. -# INDEXES_PAGES defaults to 'old posts, page %d' or 'page %d' (translated), -# depending on the value of INDEXES_PAGES_MAIN. -# INDEXES_TITLE = "" # If this is empty, defaults to BLOG_TITLE -# INDEXES_PAGES = "" # If this is empty, defaults to '[old posts,] page %d' (see above) -# INDEXES_PAGES_MAIN = False # If True, INDEXES_PAGES is also displayed on -# # the main (the newest) index page (index.html) - -# Name of the theme to use. -THEME = "bootstrap3" - -# Color scheme to be used for code blocks. If your theme provides -# "assets/css/code.css" this is ignored. -# Can be any of autumn borland bw colorful default emacs friendly fruity manni -# monokai murphy native pastie perldoc rrt tango trac vim vs -# CODE_COLOR_SCHEME = 'default' - -# If you use 'site-reveal' theme you can select several subthemes -# THEME_REVEAL_CONFIG_SUBTHEME = 'sky' -# You can also use: beige/serif/simple/night/default - -# Again, if you use 'site-reveal' theme you can select several transitions -# between the slides -# THEME_REVEAL_CONFIG_TRANSITION = 'cube' -# You can also use: page/concave/linear/none/default - -# date format used to display post dates. -# (str used by datetime.datetime.strftime) -# DATE_FORMAT = '%Y-%m-%d %H:%M' - -# FAVICONS contains (name, file, size) tuples. -# Used for create favicon link like this: -# <link rel="name" href="file" sizes="size"/> -# For creating favicons, take a look at: -# http://www.netmagazine.com/features/create-perfect-favicon -# FAVICONS = { -# ("icon", "/favicon.ico", "16x16"), -# ("icon", "/icon_128x128.png", "128x128"), -# } - -# Show only teasers in the index pages? Defaults to False. -# INDEX_TEASERS = False - -# A HTML fragment with the Read more... link. -# The following tags exist and are replaced for you: -# {link} A link to the full post page. -# {read_more} The string “Read more” in the current language. -# {{ A literal { (U+007B LEFT CURLY BRACKET) -# }} A literal } (U+007D RIGHT CURLY BRACKET) -# READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>' - -# A HTML fragment describing the license, for the sidebar. -# (translatable) +THEME = "bootblog4" LICENSE = "" -# I recommend using the Creative Commons' wizard: -# http://creativecommons.org/choose/ -# LICENSE = """ -# <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> -# <img alt="Creative Commons License BY-NC-SA" -# style="border-width:0; margin-bottom:12px;" -# src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" - -# A small copyright notice for the page footer (in HTML). -# (translatable) CONTENT_FOOTER = 'Contents © {date} <a href="mailto:{email}">{author}</a> - Powered by <a href="https://getnikola.com/" rel="nofollow">Nikola</a> {license}' - -# Things that will be passed to CONTENT_FOOTER.format(). This is done -# for translatability, as dicts are not formattable. Nikola will -# intelligently format the setting properly. -# The setting takes a dict. The keys are languages. The values are -# tuples of tuples of positional arguments and dicts of keyword arguments -# to format(). For example, {'en': (('Hello'), {'target': 'World'})} -# results in CONTENT_FOOTER['en'].format('Hello', target='World'). -# WARNING: If you do not use multiple languages with CONTENT_FOOTER, this -# still needs to be a dict of this format. (it can be empty if you -# do not need formatting) -# (translatable) CONTENT_FOOTER_FORMATS = { DEFAULT_LANG: ( (), @@ -418,335 +59,6 @@ CONTENT_FOOTER_FORMATS = { } ) } - -# To use comments, you can choose between different third party comment -# systems, one of "disqus", "livefyre", "intensedebate", "moot", -# "googleplus", "facebook" or "isso" COMMENT_SYSTEM = "disqus" -# And you also need to add your COMMENT_SYSTEM_ID which -# depends on what comment system you use. The default is -# "nikolademo" which is a test account for Disqus. More information -# is in the manual. COMMENT_SYSTEM_ID = "nikolademo" - -# Enable annotations using annotateit.org? -# If set to False, you can still enable them for individual posts and pages -# setting the "annotations" metadata. -# If set to True, you can disable them for individual posts and pages using -# the "noannotations" metadata. -# ANNOTATIONS = False - -# Create index.html for page folders? -# PAGE_INDEX = False -# Enable comments on page pages? -# COMMENTS_IN_PAGES = False -# Enable comments on picture gallery pages? -# COMMENTS_IN_GALLERIES = False - -# What file should be used for directory indexes? -# Defaults to index.html -# Common other alternatives: default.html for IIS, index.php -# INDEX_FILE = "index.html" - -# If a link ends in /index.html, drop the index.html part. -# http://mysite/foo/bar/index.html => http://mysite/foo/bar/ -# (Uses the INDEX_FILE setting, so if that is, say, default.html, -# it will instead /foo/default.html => /foo) -# (Note: This was briefly STRIP_INDEX_HTML in v 5.4.3 and 5.4.4) -# Default = False -# STRIP_INDEXES = False - -# Should the sitemap list directories which only include other directories -# and no files. -# Default to True -# If this is False -# e.g. /2012 includes only /01, /02, /03, /04, ...: don't add it to the sitemap -# if /2012 includes any files (including index.html)... add it to the sitemap -# SITEMAP_INCLUDE_FILELESS_DIRS = True - -# Instead of putting files in <slug>.html, put them in -# <slug>/index.html. Also enables STRIP_INDEXES -# This can be disabled on a per-page/post basis by adding -# .. pretty_url: False -# to the metadata -# PRETTY_URLS = False - -# If True, publish future dated posts right away instead of scheduling them. -# Defaults to False. -# FUTURE_IS_NOW = False - -# If True, future dated posts are allowed in deployed output -# Only the individual posts are published/deployed; not in indexes/sitemap -# Generally, you want FUTURE_IS_NOW and DEPLOY_FUTURE to be the same value. -# DEPLOY_FUTURE = False -# If False, draft posts will not be deployed -# DEPLOY_DRAFTS = True - -# Allows scheduling of posts using the rule specified here (new_post -s) -# Specify an iCal Recurrence Rule: http://www.kanzaki.com/docs/ical/rrule.html -# SCHEDULE_RULE = '' -# If True, use the scheduling rule to all posts by default -# SCHEDULE_ALL = False -# If True, schedules post to today if possible, even if scheduled hour is over -# SCHEDULE_FORCE_TODAY = False - -# Do you want a add a Mathjax config file? -# MATHJAX_CONFIG = "" - -# If you are using the compile-ipynb plugin, just add this one: -# MATHJAX_CONFIG = """ -# <script type="text/x-mathjax-config"> -# MathJax.Hub.Config({ -# tex2jax: { -# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ], -# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ] -# }, -# displayAlign: 'left', // Change this to 'center' to center equations. -# "HTML-CSS": { -# styles: {'.MathJax_Display': {"margin": 0}} -# } -# }); -# </script> -# """ - -# Do you want to customize the nbconversion of your IPython notebook? -# IPYNB_CONFIG = {} -# With the following example configuracion you can use a custom jinja template -# called `toggle.tpl` which has to be located in your site/blog main folder: -# IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}} - -# What MarkDown extensions to enable? -# You will also get gist, nikola and podcast because those are -# done in the code, hope you don't mind ;-) -# MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite'] - -# Social buttons. This is sample code for AddThis (which was the default for a -# long time). Insert anything you want here, or even make it empty. -# (translatable) -# SOCIAL_BUTTONS_CODE = """ -# <!-- Social buttons --> -# <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style"> -# <a class="addthis_button_more">Share</a> -# <ul><li><a class="addthis_button_facebook"></a> -# <li><a class="addthis_button_google_plusone_share"></a> -# <li><a class="addthis_button_linkedin"></a> -# <li><a class="addthis_button_twitter"></a> -# </ul> -# </div> -# <script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> -# <!-- End of social buttons --> -# """ - -# Show link to source for the posts? -# Formerly known as HIDE_SOURCELINK (inverse) -# SHOW_SOURCELINK = True -# Copy the source files for your pages? -# Setting it to False implies SHOW_SOURCELINK = False -# COPY_SOURCES = True - -# Modify the number of Post per Index Page -# Defaults to 10 -# INDEX_DISPLAY_POST_COUNT = 10 - -# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, -# the base.tmpl will use the feed Nikola generates. However, you may want to -# change it for a feedburner feed or something else. -# RSS_LINK = None - -# Show only teasers in the RSS feed? Default to True -# RSS_TEASERS = True - -# Strip HTML in the RSS feed? Default to False -# RSS_PLAIN = False - -# A search form to search this site, for the sidebar. You can use a google -# custom search (http://www.google.com/cse/) -# Or a duckduckgo search: https://duckduckgo.com/search_box.html -# Default is no search form. -# (translatable) -# SEARCH_FORM = "" -# -# This search form works for any site and looks good in the "site" theme where -# it appears on the navigation bar: -# -# SEARCH_FORM = """ -# <!-- Custom search --> -# <form method="get" id="search" action="//duckduckgo.com/" -# class="navbar-form pull-left"> -# <input type="hidden" name="sites" value="%s"/> -# <input type="hidden" name="k8" value="#444444"/> -# <input type="hidden" name="k9" value="#D51920"/> -# <input type="hidden" name="kt" value="h"/> -# <input type="text" name="q" maxlength="255" -# placeholder="Search…" class="span2" style="margin-top: 4px;"/> -# <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> -# </form> -# <!-- End of custom search --> -# """ % SITE_URL -# -# If you prefer a google search form, here's an example that should just work: -# SEARCH_FORM = """ -# <!-- Custom search with google--> -# <form id="search" action="//www.google.com/search" method="get" class="navbar-form pull-left"> -# <input type="hidden" name="q" value="site:%s" /> -# <input type="text" name="q" maxlength="255" results="0" placeholder="Search"/> -# </form> -# <!-- End of custom search --> -# """ % SITE_URL - -# Also, there is a local search plugin you can use, based on Tipue, but it requires setting several -# options: - -# SEARCH_FORM = """ -# <span class="navbar-form pull-left"> -# <input type="text" id="tipue_search_input"> -# </span>""" -# -# BODY_END = """ -# <script src="/assets/js/tipuesearch_set.js"></script> -# <script src="/assets/js/tipuesearch.js"></script> -# <script> -# $(document).ready(function() { -# $('#tipue_search_input').tipuesearch({ -# 'mode': 'json', -# 'contentLocation': '/assets/js/tipuesearch_content.json', -# 'showUrl': false -# }); -# }); -# </script> -# """ - -# EXTRA_HEAD_DATA = """ -# <link rel="stylesheet" type="text/css" href="/assets/css/tipuesearch.css"> -# <div id="tipue_search_content" style="margin-left: auto; margin-right: auto; padding: 20px;"></div> -# """ -# ENABLED_EXTRAS = ['local_search'] -# - - -# Use content distribution networks for jquery and twitter-bootstrap css and js -# If this is True, jquery and html5shiv are served from the Google CDN and -# Bootstrap is served from BootstrapCDN (provided by MaxCDN) -# Set this to False if you want to host your site without requiring access to -# external resources. -# USE_CDN = False - -# Extra things you want in the pages HEAD tag. This will be added right -# before </head> -# (translatable) -# EXTRA_HEAD_DATA = "" -# Google Analytics or whatever else you use. Added to the bottom of <body> -# in the default template (base.tmpl). -# (translatable) -# BODY_END = "" - -# The possibility to extract metadata from the filename by using a -# regular expression. -# To make it work you need to name parts of your regular expression. -# The following names will be used to extract metadata: -# - title -# - slug -# - date -# - tags -# - link -# - description -# -# An example re is the following: -# '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md' -# FILE_METADATA_REGEXP = None - -# Additional metadata that is added to a post when creating a new_post -# ADDITIONAL_METADATA = {} - -# Nikola supports Twitter Card summaries / Open Graph. -# Twitter cards make it possible for you to attach media to Tweets -# that link to your content. -# -# IMPORTANT: -# Please note, that you need to opt-in for using Twitter Cards! -# To do this please visit -# https://dev.twitter.com/form/participate-twitter-cards -# -# Uncomment and modify to following lines to match your accounts. -# Specifying the id for either 'site' or 'creator' will be preferred -# over the cleartext username. Specifying an ID is not necessary. -# Displaying images is currently not supported. -# TWITTER_CARD = { -# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph -# # 'site': '@website', # twitter nick for the website -# # 'site:id': 123456, # Same as site, but the website's Twitter user ID -# # instead. -# # 'creator': '@username', # Username for the content creator / author. -# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID. -# } - - -# Post's dates are considered in UTC by default, if you want to use -# another time zone, please set TIMEZONE to match. Check the available -# list from Wikipedia: -# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones -# (eg. 'Europe/Zurich') -# Also, if you want to use a different time zone in some of your posts, -# you can use W3C-DTF Format (ex. 2012-03-30T23:00:00+02:00) -# -# TIMEZONE = 'UTC' - -# If webassets is installed, bundle JS and CSS to make site loading faster -# USE_BUNDLES = True - -# Plugins you don't want to use. Be careful :-) -# DISABLED_PLUGINS = ["render_galleries"] - -# Add the absolute paths to directories containing plugins to use them. -# For example, the `plugins` directory of your clone of the Nikola plugins -# repository. -# EXTRA_PLUGINS_DIRS = [] - -# Experimental plugins - use at your own risk. -# They probably need some manual adjustments - please see their respective -# readme. -# ENABLED_EXTRAS = [ -# 'planetoid', -# 'ipynb', -# 'local_search', -# 'render_mustache', -# ] - -# List of regular expressions, links matching them will always be considered -# valid by "nikola check -l" -# LINK_CHECK_WHITELIST = [] - -# If set to True, enable optional hyphenation in your posts (requires pyphen) -# HYPHENATE = False - -# The <hN> tags in HTML generated by certain compilers (reST/Markdown) -# will be demoted by that much (1 → h1 will become h2 and so on) -# This was a hidden feature of the Markdown and reST compilers in the -# past. Useful especially if your post titles are in <h1> tags too, for -# example. -# (defaults to 1.) -# DEMOTE_HEADERS = 1 - -# You can configure the logging handlers installed as plugins or change the -# log level of the default stdout handler. -LOGGING_HANDLERS = { - 'stderr': {'loglevel': 'WARNING', 'bubble': True}, - # 'smtp': { - # 'from_addr': 'test-errors@example.com', - # 'recipients': ('test@example.com'), - # 'credentials':('testusername', 'password'), - # 'server_addr': ('127.0.0.1', 25), - # 'secure': (), - # 'level': 'DEBUG', - # 'bubble': True - # } -} - -# Templates will use those filters, along with the defaults. -# Consult your engine's documentation on filters if you need help defining -# those. -# TEMPLATE_FILTERS = {} - -# Put in global_context things you want available on all your templates. -# It can be anything, data, functions, modules, etc. GLOBAL_CONTEXT = {} diff --git a/tests/wordpress_export_example.xml b/tests/data/wordpress_import/wordpress_export_example.xml index e2401f7..e2401f7 100644 --- a/tests/wordpress_export_example.xml +++ b/tests/data/wordpress_import/wordpress_export_example.xml diff --git a/tests/data/wordpress_import/wordpress_qtranslate_item_modernized.xml b/tests/data/wordpress_import/wordpress_qtranslate_item_modernized.xml new file mode 100644 index 0000000..2622bfd --- /dev/null +++ b/tests/data/wordpress_import/wordpress_qtranslate_item_modernized.xml @@ -0,0 +1,30 @@ + <item> + <title>[:fr]Sous le ciel[:][:en]Under heaven[:]</title> + <link>http://www.tibonihoo.net/blog/2014/05/sous-le-ciel/</link> + <pubDate>Sat, 03 May 2014 13:20:32 +0000</pubDate> + <dc:creator><![CDATA[tibonihoo_admin]]></dc:creator> + <guid isPermaLink="false">http://www.tibonihoo.net/blog/?p=1585</guid> + <description></description> + <content:encoded><![CDATA[[:fr]<a href="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511.jpg"><img class="alignnone size-medium wp-image-1587" src="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511-300x199.jpg" alt="IMG_6851" width="300" height="199" /></a>[:][:en]<img class="alignnone size-medium wp-image-1587" src="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511-300x199.jpg" alt="IMG_6851" width="300" height="199" />[:]]]></content:encoded> + <excerpt:encoded><![CDATA[]]></excerpt:encoded> + <wp:post_id>1585</wp:post_id> + <wp:post_date><![CDATA[2014-05-03 14:20:32]]></wp:post_date> + <wp:post_date_gmt><![CDATA[2014-05-03 13:20:32]]></wp:post_date_gmt> + <wp:comment_status><![CDATA[open]]></wp:comment_status> + <wp:ping_status><![CDATA[closed]]></wp:ping_status> + <wp:post_name><![CDATA[sous-le-ciel]]></wp:post_name> + <wp:status><![CDATA[publish]]></wp:status> + <wp:post_parent>0</wp:post_parent> + <wp:menu_order>0</wp:menu_order> + <wp:post_type><![CDATA[post]]></wp:post_type> + <wp:post_password><![CDATA[]]></wp:post_password> + <wp:is_sticky>0</wp:is_sticky> + <category domain="post_tag" nicename="chine"><![CDATA[Chine]]></category> + <category domain="category" nicename="creations"><![CDATA[créations]]></category> + <category domain="post_tag" nicename="photos"><![CDATA[photos]]></category> + <category domain="post_tag" nicename="roof"><![CDATA[roof]]></category> + <wp:postmeta> + <wp:meta_key><![CDATA[_edit_last]]></wp:meta_key> + <wp:meta_value><![CDATA[2]]></wp:meta_value> + </wp:postmeta> + </item> diff --git a/tests/data/wordpress_import/wordpress_qtranslate_item_raw_export.xml b/tests/data/wordpress_import/wordpress_qtranslate_item_raw_export.xml new file mode 100644 index 0000000..50bac7b --- /dev/null +++ b/tests/data/wordpress_import/wordpress_qtranslate_item_raw_export.xml @@ -0,0 +1,30 @@ + <item> + <title><!--:fr-->Sous le ciel<!--:--><!--:en-->Under heaven<!--:--></title> + <link>http://www.tibonihoo.net/blog/2014/05/sous-le-ciel/</link> + <pubDate>Sat, 03 May 2014 13:20:32 +0000</pubDate> + <dc:creator><![CDATA[tibonihoo_admin]]></dc:creator> + <guid isPermaLink="false">http://www.tibonihoo.net/blog/?p=1585</guid> + <description></description> + <content:encoded><![CDATA[<!--:fr--><a href="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511.jpg"><img class="alignnone size-medium wp-image-1587" src="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511-300x199.jpg" alt="IMG_6851" width="300" height="199" /></a><!--:--><!--:en--><img class="alignnone size-medium wp-image-1587" src="http://www.tibonihoo.net/blog/wp-content/uploads/2014/05/IMG_68511-300x199.jpg" alt="IMG_6851" width="300" height="199" /><!--:-->]]></content:encoded> + <excerpt:encoded><![CDATA[]]></excerpt:encoded> + <wp:post_id>1585</wp:post_id> + <wp:post_date><![CDATA[2014-05-03 14:20:32]]></wp:post_date> + <wp:post_date_gmt><![CDATA[2014-05-03 13:20:32]]></wp:post_date_gmt> + <wp:comment_status><![CDATA[open]]></wp:comment_status> + <wp:ping_status><![CDATA[closed]]></wp:ping_status> + <wp:post_name><![CDATA[sous-le-ciel]]></wp:post_name> + <wp:status><![CDATA[publish]]></wp:status> + <wp:post_parent>0</wp:post_parent> + <wp:menu_order>0</wp:menu_order> + <wp:post_type><![CDATA[post]]></wp:post_type> + <wp:post_password><![CDATA[]]></wp:post_password> + <wp:is_sticky>0</wp:is_sticky> + <category domain="post_tag" nicename="chine"><![CDATA[Chine]]></category> + <category domain="category" nicename="creations"><![CDATA[créations]]></category> + <category domain="post_tag" nicename="photos"><![CDATA[photos]]></category> + <category domain="post_tag" nicename="roof"><![CDATA[roof]]></category> + <wp:postmeta> + <wp:meta_key><![CDATA[_edit_last]]></wp:meta_key> + <wp:meta_value><![CDATA[2]]></wp:meta_value> + </wp:postmeta> + </item> diff --git a/tests/wordpress_unicode_export.xml b/tests/data/wordpress_import/wordpress_unicode_export.xml index b2204fc..b2204fc 100644 --- a/tests/wordpress_unicode_export.xml +++ b/tests/data/wordpress_import/wordpress_unicode_export.xml diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..36558da --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,131 @@ +""" +Helper utilities and classes for Nikola tests. + +Alongside a contextmanager to switch directories this module contains +a Site substitute for rendering tests. +""" + +import os +from contextlib import contextmanager + +from yapsy.PluginManager import PluginManager + +import nikola.utils +import nikola.shortcodes +from nikola.plugin_categories import ( + Command, + Task, + LateTask, + TemplateSystem, + PageCompiler, + TaskMultiplier, + CompilerExtension, + MarkdownExtension, + RestExtension, +) + +__all__ = ["cd", "FakeSite"] + + +@contextmanager +def cd(path): + old_dir = os.getcwd() + os.chdir(path) + yield + os.chdir(old_dir) + + +class FakeSite: + def __init__(self): + self.template_system = self + self.invariant = False + self.debug = True + self.config = { + "DISABLED_PLUGINS": [], + "EXTRA_PLUGINS": [], + "DEFAULT_LANG": "en", + "MARKDOWN_EXTENSIONS": [ + "markdown.extensions.fenced_code", + "markdown.extensions.codehilite", + ], + "TRANSLATIONS_PATTERN": "{path}.{lang}.{ext}", + "LISTINGS_FOLDERS": {"listings": "listings"}, + "TRANSLATIONS": {"en": ""}, + } + self.EXTRA_PLUGINS = self.config["EXTRA_PLUGINS"] + self.plugin_manager = PluginManager( + categories_filter={ + "Command": Command, + "Task": Task, + "LateTask": LateTask, + "TemplateSystem": TemplateSystem, + "PageCompiler": PageCompiler, + "TaskMultiplier": TaskMultiplier, + "CompilerExtension": CompilerExtension, + "MarkdownExtension": MarkdownExtension, + "RestExtension": RestExtension, + } + ) + self.shortcode_registry = {} + self.plugin_manager.setPluginInfoExtension("plugin") + places = [os.path.join(os.path.dirname(nikola.utils.__file__), "plugins")] + self.plugin_manager.setPluginPlaces(places) + self.plugin_manager.collectPlugins() + self.compiler_extensions = self._activate_plugins_of_category( + "CompilerExtension" + ) + + self.timeline = [FakePost(title="Fake post", slug="fake-post")] + self.rst_transforms = [] + self.post_per_input_file = {} + # This is to make plugin initialization happy + self.template_system = self + self.name = "mako" + + def _activate_plugins_of_category(self, category): + """Activate all the plugins of a given category and return them.""" + # this code duplicated in nikola/nikola.py + plugins = [] + for plugin_info in self.plugin_manager.getPluginsOfCategory(category): + if plugin_info.name in self.config.get("DISABLED_PLUGINS"): + self.plugin_manager.removePluginFromCategory(plugin_info, category) + else: + self.plugin_manager.activatePluginByName(plugin_info.name) + plugin_info.plugin_object.set_site(self) + plugins.append(plugin_info) + return plugins + + def render_template(self, name, _, context): + return '<img src="IMG.jpg">' + + # this code duplicated in nikola/nikola.py + def register_shortcode(self, name, f): + """Register function f to handle shortcode "name".""" + if name in self.shortcode_registry: + nikola.utils.LOGGER.warning('Shortcode name conflict: %s', name) + return + self.shortcode_registry[name] = f + + def apply_shortcodes(self, data, *a, **kw): + """Apply shortcodes from the registry on data.""" + return nikola.shortcodes.apply_shortcodes(data, self.shortcode_registry, **kw) + + def apply_shortcodes_uuid(self, data, shortcodes, *a, **kw): + """Apply shortcodes from the registry on data.""" + return nikola.shortcodes.apply_shortcodes(data, self.shortcode_registry, **kw) + + +class FakePost: + def __init__(self, title, slug): + self._title = title + self._slug = slug + self._meta = {"slug": slug} + + def title(self): + return self._title + + def meta(self, key): + return self._meta[key] + + def permalink(self): + return "/posts/" + self._slug diff --git a/tests/import_wordpress_and_build_workflow.py b/tests/import_wordpress_and_build_workflow.py deleted file mode 100644 index bc04a1f..0000000 --- a/tests/import_wordpress_and_build_workflow.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Script to test the import workflow. - -It will remove an existing Nikola installation and then install from the -package directory. -After that it will do create a new site with the import_wordpress -command and use that newly created site to make a build. -""" -from __future__ import unicode_literals, print_function - -import os -import shutil - -TEST_SITE_DIRECTORY = 'import_test_site' - - -def main(import_directory=None): - if import_directory is None: - import_directory = TEST_SITE_DIRECTORY - - if os.path.exists(import_directory): - print('deleting %s' % import_directory) - shutil.rmtree(import_directory) - - test_directory = os.path.dirname(__file__) - package_directory = os.path.abspath(os.path.join(test_directory, '..')) - - os.system('pip uninstall -y Nikola') - os.system('pip install %s' % package_directory) - os.system('nikola') - import_file = os.path.join(test_directory, 'wordpress_export_example.xml') - os.system( - 'nikola import_wordpress -o {folder} {file}'.format(file=import_file, - folder=import_directory)) - - assert os.path.exists( - import_directory), "The directory %s should be existing." - os.chdir(import_directory) - os.system('nikola build') - -if __name__ == '__main__': - main() diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..ee2f4ea --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,12 @@ +""" +Various integration tests for the Nikola commands. + +Each test module provides a different build fixture that will create a +build in the desired way that then later can be checked in test +functions. +To avoid duplicating code many of the fixtures are shared between +modules. + +The build fixtures are scoped for module level in order to avoid +re-building the whole site for every test and to make these tests fast. +""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..a87cb07 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,74 @@ +import os +import sys + +import docutils +import pytest + +from nikola.utils import LocaleBorg +from ..helper import FakeSite + + +@pytest.fixture(scope="module") +def test_dir(): + """ + Absolute path to the directory with the tests. + """ + return os.path.abspath(os.path.dirname(__file__)) + + +@pytest.fixture(scope="session") +def other_locale() -> str: + return os.environ.get("NIKOLA_LOCALE_OTHER", "pl") + + +@pytest.fixture(scope="module") +def output_dir(target_dir): + return os.path.join(target_dir, "output") + + +@pytest.fixture(scope="module") +def target_dir(tmpdir_factory): + tdir = tmpdir_factory.mktemp("integration").join("target") + yield str(tdir) + + +@pytest.fixture(scope="module", autouse=True) +def remove_conf_module(): + """ + Remove the module `conf` from `sys.modules` after loading the config. + + Fixes issue #438 + """ + try: + yield + finally: + try: + del sys.modules["conf"] + except KeyError: + pass + + +@pytest.fixture(scope="module", autouse=True) +def localeborg_setup(default_locale): + """ + Reset the LocaleBorg before and after every test. + """ + LocaleBorg.reset() + LocaleBorg.initialize({}, default_locale) + try: + yield + finally: + LocaleBorg.reset() + + +@pytest.fixture(autouse=True, scope="module") +def fix_leaked_state(): + """Fix leaked state from integration tests""" + try: + yield + finally: + try: + func = docutils.parsers.rst.roles.role("doc", None, None, None)[0] + func.site = FakeSite() + except AttributeError: + pass diff --git a/tests/integration/helper.py b/tests/integration/helper.py new file mode 100644 index 0000000..c120721 --- /dev/null +++ b/tests/integration/helper.py @@ -0,0 +1,56 @@ +import io +import os +import shutil + +from ..helper import cd + +__all__ = ["add_post_without_text", "append_config", "cd", "create_simple_post", "patch_config"] + + +def add_post_without_text(directory): + """Add a post without text.""" + # File for Issue #374 (empty post text) + create_simple_post(directory, "empty.txt", "foobar") + + +def create_simple_post(directory, filename, title_slug, text='', date='2013-03-06 19:08:15'): + """Create a simple post in a given directory.""" + path = os.path.join(directory, filename) + text_processed = '\n' + text if text else '' + with io.open(path, "w+", encoding="utf8") as outf: + outf.write( + """ +.. title: {0} +.. slug: {0} +.. date: {1} +{2}""".format(title_slug, date, text_processed) + ) + + +def copy_example_post(destination_dir): + """Copy a modified version of the example post into the site.""" + test_dir = os.path.abspath(os.path.dirname(__file__)) + source_file = os.path.join(test_dir, "..", "data", "1-nolinks.rst") + destination = os.path.join(destination_dir, "1.rst") + shutil.copy(source_file, destination) + + +def append_config(config_dir, appendix): + """Append text to the config file.""" + config_path = os.path.join(config_dir, "conf.py") + with io.open(config_path, "a", encoding="utf8") as outf: + outf.write(appendix) + + +def patch_config(config_dir, *replacements): + """Patch the config file with new values (find and replace).""" + config_path = os.path.join(config_dir, "conf.py") + with io.open(config_path, "r", encoding="utf-8") as inf: + data = inf.read() + + for old, new in replacements: + data = data.replace(old, new) + + with io.open(config_path, "w+", encoding="utf8") as outf: + outf.write(data) + outf.flush() diff --git a/tests/integration/test_archive_full.py b/tests/integration/test_archive_full.py new file mode 100644 index 0000000..70d9504 --- /dev/null +++ b/tests/integration/test_archive_full.py @@ -0,0 +1,44 @@ +"""Check that full archives build and are correct.""" + +import os + +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +@pytest.mark.parametrize( + "path", + [ + pytest.param(["archive.html"], id="overall"), + pytest.param(["2012", "index.html"], id="year"), + pytest.param(["2012", "03", "index.html"], id="month"), + pytest.param(["2012", "03", "30", "index.html"], id="day"), + ], +) +def test_full_archive(build, output_dir, path): + """Check existance of archive pages""" + expected_path = os.path.join(output_dir, *path) + assert os.path.isfile(expected_path) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + patch_config( + target_dir, ("# CREATE_FULL_ARCHIVES = False", "CREATE_FULL_ARCHIVES = True") + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_archive_per_day.py b/tests/integration/test_archive_per_day.py new file mode 100644 index 0000000..52578a6 --- /dev/null +++ b/tests/integration/test_archive_per_day.py @@ -0,0 +1,36 @@ +"""Check that per-day archives build and are correct.""" + +import os + +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_day_archive(build, output_dir): + """See that it builds""" + archive = os.path.join(output_dir, "2012", "03", "30", "index.html") + assert os.path.isfile(archive) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + patch_config( + target_dir, ("# CREATE_DAILY_ARCHIVE = False", "CREATE_DAILY_ARCHIVE = True") + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_archive_per_month.py b/tests/integration/test_archive_per_month.py new file mode 100644 index 0000000..5f6c94b --- /dev/null +++ b/tests/integration/test_archive_per_month.py @@ -0,0 +1,36 @@ +"""Check that the monthly archives build and are correct.""" + +import os + +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_monthly_archive(build, output_dir): + """Check that the monthly archive is build.""" + assert os.path.isfile(os.path.join(output_dir, "2012", "03", "index.html")) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + patch_config( + target_dir, + ("# CREATE_MONTHLY_ARCHIVE = False", "CREATE_MONTHLY_ARCHIVE = True"), + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_building_in_subdir.py b/tests/integration/test_building_in_subdir.py new file mode 100644 index 0000000..37b2aae --- /dev/null +++ b/tests/integration/test_building_in_subdir.py @@ -0,0 +1,32 @@ +""" +Check that running nikola from subdir works. + +Check whether build works from posts/ +""" + +import os + +import pytest + +from nikola import __main__ + +from .helper import cd +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + build_dir = os.path.join(target_dir, "posts") + + with cd(build_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_category_destpath.py b/tests/integration/test_category_destpath.py new file mode 100644 index 0000000..9defcd3 --- /dev/null +++ b/tests/integration/test_category_destpath.py @@ -0,0 +1,88 @@ +"""Test if category destpath indexes avoid pages.""" + +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ +from nikola.utils import makedirs + +from .helper import append_config, cd, create_simple_post +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_destpath_with_avoidance(build, output_dir): + """Test destpath categories page generation and avoidance.""" + + def _make_output_path(dir, name): + """Make a file path to the output.""" + return os.path.join(dir, name + ".html") + + cat1 = os.path.join(output_dir, "posts", "cat1") + cat2 = os.path.join(output_dir, "posts", "cat2") + + index1 = _make_output_path(cat1, "index") + index2 = _make_output_path(cat2, "index") + + # Do all files exist? + assert os.path.isfile(index1) + assert os.path.isfile(index2) + + # Are their contents correct? + with open(index1, "r", encoding="utf-8") as fh: + page = fh.read() + + assert "Posts about cat1" in page + assert "test-destpath-p1" in page + assert "test-destpath-p2" in page + assert "test-destpath-p3" not in page + + with open(index2, "r", encoding="utf-8") as fh: + page = fh.read() + + assert "Posts about cat2" not in page + assert "This is a post that conflicts with cat2." in page + + +@pytest.fixture(scope="module") +def build(target_dir): + """ + Add subdirectories and create a post in category "cat1" and a page + with the same URL as the category index (created via destpaths). + """ + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + posts = os.path.join(target_dir, "posts") + cat1 = os.path.join(posts, "cat1") + cat2 = os.path.join(posts, "cat2") + + makedirs(cat1) + makedirs(cat2) + + create_simple_post(cat1, "p1.txt", "test-destpath-p1", "This is a post in cat1.") + create_simple_post(cat1, "p2.txt", "test-destpath-p2", "This is a post in cat1.") + create_simple_post(cat2, "p3.txt", "test-destpath-p3", "This is a post in cat2.") + create_simple_post(posts, "cat2.txt", "cat2", "This is a post that conflicts with cat2.") + + append_config( + target_dir, + """ +PRETTY_URLS = True +CATEGORY_ALLOW_HIERARCHIES = True +CATEGORY_DESTPATH_AS_DEFAULT = True +CATEGORY_DESTPATH_TRIM_PREFIX = True +CATEGORY_PAGES_FOLLOW_DESTPATH = True +""", + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_check_absolute_subfolder.py b/tests/integration/test_check_absolute_subfolder.py new file mode 100644 index 0000000..86af065 --- /dev/null +++ b/tests/integration/test_check_absolute_subfolder.py @@ -0,0 +1,50 @@ +""" +Validate links in a site which is: + +* built in URL_TYPE="absolute" +* deployable to a subfolder (BASE_URL="https://example.com/foo/") +""" + +import io +import os + +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, +) + + +def test_index_in_sitemap(build, output_dir): + """ + Test that the correct path is in sitemap, and not the wrong one. + + The correct path ends in /foo/ because this is where we deploy to. + """ + sitemap_path = os.path.join(output_dir, "sitemap.xml") + with io.open(sitemap_path, "r", encoding="utf8") as inf: + sitemap_data = inf.read() + + assert "<loc>https://example.com/foo/</loc>" in sitemap_data + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + patch_config( + target_dir, + ('SITE_URL = "https://example.com/"', 'SITE_URL = "https://example.com/foo/"'), + ("# URL_TYPE = 'rel_path'", "URL_TYPE = 'absolute'"), + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_check_failure.py b/tests/integration/test_check_failure.py new file mode 100644 index 0000000..08e9447 --- /dev/null +++ b/tests/integration/test_check_failure.py @@ -0,0 +1,47 @@ +""" +The demo build should pass 'nikola check' and fail with missing files. + +This tests the red path (failures) for the `check` command. +Green path tests (working as expected) can be found in `test_demo_build`. +""" + +import io +import os + +import pytest + +from nikola import __main__ + +from .helper import cd +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_avoid_double_slash_in_rss, + test_index_in_sitemap, +) + + +def test_check_links_fail(build, output_dir, target_dir): + os.unlink(os.path.join(output_dir, "archive.html")) + + with cd(target_dir): + result = __main__.main(["check", "-l"]) + assert result != 0 + + +def test_check_files_fail(build, output_dir, target_dir): + manually_added_file = os.path.join(output_dir, "foobar") + with io.open(manually_added_file, "w+", encoding="utf8") as outf: + outf.write("foo") + + with cd(target_dir): + result = __main__.main(["check", "-f"]) + assert result != 0 + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_check_full_path_subfolder.py b/tests/integration/test_check_full_path_subfolder.py new file mode 100644 index 0000000..e678016 --- /dev/null +++ b/tests/integration/test_check_full_path_subfolder.py @@ -0,0 +1,35 @@ +""" +Validate links in a site which is: + +* built in URL_TYPE="full_path" +* deployable to a subfolder (BASE_URL="https://example.com/foo/") +""" + +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_check_absolute_subfolder import test_index_in_sitemap # NOQA +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, +) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + patch_config( + target_dir, + ('SITE_URL = "https://example.com/"', 'SITE_URL = "https://example.com/foo/"'), + ("# URL_TYPE = 'rel_path'", "URL_TYPE = 'full_path'"), + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_demo_build.py b/tests/integration/test_demo_build.py new file mode 100644 index 0000000..57a1807 --- /dev/null +++ b/tests/integration/test_demo_build.py @@ -0,0 +1,43 @@ +""" +Test that a default build of a new site based on the demo site works. + +This module also is one place where green path tests (working as +expected) for the `check` command are tested. +In this case these are tested against the demo site with default +settings. +""" + +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import add_post_without_text, cd, copy_example_post +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + with cd(target_dir): + __main__.main(["build"]) + + +def prepare_demo_site(target_dir): + init_command = nikola.plugins.command.init.CommandInit() + init_command.copy_sample_site(target_dir) + init_command.create_configuration(target_dir) + + posts_dir = os.path.join(target_dir, "posts") + copy_example_post(posts_dir) + add_post_without_text(posts_dir) diff --git a/tests/integration/test_empty_build.py b/tests/integration/test_empty_build.py new file mode 100644 index 0000000..a21bf73 --- /dev/null +++ b/tests/integration/test_empty_build.py @@ -0,0 +1,54 @@ +"""Performaning the build of an empty site.""" + +import io +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import cd + + +def test_check_links(build, target_dir): + with cd(target_dir): + assert __main__.main(["check", "-l"]) is None + + +def test_check_files(build, target_dir): + with cd(target_dir): + assert __main__.main(["check", "-f"]) is None + + +def test_index_in_sitemap(build, output_dir): + sitemap_path = os.path.join(output_dir, "sitemap.xml") + with io.open(sitemap_path, "r", encoding="utf8") as inf: + sitemap_data = inf.read() + + assert "<loc>https://example.com/</loc>" in sitemap_data + + +def test_avoid_double_slash_in_rss(build, output_dir): + rss_path = os.path.join(output_dir, "rss.xml") + with io.open(rss_path, "r", encoding="utf8") as inf: + rss_data = inf.read() + + assert "https://example.com//" not in rss_data + + +def test_archive_exists(build, output_dir): + """Ensure the build did something.""" + index_path = os.path.join(output_dir, "archive.html") + assert os.path.isfile(index_path) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_future_post.py b/tests/integration/test_future_post.py new file mode 100644 index 0000000..4645464 --- /dev/null +++ b/tests/integration/test_future_post.py @@ -0,0 +1,109 @@ +"""Test a site with future posts.""" + +import io +import os +from datetime import timedelta + +import pytest + +import nikola +import nikola.plugins.command.init +from nikola.utils import current_time +from nikola import __main__ + +from .helper import append_config, cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_future_post_deployment(build, output_dir, target_dir): + """ Ensure that the future post is deleted upon deploying. """ + index_path = os.path.join(output_dir, "index.html") + post_in_past = os.path.join(output_dir, "posts", "foo", "index.html") + post_in_future = os.path.join(output_dir, "posts", "bar", "index.html") + + assert os.path.isfile(index_path) + assert os.path.isfile(post_in_past) + assert os.path.isfile(post_in_future) + + # Run deploy command to see if future post is deleted + with cd(target_dir): + __main__.main(["deploy"]) + + assert os.path.isfile(index_path) + assert os.path.isfile(post_in_past) + assert not os.path.isfile(post_in_future) + + +@pytest.mark.parametrize("filename", ["index.html", "sitemap.xml"]) +def test_future_post_not_in_indexes(build, output_dir, filename): + """ Ensure that the future post is not present in the index and sitemap.""" + filepath = os.path.join(output_dir, filename) + assert os.path.isfile(filepath) + + with io.open(filepath, "r", encoding="utf8") as inf: + content = inf.read() + assert "foo/" in content + assert "bar/" not in content + assert "baz" not in content + + +@pytest.fixture(scope="module") +def build(target_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + # Change COMMENT_SYSTEM_ID to not wait for 5 seconds + append_config(target_dir, '\nCOMMENT_SYSTEM_ID = "nikolatest"\n') + + def format_datetime(datetime): + return datetime.strftime("%Y-%m-%d %H:%M:%S") + + past_datetime = format_datetime(current_time() + timedelta(days=-1)) + with io.open( + os.path.join(target_dir, "posts", "empty1.txt"), "w+", encoding="utf8" + ) as past_post: + past_post.write( + """\ +.. title: foo +.. slug: foo +.. date: %s +""" + % past_datetime + ) + + future_datetime = format_datetime(current_time() + timedelta(days=1)) + with io.open( + os.path.join(target_dir, "posts", "empty2.txt"), "w+", encoding="utf8" + ) as future_post: + future_post.write( + """\ +.. title: bar +.. slug: bar +.. date: %s +""" + % future_datetime + ) + + with io.open( + os.path.join(target_dir, "posts", "empty3.txt"), "w+", encoding="utf8" + ) as future_post: + future_post.write( + """\ +.. title: baz +.. slug: baz +.. date: %s +.. pretty_url: false +""" + % future_datetime + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_page_index_normal_urls.py b/tests/integration/test_page_index_normal_urls.py new file mode 100644 index 0000000..4dbedfd --- /dev/null +++ b/tests/integration/test_page_index_normal_urls.py @@ -0,0 +1,238 @@ +"""Test if PAGE_INDEX works, with different PRETTY_URLS=False settings.""" + +import io +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ +from nikola.utils import makedirs + +from .helper import append_config, cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def get_last_folder_as_id(value): + """Use the last part of the directories as test identifier.""" + if isinstance(value, (tuple,)): + return value[-1] + + return value + + +@pytest.mark.parametrize( + "dirs, expected_file", + [ + (("pages",), "page0"), + (("pages", "subdir1"), "page1"), + (("pages", "subdir1"), "page2"), + (("pages", "subdir2"), "page3"), + (("pages", "subdir3"), "page4"), + ], + ids=get_last_folder_as_id, +) +def test_page_index(build, output_dir, dirs, expected_file, output_path_func): + """Test PAGE_INDEX - Do all files exist?""" + path_func = output_path_func + + checkdir = os.path.join(output_dir, *dirs) + + assert os.path.isfile(path_func(checkdir, expected_file)) + + +@pytest.mark.parametrize( + "dirs, expected_index_file", + [ + (("pages",), "index.html"), + (("pages", "subdir1"), "index.html"), + (("pages", "subdir2"), "index.html"), + (("pages", "subdir3"), "index.php"), + ], + ids=get_last_folder_as_id, +) +def test_page_index_in_subdir(build, output_dir, dirs, expected_index_file): + """Test PAGE_INDEX - Do index files in subdir exist?""" + checkdir = os.path.join(output_dir, *dirs) + + assert os.path.isfile(os.path.join(checkdir, expected_index_file)) + if expected_index_file == "index.php": + assert not os.path.isfile(os.path.join(checkdir, "index.html")) + + +@pytest.fixture(scope="module") +def output_path_func(): + def output_path(dir, name): + """Make a file path to the output.""" + return os.path.join(dir, name + ".html") + + return output_path + + +def test_page_index_content_in_pages(build, output_dir): + """Do the indexes only contain the pages the should?""" + pages = os.path.join(output_dir, "pages") + + with io.open(os.path.join(pages, "index.html"), "r", encoding="utf-8") as fh: + pages_index = fh.read() + + assert "Page 0" in pages_index + assert "Page 1" not in pages_index + assert "Page 2" not in pages_index + assert "Page 3" not in pages_index + assert "Page 4" not in pages_index + assert "This is not the page index" not in pages_index + + +def test_page_index_content_in_subdir1(build, output_dir): + """Do the indexes only contain the pages the should?""" + subdir1 = os.path.join(output_dir, "pages", "subdir1") + + with io.open(os.path.join(subdir1, "index.html"), "r", encoding="utf-8") as fh: + subdir1_index = fh.read() + + assert "Page 0" not in subdir1_index + assert "Page 1" in subdir1_index + assert "Page 2" in subdir1_index + assert "Page 3" not in subdir1_index + assert "Page 4" not in subdir1_index + assert "This is not the page index" not in subdir1_index + + +def test_page_index_content_in_subdir2(build, output_dir): + """Do the indexes only contain the pages the should?""" + subdir2 = os.path.join(output_dir, "pages", "subdir2") + + with io.open(os.path.join(subdir2, "index.html"), "r", encoding="utf-8") as fh: + subdir2_index = fh.read() + + assert "Page 0" not in subdir2_index + assert "Page 1" not in subdir2_index + assert "Page 2" not in subdir2_index + assert "Page 3" not in subdir2_index + assert "Page 4" not in subdir2_index + assert "This is not the page index." in subdir2_index + + +def test_page_index_content_in_subdir3(build, output_dir): + """Do the indexes only contain the pages the should?""" + subdir3 = os.path.join(output_dir, "pages", "subdir3") + + with io.open(os.path.join(subdir3, "index.php"), "r", encoding="utf-8") as fh: + subdir3_index = fh.read() + + assert "Page 0" not in subdir3_index + assert "Page 1" not in subdir3_index + assert "Page 2" not in subdir3_index + assert "Page 3" not in subdir3_index + assert "Page 4" not in subdir3_index + assert "This is not the page index either." in subdir3_index + + +@pytest.fixture(scope="module") +def build(target_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + create_pages(target_dir) + + append_config( + target_dir, + """ +PAGE_INDEX = True +PRETTY_URLS = False +PAGES = PAGES + (('pages/*.php', 'pages', 'page.tmpl'),) +""", + ) + + with cd(target_dir): + __main__.main(["build"]) + + +def create_pages(target_dir): + pages = os.path.join(target_dir, "pages") + subdir1 = os.path.join(target_dir, "pages", "subdir1") + subdir2 = os.path.join(target_dir, "pages", "subdir2") + subdir3 = os.path.join(target_dir, "pages", "subdir3") + + makedirs(subdir1) + makedirs(subdir2) + makedirs(subdir3) + + with io.open(os.path.join(pages, "page0.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Page 0 +.. slug: page0 + +This is page 0. +""" + ) + + with io.open(os.path.join(subdir1, "page1.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Page 1 +.. slug: page1 + +This is page 1. +""" + ) + + with io.open(os.path.join(subdir1, "page2.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Page 2 +.. slug: page2 + +This is page 2. +""" + ) + + with io.open(os.path.join(subdir2, "page3.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Page 3 +.. slug: page3 + +This is page 3. +""" + ) + + with io.open(os.path.join(subdir2, "foo.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Not the page index +.. slug: index + +This is not the page index. +""" + ) + + with io.open(os.path.join(subdir3, "page4.txt"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Page 4 +.. slug: page4 + +This is page 4. +""" + ) + + with io.open(os.path.join(subdir3, "bar.php"), "w+", encoding="utf8") as outf: + outf.write( + """\ +.. title: Still not the page index +.. slug: index + +This is not the page index either. +""" + ) diff --git a/tests/integration/test_page_index_pretty_urls.py b/tests/integration/test_page_index_pretty_urls.py new file mode 100644 index 0000000..b31680f --- /dev/null +++ b/tests/integration/test_page_index_pretty_urls.py @@ -0,0 +1,57 @@ +"""Test if PAGE_INDEX works, with different PRETTY_URLS=True.""" + +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import append_config, cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) +from .test_page_index_normal_urls import create_pages +from .test_page_index_normal_urls import ( # NOQA + test_page_index, + test_page_index_in_subdir, + test_page_index_content_in_pages, + test_page_index_content_in_subdir1, + test_page_index_content_in_subdir2, + test_page_index_content_in_subdir3, +) + + +@pytest.fixture(scope="module") +def output_path_func(): + def output_path(dir, name): + """Make a file path to the output.""" + return os.path.join(dir, name + "/index.html") + + return output_path + + +@pytest.fixture(scope="module") +def build(target_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + create_pages(target_dir) + + append_config( + target_dir, + """ +PAGE_INDEX = True +PRETTY_URLS = True +PAGES = PAGES + (('pages/*.php', 'pages', 'page.tmpl'),) +""", + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_redirection.py b/tests/integration/test_redirection.py new file mode 100644 index 0000000..323740d --- /dev/null +++ b/tests/integration/test_redirection.py @@ -0,0 +1,106 @@ +""" +Check REDIRECTIONS. + +This module tests absolute, external and relative redirects. +Each of the different redirect types is specified in the config and +then tested by at least one test.""" + +import io +import os + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import append_config, cd +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_absolute_redirection(build, output_dir): + abs_source = os.path.join(output_dir, "redirects", "absolute_source.html") + assert os.path.exists(abs_source) + + abs_destination = os.path.join(output_dir, "posts", "absolute.html") + assert os.path.exists(abs_destination) + + with open(abs_destination) as abs_destination_fd: + abs_destination_content = abs_destination_fd.read() + + redirect_tag = '<meta http-equiv="refresh" content="0; url=/redirects/absolute_source.html">' + assert redirect_tag in abs_destination_content + + with open(abs_source) as abs_source_fd: + absolute_source_content = abs_source_fd.read() + + assert absolute_source_content == "absolute" + + +def test_external_redirection(build, output_dir): + ext_link = os.path.join(output_dir, "external.html") + + assert os.path.exists(ext_link) + with open(ext_link) as ext_link_fd: + ext_link_content = ext_link_fd.read() + + redirect_tag = '<meta http-equiv="refresh" content="0; url=http://www.example.com/">' + assert redirect_tag in ext_link_content + + +def test_relative_redirection(build, output_dir): + rel_destination = os.path.join(output_dir, "relative.html") + assert os.path.exists(rel_destination) + rel_source = os.path.join(output_dir, "redirects", "rel_src.html") + assert os.path.exists(rel_source) + + with open(rel_destination) as rel_destination_fd: + rel_destination_content = rel_destination_fd.read() + + redirect_tag = '<meta http-equiv="refresh" content="0; url=redirects/rel_src.html">' + assert redirect_tag in rel_destination_content + + with open(rel_source) as rel_source_fd: + rel_source_content = rel_source_fd.read() + + assert rel_source_content == "relative" + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + redirects_dir = os.path.join(target_dir, "files", "redirects") + nikola.utils.makedirs(redirects_dir) + + # Source file for absolute redirect + target_path = os.path.join(redirects_dir, "absolute_source.html") + with io.open(target_path, "w+", encoding="utf8") as outf: + outf.write("absolute") + + # Source file for relative redirect + target_path = os.path.join(redirects_dir, "rel_src.html") + with io.open(target_path, "w+", encoding="utf8") as outf: + outf.write("relative") + + # Configure usage of specific redirects + append_config( + target_dir, + """ +REDIRECTIONS = [ + ("posts/absolute.html", "/redirects/absolute_source.html"), + ("external.html", "http://www.example.com/"), + ("relative.html", "redirects/rel_src.html"), +] +""", + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_relative_links.py b/tests/integration/test_relative_links.py new file mode 100644 index 0000000..3b158cf --- /dev/null +++ b/tests/integration/test_relative_links.py @@ -0,0 +1,60 @@ +"""Check that SITE_URL with a path doesn't break links.""" + +import io +import os + +import lxml +import pytest + +from nikola import __main__ + +from .helper import cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, +) + + +def test_relative_links(build, output_dir): + """Check that the links in output/index.html are correct""" + test_path = os.path.join(output_dir, "index.html") + + with io.open(test_path, "rb") as inf: + data = inf.read() + + assert not any( + url.startswith("..") + for _, _, url, _ in lxml.html.iterlinks(data) + if url.endswith("css") + ) + + +def test_index_in_sitemap(build, output_dir): + """Test that the correct path is in sitemap, and not the wrong one.""" + sitemap_path = os.path.join(output_dir, "sitemap.xml") + with io.open(sitemap_path, "r", encoding="utf8") as inf: + sitemap_data = inf.read() + + assert "<loc>https://example.com/</loc>" not in sitemap_data + assert "<loc>https://example.com/foo/bar/</loc>" in sitemap_data + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + # Set the SITE_URL to have a path with subfolder + patch_config( + target_dir, + ( + 'SITE_URL = "https://example.com/"', + 'SITE_URL = "https://example.com/foo/bar/"', + ), + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_relative_links_with_pages_in_root.py b/tests/integration/test_relative_links_with_pages_in_root.py new file mode 100644 index 0000000..16f9d6f --- /dev/null +++ b/tests/integration/test_relative_links_with_pages_in_root.py @@ -0,0 +1,65 @@ +"""Check that dropping pages to the root doesn't break links.""" + +import io +import os + +import lxml +import pytest + +from nikola import __main__ + +from .helper import append_config, cd, patch_config +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, +) + + +def test_relative_links(build, output_dir): + """Check that the links in a page are correct""" + test_path = os.path.join(output_dir, "about-nikola.html") + + with io.open(test_path, "rb") as inf: + data = inf.read() + + assert not any( + url.startswith("..") + for _, _, url, _ in lxml.html.iterlinks(data) + if url.endswith("css") + ) + + +def test_index_in_sitemap(build, output_dir): + """Test that the correct path is in sitemap, and not the wrong one.""" + sitemap_path = os.path.join(output_dir, "sitemap.xml") + with io.open(sitemap_path, "r", encoding="utf8") as inf: + sitemap_data = inf.read() + + assert "<loc>https://example.com/</loc>" not in sitemap_data + assert "<loc>https://example.com/blog/index.html</loc>" in sitemap_data + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + # Configure our pages to reside in the root + patch_config( + target_dir, + ('("pages/*.txt", "pages", "page.tmpl"),', '("pages/*.txt", "", "page.tmpl"),'), + ('("pages/*.rst", "pages", "page.tmpl"),', '("pages/*.rst", "", "page.tmpl"),'), + ('# INDEX_PATH = ""', 'INDEX_PATH = "blog"'), + ) + append_config( + target_dir, + """ +PRETTY_URLS = False +STRIP_INDEXES = False +""", + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_repeated_posts_setting.py b/tests/integration/test_repeated_posts_setting.py new file mode 100644 index 0000000..2a03338 --- /dev/null +++ b/tests/integration/test_repeated_posts_setting.py @@ -0,0 +1,36 @@ +""" +Duplicate POSTS in settings. + +Should not read each post twice, which causes conflicts. +""" + +import pytest + +from nikola import __main__ + +from .helper import append_config, cd +from .test_demo_build import prepare_demo_site +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +@pytest.fixture(scope="module") +def build(target_dir): + """Fill the site with demo content and build it.""" + prepare_demo_site(target_dir) + + append_config( + target_dir, + """ +POSTS = (("posts/*.txt", "posts", "post.tmpl"), + ("posts/*.txt", "posts", "post.tmpl")) +""", + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_translated_content.py b/tests/integration/test_translated_content.py new file mode 100644 index 0000000..9d1338f --- /dev/null +++ b/tests/integration/test_translated_content.py @@ -0,0 +1,62 @@ +""" +Test a site with translated content. + +Do not test titles as we remove the translation. +""" + +import io +import os +import shutil + +import lxml.html +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +def test_translated_titles(build, output_dir, other_locale): + """Check that translated title is picked up.""" + normal_file = os.path.join(output_dir, "pages", "1", "index.html") + translated_file = os.path.join(output_dir, other_locale, "pages", "1", "index.html") + + # Files should be created + assert os.path.isfile(normal_file) + assert os.path.isfile(translated_file) + + # And now let's check the titles + with io.open(normal_file, "r", encoding="utf8") as inf: + doc = lxml.html.parse(inf) + assert doc.find("//title").text == "Foo | Demo Site" + + with io.open(translated_file, "r", encoding="utf8") as inf: + doc = lxml.html.parse(inf) + assert doc.find("//title").text == "Bar | Demo Site" + + +@pytest.fixture(scope="module") +def build(target_dir, test_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + src = os.path.join(test_dir, "..", "data", "translated_titles") + for root, dirs, files in os.walk(src): + for src_name in files: + rel_dir = os.path.relpath(root, src) + dst_file = os.path.join(target_dir, rel_dir, src_name) + src_file = os.path.join(root, src_name) + shutil.copy2(src_file, dst_file) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_translated_content_secondary_language.py b/tests/integration/test_translated_content_secondary_language.py new file mode 100644 index 0000000..f826d1d --- /dev/null +++ b/tests/integration/test_translated_content_secondary_language.py @@ -0,0 +1,40 @@ +"""Make sure posts only in secondary languages work.""" + +import os +import shutil + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) + + +@pytest.fixture(scope="module") +def build(target_dir, test_dir): + """Build the site.""" + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + src = os.path.join(test_dir, "..", "data", "translated_titles") + for root, dirs, files in os.walk(src): + for src_name in files: + if src_name == "1.txt": # English post + continue + + rel_dir = os.path.relpath(root, src) + dst_file = os.path.join(target_dir, rel_dir, src_name) + src_file = os.path.join(root, src_name) + shutil.copy2(src_file, dst_file) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_translation_patterns.py b/tests/integration/test_translation_patterns.py new file mode 100644 index 0000000..6f77960 --- /dev/null +++ b/tests/integration/test_translation_patterns.py @@ -0,0 +1,55 @@ +"""Check that the path.lang.ext TRANSLATIONS_PATTERN works too""" + +import os +import shutil + +import pytest + +import nikola.plugins.command.init +from nikola import __main__ + +from .helper import cd, patch_config +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, + test_check_links, + test_index_in_sitemap, +) +from .test_translated_content import test_translated_titles # NOQA + + +@pytest.fixture(scope="module") +def build(target_dir, test_dir, other_locale): + """ + Build the site. + + Set the TRANSLATIONS_PATTERN to the old v6 default. + """ + init_command = nikola.plugins.command.init.CommandInit() + init_command.create_empty_site(target_dir) + init_command.create_configuration(target_dir) + + src = os.path.join(test_dir, "..", "data", "translated_titles") + for root, dirs, files in os.walk(src): + for src_name in files: + rel_dir = os.path.relpath(root, src) + dst_file = os.path.join(target_dir, rel_dir, src_name) + src_file = os.path.join(root, src_name) + shutil.copy2(src_file, dst_file) + + os.rename( + os.path.join(target_dir, "pages", "1.%s.txt" % other_locale), + os.path.join(target_dir, "pages", "1.txt.%s" % other_locale), + ) + + patch_config( + target_dir, + ( + 'TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}"', + 'TRANSLATIONS_PATTERN = "{path}.{ext}.{lang}"', + ), + ) + + with cd(target_dir): + __main__.main(["build"]) diff --git a/tests/integration/test_wordpress_import.py b/tests/integration/test_wordpress_import.py new file mode 100644 index 0000000..6d3bfb8 --- /dev/null +++ b/tests/integration/test_wordpress_import.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +Testing the wordpress import. + +It will do create a new site with the import_wordpress command +and use that newly created site to make a build. +""" + +import os.path +from glob import glob + +import pytest + +from nikola import __main__ + +from .helper import cd +from .test_empty_build import ( # NOQA + test_archive_exists, + test_avoid_double_slash_in_rss, + test_check_files, +) + + +def test_import_created_files(build, target_dir): + assert os.path.exists(target_dir) + assert os.path.exists(os.path.join(target_dir, "conf.py")) + + +@pytest.mark.parametrize("dirname", ["pages", "posts"]) +def test_filled_directories(build, target_dir, dirname): + folder = os.path.join(target_dir, dirname) + assert os.path.isdir(folder) + content = glob(os.path.join(folder, "**"), recursive=True) + assert any(os.path.isfile(element) for element in content) + + +@pytest.fixture(scope="module") +def build(target_dir, import_file): + __main__.main( + [ + "import_wordpress", + "--no-downloads", + "--output-folder", + target_dir, + import_file, + ] + ) + + with cd(target_dir): + result = __main__.main(["build"]) + assert not result + + +@pytest.fixture(scope="module") +def import_file(test_dir): + """Path to the Wordpress export file.""" + return os.path.join( + test_dir, "..", "data", "wordpress_import", "wordpress_export_example.xml" + ) diff --git a/tests/test_command_import_wordpress.py b/tests/test_command_import_wordpress.py index c05188f..c8bd19f 100644 --- a/tests/test_command_import_wordpress.py +++ b/tests/test_command_import_wordpress.py @@ -1,241 +1,112 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import os +from unittest import mock -import unittest -import mock +import pytest -import nikola import nikola.plugins.command.import_wordpress -from .base import BaseTestCase - - -class BasicCommandImportWordpress(BaseTestCase): - def setUp(self): - self.module = nikola.plugins.command.import_wordpress - self.import_command = self.module.CommandImportWordpress() - self.import_command.onefile = False - self.import_filename = os.path.abspath(os.path.join( - os.path.dirname(__file__), 'wordpress_export_example.xml')) - - def tearDown(self): - del self.import_command - del self.import_filename - - -class TestQTranslateContentSeparation(BasicCommandImportWordpress): - - def test_conserves_qtranslate_less_post(self): - content = """Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. - -Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues !""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual(1, len(content_translations)) - self.assertEqual(content, content_translations[""]) - - def test_split_a_two_language_post(self): - content = """<!--:fr-->Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. - -Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! -<!--:--><!--:en-->If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. - -Comments, questions and suggestions are welcome ! -<!--:-->""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual("""Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. - -Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! -""", content_translations["fr"]) - self.assertEqual("""If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. - -Comments, questions and suggestions are welcome ! -""", content_translations["en"]) - - def test_split_a_two_language_post_with_teaser(self): - content = """<!--:fr-->Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. - -Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! -<!--:--><!--:en-->If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. - -Comments, questions and suggestions are welcome ! -<!--:--><!--more--><!--:fr--> -Plus de détails ici ! -<!--:--><!--:en--> -More details here ! -<!--:-->""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual("""Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. - -Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! - <!--more--> \n\ -Plus de détails ici ! -""", content_translations["fr"]) - self.assertEqual("""If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. - -Comments, questions and suggestions are welcome ! - <!--more--> \n\ -More details here ! -""", content_translations["en"]) - - def test_split_a_two_language_post_with_intermission(self): - content = """<!--:fr-->Voila voila<!--:-->COMMON<!--:en-->BLA<!--:-->""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual("Voila voila COMMON", content_translations["fr"]) - self.assertEqual("COMMON BLA", content_translations["en"]) - - def test_split_a_two_language_post_with_uneven_repartition(self): - content = """<!--:fr-->Voila voila<!--:-->COMMON<!--:fr-->MOUF<!--:--><!--:en-->BLA<!--:-->""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual("Voila voila COMMON MOUF", content_translations["fr"]) - self.assertEqual("COMMON BLA", content_translations["en"]) - - def test_split_a_two_language_post_with_uneven_repartition_bis(self): - content = """<!--:fr-->Voila voila<!--:--><!--:en-->BLA<!--:-->COMMON<!--:fr-->MOUF<!--:-->""" - content_translations = self.module.separate_qtranslate_content(content) - self.assertEqual("Voila voila COMMON MOUF", content_translations["fr"]) - self.assertEqual("BLA COMMON", content_translations["en"]) - - -class CommandImportWordpressRunTest(BasicCommandImportWordpress): - def setUp(self): - super(self.__class__, self).setUp() - self.data_import = mock.MagicMock() - self.site_generation = mock.MagicMock() - self.write_urlmap = mock.MagicMock() - self.write_configuration = mock.MagicMock() - - site_generation_patch = mock.patch('os.system', self.site_generation) - data_import_patch = mock.patch( - 'nikola.plugins.command.import_wordpress.CommandImportWordpress.import_posts', self.data_import) - write_urlmap_patch = mock.patch( - 'nikola.plugins.command.import_wordpress.CommandImportWordpress.write_urlmap_csv', self.write_urlmap) - write_configuration_patch = mock.patch( - 'nikola.plugins.command.import_wordpress.CommandImportWordpress.write_configuration', self.write_configuration) - - self.patches = [site_generation_patch, data_import_patch, - write_urlmap_patch, write_configuration_patch] - for patch in self.patches: - patch.start() - - def tearDown(self): - del self.data_import - del self.site_generation - del self.write_urlmap - del self.write_configuration - - for patch in self.patches: - patch.stop() - del self.patches - - super(self.__class__, self).tearDown() - - def test_create_import(self): - valid_import_arguments = ( - dict(options={'output_folder': 'some_folder'}, - args=[self.import_filename]), - dict(args=[self.import_filename]), - dict(args=[self.import_filename, 'folder_argument']), - ) - for arguments in valid_import_arguments: - self.import_command.execute(**arguments) - - self.assertTrue(self.site_generation.called) - self.assertTrue(self.data_import.called) - self.assertTrue(self.write_urlmap.called) - self.assertTrue(self.write_configuration.called) - self.assertFalse(self.import_command.exclude_drafts) - - def test_ignoring_drafts(self): - valid_import_arguments = ( - dict(options={'exclude_drafts': True}, args=[ - self.import_filename]), - dict( - options={'exclude_drafts': True, - 'output_folder': 'some_folder'}, - args=[self.import_filename]), - ) - for arguments in valid_import_arguments: - self.import_command.execute(**arguments) - self.assertTrue(self.import_command.exclude_drafts) - - -class CommandImportWordpressTest(BasicCommandImportWordpress): - def test_create_import_work_without_argument(self): - # Running this without an argument must not fail. - # It should show the proper usage of the command. - self.import_command.execute() - - def test_populate_context(self): - channel = self.import_command.get_channel_from_file( - self.import_filename) - self.import_command.html2text = False - self.import_command.transform_to_markdown = False - self.import_command.transform_to_html = False - self.import_command.use_wordpress_compiler = False - context = self.import_command.populate_context(channel) - - for required_key in ('POSTS', 'PAGES', 'COMPILERS'): - self.assertTrue(required_key in context) - - self.assertEqual('de', context['DEFAULT_LANG']) - self.assertEqual('Wordpress blog title', context['BLOG_TITLE']) - self.assertEqual('Nikola test blog ;) - with moré Ümläüts', - context['BLOG_DESCRIPTION']) - self.assertEqual('http://some.blog/', context['SITE_URL']) - self.assertEqual('mail@some.blog', context['BLOG_EMAIL']) - self.assertEqual('Niko', context['BLOG_AUTHOR']) - - def test_importing_posts_and_attachments(self): - channel = self.import_command.get_channel_from_file( - self.import_filename) - self.import_command.base_dir = '' - self.import_command.output_folder = 'new_site' - self.import_command.squash_newlines = True - self.import_command.no_downloads = False - self.import_command.export_categories_as_categories = False - self.import_command.export_comments = False - self.import_command.html2text = False - self.import_command.transform_to_markdown = False - self.import_command.transform_to_html = False - self.import_command.use_wordpress_compiler = False - self.import_command.tag_saniziting_strategy = 'first' - self.import_command.context = self.import_command.populate_context( - channel) - - # Ensuring clean results - self.import_command.url_map = {} - self.module.links = {} - - write_metadata = mock.MagicMock() - write_content = mock.MagicMock() - write_post = mock.MagicMock() - write_attachments_info = mock.MagicMock() - download_mock = mock.MagicMock() - - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.write_content', write_content): - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.write_metadata', write_metadata): - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.download_url_content_to_file', download_mock): - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.write_attachments_info', write_attachments_info): - with mock.patch('nikola.plugins.command.import_wordpress.os.makedirs'): - self.import_command.import_posts(channel) - - self.assertTrue(download_mock.called) - qpath = 'new_site/files/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png' - download_mock.assert_any_call( - 'http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png', - qpath.replace('/', os.sep)) - - self.assertTrue(write_metadata.called) - write_metadata.assert_any_call( - 'new_site/pages/kontakt.meta'.replace('/', os.sep), 'Kontakt', - 'kontakt', '2009-07-16 20:20:32', '', [], **{'wp-status': 'publish'}) - - self.assertTrue(write_content.called) - write_content.assert_any_call('new_site/posts/2007/04/hoert.md'.replace('/', os.sep), - """An image. +def test_create_import_work_without_argument(import_command): + """ + Running import command without an argument must not fail. + It should show the proper usage of the command. + """ + import_command.execute() + + +@pytest.mark.parametrize( + "key, expected_value", + [ + ("DEFAULT_LANG", "de"), + ("BLOG_TITLE", "Wordpress blog title"), + ("BLOG_DESCRIPTION", "Nikola test blog ;) - with moré Ümläüts"), + ("SITE_URL", "http://some.blog/"), + ("BLOG_EMAIL", "mail@some.blog"), + ("BLOG_AUTHOR", "Niko"), + ], +) +def test_populate_context(import_command, import_filename, key, expected_value): + channel = import_command.get_channel_from_file(import_filename) + import_command.html2text = False + import_command.transform_to_markdown = False + import_command.transform_to_html = False + import_command.use_wordpress_compiler = False + import_command.translations_pattern = "{path}.{lang}.{ext}" + context = import_command.populate_context(channel) + + for required_key in ("POSTS", "PAGES", "COMPILERS"): + assert required_key in context + + assert expected_value == context[key] + + +def test_importing_posts_and_attachments(module, import_command, import_filename): + channel = import_command.get_channel_from_file(import_filename) + import_command.base_dir = "" + import_command.output_folder = "new_site" + import_command.squash_newlines = True + import_command.no_downloads = False + import_command.export_categories_as_categories = False + import_command.export_comments = False + import_command.html2text = False + import_command.transform_to_markdown = False + import_command.transform_to_html = False + import_command.use_wordpress_compiler = False + import_command.tag_saniziting_strategy = "first" + import_command.separate_qtranslate_content = False + import_command.translations_pattern = "{path}.{lang}.{ext}" + + import_command.context = import_command.populate_context(channel) + + # Ensuring clean results + # assert not import_command.url_map + assert not module.links + import_command.url_map = {} + + write_metadata = mock.MagicMock() + write_content = mock.MagicMock() + write_attachments_info = mock.MagicMock() + download_mock = mock.MagicMock() + + with mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.write_content", + write_content, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.write_metadata", + write_metadata, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.download_url_content_to_file", + download_mock, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.write_attachments_info", + write_attachments_info, + ), mock.patch( + "nikola.plugins.command.import_wordpress.os.makedirs" + ): + import_command.import_posts(channel) + + assert download_mock.called + qpath = "new_site/files/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" + download_mock.assert_any_call( + "http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png", + qpath.replace("/", os.sep), + ) + + assert write_metadata.called + write_metadata.assert_any_call( + "new_site/pages/kontakt.meta".replace("/", os.sep), + "Kontakt", + "kontakt", + "2009-07-16 20:20:32", + "", + [], + **{"wp-status": "publish"} + ) + + assert write_content.called + write_content.assert_any_call( + "new_site/posts/2007/04/hoert.md".replace("/", os.sep), + """An image. <img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" /> @@ -249,26 +120,43 @@ print sys.version ``` The end. -""", True) - - self.assertTrue(write_attachments_info.called) - write_attachments_info.assert_any_call('new_site/posts/2008/07/arzt-und-pfusch-s-i-c-k.attachments.json'.replace('/', os.sep), - {10: {'wordpress_user_name': 'Niko', - 'files_meta': [{'width': 300, 'height': 299}, - {'width': 150, 'size': 'thumbnail', 'height': 150}], - 'excerpt': 'Arzt+Pfusch - S.I.C.K.', - 'date_utc': '2009-07-16 19:40:37', - 'content': 'Das Cover von Arzt+Pfusch - S.I.C.K.', - 'files': ['/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png', - '/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover-150x150.png'], - 'title': 'Arzt+Pfusch - S.I.C.K.'}}) - - write_content.assert_any_call( - 'new_site/posts/2008/07/arzt-und-pfusch-s-i-c-k.md'.replace('/', os.sep), - '''<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz. -Die Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.''', True) - write_content.assert_any_call( - 'new_site/pages/kontakt.md'.replace('/', os.sep), """<h1>Datenschutz</h1> +""", + True, + ) + + assert write_attachments_info.called + write_attachments_info.assert_any_call( + "new_site/posts/2008/07/arzt-und-pfusch-s-i-c-k.attachments.json".replace( + "/", os.sep + ), + { + 10: { + "wordpress_user_name": "Niko", + "files_meta": [ + {"width": 300, "height": 299}, + {"width": 150, "size": "thumbnail", "height": 150}, + ], + "excerpt": "Arzt+Pfusch - S.I.C.K.", + "date_utc": "2009-07-16 19:40:37", + "content": "Das Cover von Arzt+Pfusch - S.I.C.K.", + "files": [ + "/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png", + "/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover-150x150.png", + ], + "title": "Arzt+Pfusch - S.I.C.K.", + } + }, + ) + + write_content.assert_any_call( + "new_site/posts/2008/07/arzt-und-pfusch-s-i-c-k.md".replace("/", os.sep), + """<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz. +Die Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.""", + True, + ) + write_content.assert_any_call( + "new_site/pages/kontakt.md".replace("/", os.sep), + """<h1>Datenschutz</h1> Ich erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind: <ul> <li>Browsertyp und -version</li> @@ -277,114 +165,125 @@ Ich erhebe und speichere automatisch in meine Server Log Files Informationen, di <li>IP Adresse des zugreifenden Rechners</li> <li>Uhrzeit der Serveranfrage.</li> </ul> -Diese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.""", True) - - self.assertTrue(len(self.import_command.url_map) > 0) - - self.assertEqual( - self.import_command.url_map['http://some.blog/2007/04/hoert/'], - 'http://some.blog/posts/2007/04/hoert.html') - self.assertEqual( - self.import_command.url_map[ - 'http://some.blog/2008/07/arzt-und-pfusch-s-i-c-k/'], - 'http://some.blog/posts/2008/07/arzt-und-pfusch-s-i-c-k.html') - self.assertEqual( - self.import_command.url_map['http://some.blog/kontakt/'], - 'http://some.blog/pages/kontakt.html') - - image_thumbnails = [ - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-64x64.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-300x175.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-36x36.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-24x24.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-48x48.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png', - 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-150x150.png' - ] - - for link in image_thumbnails: - self.assertTrue( - link in self.module.links, - 'No link to "{0}" found in {map}.'.format( - link, - map=self.module.links - ) - ) - - def test_transforming_content(self): - """Applying markup conversions to content.""" - transform_code = mock.MagicMock() - transform_caption = mock.MagicMock() - transform_newlines = mock.MagicMock() - - self.import_command.html2text = False - self.import_command.transform_to_markdown = False - self.import_command.transform_to_html = False - self.import_command.use_wordpress_compiler = False - - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_code', transform_code): - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_caption', transform_caption): - with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_multiple_newlines', transform_newlines): - self.import_command.transform_content("random content", "wp", None) - - self.assertTrue(transform_code.called) - self.assertTrue(transform_caption.called) - self.assertTrue(transform_newlines.called) - - def test_transforming_source_code(self): - """ - Tests the handling of sourcecode tags. - """ - content = """Hello World. +Diese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.""", + True, + ) + + assert len(import_command.url_map) > 0 + + assert ( + "http://some.blog/posts/2007/04/hoert.html" == + import_command.url_map["http://some.blog/2007/04/hoert/"] + ) + assert ( + "http://some.blog/posts/2008/07/arzt-und-pfusch-s-i-c-k.html" == + import_command.url_map["http://some.blog/2008/07/arzt-und-pfusch-s-i-c-k/"] + ) + assert ( + "http://some.blog/pages/kontakt.html" == + import_command.url_map["http://some.blog/kontakt/"] + ) + + image_thumbnails = [ + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-64x64.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-300x175.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-36x36.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-24x24.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-48x48.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png", + "http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-150x150.png", + ] + + for link in image_thumbnails: + assert link in module.links + + +def test_transforming_content(import_command): + """Applying markup conversions to content.""" + + import_command.html2text = False + import_command.transform_to_markdown = False + import_command.transform_to_html = False + import_command.use_wordpress_compiler = False + import_command.translations_pattern = "{path}.{lang}.{ext}" + + transform_code = mock.MagicMock() + transform_caption = mock.MagicMock() + transform_newlines = mock.MagicMock() + + with mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_code", + transform_code, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_caption", + transform_caption, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_multiple_newlines", + transform_newlines, + ): + import_command.transform_content("random content", "wp", None) + + assert transform_code.called + assert transform_caption.called + assert transform_newlines.called + + +def test_transforming_source_code(import_command): + """ + Tests the handling of sourcecode tags. + """ + content = """Hello World. [sourcecode language="Python"] import sys print sys.version [/sourcecode]""" - content = self.import_command.transform_code(content) + content = import_command.transform_code(content) - self.assertFalse('[/sourcecode]' in content) - self.assertFalse('[sourcecode language=' in content) + assert "[/sourcecode]" not in content + assert "[sourcecode language=" not in content - replaced_content = """Hello World. + replaced_content = """Hello World. ```Python import sys print sys.version ```""" - self.assertEqual(content, replaced_content) + assert content == replaced_content + - def test_transform_caption(self): - caption = '[caption id="attachment_16" align="alignnone" width="739" caption="beautiful picture"]<img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />[/caption]' - transformed_content = self.import_command.transform_caption(caption) +def test_transform_caption(import_command): + caption = '[caption id="attachment_16" align="alignnone" width="739" caption="beautiful picture"]<img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />[/caption]' + transformed_content = import_command.transform_caption(caption) - expected_content = '<img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />' + expected_content = '<img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />' - self.assertEqual(transformed_content, expected_content) + assert transformed_content == expected_content - def test_transform_multiple_captions_in_a_post(self): - content = """asdasdas + +def test_transform_multiple_captions_in_a_post(import_command): + content = """asdasdas [caption id="attachment_16" align="alignnone" width="739" caption="beautiful picture"]<img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />[/caption] asdasdas asdasdas [caption id="attachment_16" align="alignnone" width="739" caption="beautiful picture"]<img class="size-full wp-image-16" title="pretty" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" />[/caption] asdasdas""" - expected_content = """asdasdas + expected_content = """asdasdas <img class="size-full wp-image-16" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" /> asdasdas asdasdas <img class="size-full wp-image-16" title="pretty" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="beautiful picture" width="739" height="517" /> asdasdas""" - self.assertEqual( - expected_content, self.import_command.transform_caption(content)) + assert expected_content == import_command.transform_caption(content) + - def test_transform_multiple_newlines(self): - content = """This +def test_transform_multiple_newlines(import_command): + content = """This has @@ -397,7 +296,7 @@ newlines. """ - expected_content = """This + expected_content = """This has @@ -406,63 +305,166 @@ way to many newlines. """ - self.import_command.squash_newlines = False - self.assertEqual(content, - self.import_command.transform_multiple_newlines(content)) + import_command.squash_newlines = False + assert content == import_command.transform_multiple_newlines(content) - self.import_command.squash_newlines = True - self.assertEqual(expected_content, - self.import_command.transform_multiple_newlines(content)) + import_command.squash_newlines = True + assert expected_content == import_command.transform_multiple_newlines(content) - def test_transform_caption_with_link_inside(self): - content = """[caption caption="Fehlermeldung"]<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>[/caption]""" - transformed_content = self.import_command.transform_caption(content) - expected_content = """<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>""" - self.assertEqual(expected_content, transformed_content) +def test_transform_caption_with_link_inside(import_command): + content = """[caption caption="Fehlermeldung"]<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>[/caption]""" + transformed_content = import_command.transform_caption(content) - def test_get_configuration_output_path(self): - self.import_command.output_folder = 'new_site' - default_config_path = os.path.join('new_site', 'conf.py') + expected_content = """<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>""" + assert expected_content == transformed_content - self.import_command.import_into_existing_site = False - self.assertEqual(default_config_path, - self.import_command.get_configuration_output_path()) - self.import_command.import_into_existing_site = True - config_path_with_timestamp = self.import_command.get_configuration_output_path( - ) - self.assertNotEqual(default_config_path, config_path_with_timestamp) - self.assertTrue(self.import_command.name in config_path_with_timestamp) +def test_get_configuration_output_path(import_command): + import_command.output_folder = "new_site" + default_config_path = os.path.join("new_site", "conf.py") + + import_command.import_into_existing_site = False + assert default_config_path == import_command.get_configuration_output_path() + + import_command.import_into_existing_site = True + config_path_with_timestamp = import_command.get_configuration_output_path() + + assert default_config_path != config_path_with_timestamp + assert import_command.name in config_path_with_timestamp - def test_write_content_does_not_detroy_text(self): - content = b"""FOO""" - open_mock = mock.mock_open() - with mock.patch('nikola.plugins.basic_import.open', open_mock, create=True): - self.import_command.write_content('some_file', content) - open_mock.assert_has_calls([ - mock.call(u'some_file', u'wb+'), +def test_write_content_does_not_detroy_text(import_command): + content = b"""FOO""" + open_mock = mock.mock_open() + with mock.patch("nikola.plugins.basic_import.open", open_mock, create=True): + import_command.write_content("some_file", content) + + open_mock.assert_has_calls( + [ + mock.call(u"some_file", u"wb+"), mock.call().__enter__(), - mock.call().write(b'<html><body><p>FOO</p></body></html>'), - mock.call().__exit__(None, None, None)] - ) + mock.call().write(b"<html><body><p>FOO</p></body></html>"), + mock.call().__exit__(None, None, None), + ] + ) - def test_configure_redirections(self): - """ - Testing the configuration of the redirections. - We need to make sure that we have valid sources and target links. - """ - url_map = { - '/somewhere/else': 'http://foo.bar/posts/somewhereelse.html' - } +def test_configure_redirections(import_command): + """ + Testing the configuration of the redirections. - redirections = self.import_command.configure_redirections(url_map) + We need to make sure that we have valid sources and target links. + """ + url_map = {"/somewhere/else": "http://foo.bar/posts/somewhereelse.html"} - self.assertEqual(1, len(redirections)) - self.assertTrue(('somewhere/else/index.html', '/posts/somewhereelse.html') in redirections) + redirections = import_command.configure_redirections(url_map) + assert 1 == len(redirections) + assert ("somewhere/else/index.html", "/posts/somewhereelse.html") in redirections -if __name__ == '__main__': - unittest.main() + +@pytest.mark.parametrize( + "options, additional_args", + [ + pytest.param(None, None, id="only import filename"), + ({"output_folder": "some_folder"}, None), + (None, ["folder_argument"]), + ], +) +def test_create_import( + patched_import_command, import_filename, mocks, options, additional_args +): + arguments = {"args": [import_filename]} + if options: + arguments["options"] = options + if additional_args: + arguments["args"].extend(additional_args) + + patched_import_command.execute(**arguments) + + for applied_mock in mocks: + assert applied_mock.called + + assert patched_import_command.exclude_drafts is False + + +@pytest.mark.parametrize( + "options", + [ + {"exclude_drafts": True}, + {"exclude_drafts": True, "output_folder": "some_folder"}, + ], +) +def test_ignoring_drafts_during_import( + patched_import_command, import_filename, options +): + arguments = {"options": options, "args": [import_filename]} + + patched_import_command.execute(**arguments) + assert patched_import_command.exclude_drafts is True + + +@pytest.fixture +def import_command(module): + command = module.CommandImportWordpress() + command.onefile = False + return command + + +@pytest.fixture +def module(): + return nikola.plugins.command.import_wordpress + + +@pytest.fixture +def import_filename(test_dir): + return os.path.abspath( + os.path.join( + test_dir, "data", "wordpress_import", "wordpress_export_example.xml" + ) + ) + + +@pytest.fixture +def patched_import_command(import_command, testsite, mocks): + """ + Import command with disabled site generation and various functions mocked. + """ + data_import, site_generation, write_urlmap, write_configuration = mocks + + import_command.site = testsite + with mock.patch("os.system", site_generation), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.import_posts", + data_import, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.write_urlmap_csv", + write_urlmap, + ), mock.patch( + "nikola.plugins.command.import_wordpress.CommandImportWordpress.write_configuration", + write_configuration, + ): + yield import_command + + +@pytest.fixture +def testsite(): + return FakeSite() + + +class FakeSite: + def link(self, *args, **kwargs): + # We need a link function. + # Stubbed because there is nothing done with the results. + pass + + +@pytest.fixture +def mocks(): + "Mocks to be used in `patched_import_command`" + return [ + mock.MagicMock(name="data_import"), + mock.MagicMock(name="site_generation"), + mock.MagicMock(name="write_urlmap"), + mock.MagicMock(name="write_configuration"), + ] diff --git a/tests/test_command_import_wordpress_translation.py b/tests/test_command_import_wordpress_translation.py new file mode 100644 index 0000000..6882ba5 --- /dev/null +++ b/tests/test_command_import_wordpress_translation.py @@ -0,0 +1,144 @@ +import os + +import pytest + +from nikola.plugins.command.import_wordpress import ( + modernize_qtranslate_tags, + separate_qtranslate_tagged_langs, +) + + +def legacy_qtranslate_separate(text): + """This method helps keeping the legacy tests covering various + corner cases, but plugged on the newer methods.""" + text_bytes = text.encode("utf-8") + modern_bytes = modernize_qtranslate_tags(text_bytes) + modern_text = modern_bytes.decode("utf-8") + return separate_qtranslate_tagged_langs(modern_text) + + +@pytest.mark.parametrize( + "content, french_translation, english_translation", + [ + pytest.param("[:fr]Voila voila[:en]BLA[:]", "Voila voila", "BLA", id="simple"), + pytest.param( + "[:fr]Voila voila[:]COMMON[:en]BLA[:]", + "Voila voila COMMON", + "COMMON BLA", + id="pre modern with intermission", + ), + pytest.param( + "<!--:fr-->Voila voila<!--:-->COMMON<!--:en-->BLA<!--:-->", + "Voila voila COMMON", + "COMMON BLA", + id="withintermission", + ), + pytest.param( + "<!--:fr-->Voila voila<!--:-->COMMON<!--:fr-->MOUF<!--:--><!--:en-->BLA<!--:-->", + "Voila voila COMMON MOUF", + "COMMON BLA", + id="with uneven repartition", + ), + pytest.param( + "<!--:fr-->Voila voila<!--:--><!--:en-->BLA<!--:-->COMMON<!--:fr-->MOUF<!--:-->", + "Voila voila COMMON MOUF", + "BLA COMMON", + id="with uneven repartition bis", + ), + ], +) +def test_legacy_split_a_two_language_post( + content, french_translation, english_translation +): + content_translations = legacy_qtranslate_separate(content) + assert french_translation == content_translations["fr"] + assert english_translation == content_translations["en"] + + +def test_conserves_qtranslate_less_post(): + content = """Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. + +Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues !""" + content_translations = legacy_qtranslate_separate(content) + assert 1 == len(content_translations) + assert content == content_translations[""] + + +def test_modernize_a_wordpress_export_xml_chunk(test_dir): + raw_export_path = os.path.join( + test_dir, "data", "wordpress_import", "wordpress_qtranslate_item_raw_export.xml" + ) + with open(raw_export_path, "rb") as raw_xml_chunk_file: + content = raw_xml_chunk_file.read() + + output = modernize_qtranslate_tags(content) + + modernized_xml_path = os.path.join( + test_dir, "data", "wordpress_import", "wordpress_qtranslate_item_modernized.xml" + ) + with open(modernized_xml_path, "rb") as modernized_chunk_file: + expected = modernized_chunk_file.read() + + assert expected == output + + +def test_modernize_qtranslate_tags(): + content = b"<!--:fr-->Voila voila<!--:-->COMMON<!--:fr-->MOUF<!--:--><!--:en-->BLA<!--:-->" + output = modernize_qtranslate_tags(content) + assert b"[:fr]Voila voila[:]COMMON[:fr]MOUF[:][:en]BLA[:]" == output + + +def test_split_a_two_language_post(): + content = """<!--:fr-->Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. + +Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! +<!--:--><!--:en-->If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. + +Comments, questions and suggestions are welcome ! +<!--:-->""" + content_translations = legacy_qtranslate_separate(content) + + assert ( + content_translations["fr"] == """Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. + +Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! +""" + ) + + assert ( + content_translations["en"] == """If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. + +Comments, questions and suggestions are welcome ! +""" + ) + + +def test_split_a_two_language_post_with_teaser(): + content = """<!--:fr-->Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. + +Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! +<!--:--><!--:en-->If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. + +Comments, questions and suggestions are welcome ! +<!--:--><!--more--><!--:fr--> +Plus de détails ici ! +<!--:--><!--:en--> +More details here ! +<!--:-->""" + content_translations = legacy_qtranslate_separate(content) + assert ( + content_translations["fr"] == """Si vous préférez savoir à qui vous parlez commencez par visiter l'<a title="À propos" href="http://some.blog/about/">À propos</a>. + +Quoiqu'il en soit, commentaires, questions et suggestions sont les bienvenues ! + <!--more--> \n\ +Plus de détails ici ! +""" + ) + assert ( + content_translations["en"] == """If you'd like to know who you're talking to, please visit the <a title="À propos" href="http://some.blog/about/">about page</a>. + +Comments, questions and suggestions are welcome ! + <!--more--> \n\ +More details here ! +""" + ) diff --git a/tests/test_command_init.py b/tests/test_command_init.py index 9270497..96fa78e 100644 --- a/tests/test_command_init.py +++ b/tests/test_command_init.py @@ -1,104 +1,134 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - -import os -import sys - -import unittest -import mock - -import nikola -from nikola.plugins.command.init import SAMPLE_CONF -from nikola.plugins.command.init import format_default_translations_config - - -class CommandInitCallTest(unittest.TestCase): - def setUp(self): - self.ask_questions = mock.MagicMock() - self.copy_sample_site = mock.MagicMock() - self.create_configuration = mock.MagicMock() - self.create_empty_site = mock.MagicMock() - ask_questions_patch = mock.patch( - 'nikola.plugins.command.init.CommandInit.ask_questions', self.ask_questions) - copy_sample_site_patch = mock.patch( - 'nikola.plugins.command.init.CommandInit.copy_sample_site', self.copy_sample_site) - create_configuration_patch = mock.patch( - 'nikola.plugins.command.init.CommandInit.create_configuration', self.create_configuration) - create_empty_site_patch = mock.patch( - 'nikola.plugins.command.init.CommandInit.create_empty_site', self.create_empty_site) - - self.patches = [ask_questions_patch, copy_sample_site_patch, - create_configuration_patch, create_empty_site_patch] - for patch in self.patches: - patch.start() - - self.init_command = nikola.plugins.command.init.CommandInit() - - def tearDown(self): - for patch in self.patches: - patch.stop() - del self.patches - - del self.copy_sample_site - del self.create_configuration - del self.create_empty_site - - def test_init_default(self): - self.init_command.execute() - - self.assertTrue(self.ask_questions.called) - self.assertTrue(self.create_configuration.called) - self.assertFalse(self.copy_sample_site.called) - self.assertTrue(self.create_empty_site.called) - - def test_init_args(self): - arguments = dict(options={'demo': True, 'quiet': True}, args=['destination']) - self.init_command.execute(**arguments) - - self.assertFalse(self.ask_questions.called) - self.assertTrue(self.create_configuration.called) - self.assertTrue(self.copy_sample_site.called) - self.assertFalse(self.create_empty_site.called) - - def test_init_called_without_target_quiet(self): - self.init_command.execute(**dict(options={'quiet': True})) - - self.assertFalse(self.ask_questions.called) - self.assertFalse(self.create_configuration.called) - self.assertFalse(self.copy_sample_site.called) - self.assertFalse(self.create_empty_site.called) - - def test_init_empty_dir(self): - self.init_command.execute(args=['destination']) - - self.assertTrue(self.ask_questions.called) - self.assertTrue(self.create_configuration.called) - self.assertFalse(self.copy_sample_site.called) - self.assertTrue(self.create_empty_site.called) - - -class InitHelperTests(unittest.TestCase): - """Test helper functions provided with the init command.""" - - def test_configure_translations_without_additional_languages(self): - """ - Testing the configuration of the translation when no additional language has been found. - """ - translations_cfg = format_default_translations_config(set()) - self.assertEqual(SAMPLE_CONF["TRANSLATIONS"], translations_cfg) - - def test_configure_translations_with_2_additional_languages(self): - """ - Testing the configuration of the translation when no additional language has been found. - """ - translations_cfg = format_default_translations_config( - set(["es", "en"])) - self.assertEqual("""{ +from unittest import mock + +import pytest + +from nikola.plugins.command.init import ( + SAMPLE_CONF, + CommandInit, + format_default_translations_config, +) + +from .helper import cd + + +def test_command_init_with_defaults( + init_command, + ask_questions, + copy_sample_site, + create_configuration, + create_empty_site, +): + init_command.execute() + + assert ask_questions.called + assert create_configuration.called + assert not copy_sample_site.called + assert create_empty_site.called + + +def test_command_init_with_arguments( + init_command, + ask_questions, + copy_sample_site, + create_configuration, + create_empty_site, +): + arguments = dict(options={"demo": True, "quiet": True}, args=["destination"]) + init_command.execute(**arguments) + + assert not ask_questions.called + assert create_configuration.called + assert copy_sample_site.called + assert not create_empty_site.called + + +def test_init_called_without_target_quiet( + init_command, + ask_questions, + copy_sample_site, + create_configuration, + create_empty_site, +): + init_command.execute(**{"options": {"quiet": True}}) + + assert not ask_questions.called + assert not create_configuration.called + assert not copy_sample_site.called + assert not create_empty_site.called + + +def test_command_init_with_empty_dir( + init_command, + ask_questions, + copy_sample_site, + create_configuration, + create_empty_site, +): + init_command.execute(args=["destination"]) + + assert ask_questions.called + assert create_configuration.called + assert not copy_sample_site.called + assert create_empty_site.called + + +def test_configure_translations_without_additional_languages(): + """ + Testing the configuration of the translation when no additional language has been found. + """ + translations_cfg = format_default_translations_config(set()) + assert SAMPLE_CONF["TRANSLATIONS"] == translations_cfg + + +def test_configure_translations_with_2_additional_languages(): + """ + Testing the configuration of the translation when two additional languages are given. + """ + translations_cfg = format_default_translations_config(set(["es", "en"])) + assert translations_cfg == """{ DEFAULT_LANG: "", "en": "./en", "es": "./es", -}""", translations_cfg) +}""" -if __name__ == '__main__': - unittest.main() +@pytest.fixture +def init_command( + tmpdir, ask_questions, copy_sample_site, create_configuration, create_empty_site +): + with mock.patch( + "nikola.plugins.command.init.CommandInit.ask_questions", ask_questions + ): + with mock.patch( + "nikola.plugins.command.init.CommandInit.copy_sample_site", copy_sample_site + ): + with mock.patch( + "nikola.plugins.command.init.CommandInit.create_configuration", + create_configuration, + ): + with mock.patch( + "nikola.plugins.command.init.CommandInit.create_empty_site", + create_empty_site, + ): + with cd(str(tmpdir)): + yield CommandInit() + + +@pytest.fixture +def ask_questions(): + return mock.MagicMock() + + +@pytest.fixture +def copy_sample_site(): + return mock.MagicMock() + + +@pytest.fixture +def create_configuration(): + return mock.MagicMock() + + +@pytest.fixture +def create_empty_site(): + return mock.MagicMock() diff --git a/tests/test_commands.py b/tests/test_commands.py deleted file mode 100644 index d70d164..0000000 --- a/tests/test_commands.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - -import os -import sys - -import unittest - -from nikola.plugins.command.version import CommandVersion - - -class CommandVersionCallTest(unittest.TestCase): - def test_version(self): - """Test `nikola version`.""" - CommandVersion().execute() diff --git a/tests/test_compile_markdown.py b/tests/test_compile_markdown.py index 99f44fd..88ac290 100644 --- a/tests/test_compile_markdown.py +++ b/tests/test_compile_markdown.py @@ -1,72 +1,78 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import os -import sys - - import io -import shutil -import tempfile -import unittest from os import path -from nikola.plugins.compile.markdown import CompileMarkdown -from .base import BaseTestCase, FakeSite +import pytest +from nikola.plugins.compile.markdown import CompileMarkdown -class CompileMarkdownTests(BaseTestCase): - def setUp(self): - self.tmp_dir = tempfile.mkdtemp() - self.input_path = path.join(self.tmp_dir, 'input.markdown') - self.output_path = path.join(self.tmp_dir, 'output.html') +from .helper import FakeSite + + +@pytest.mark.parametrize( + "input_str, expected_output", + [ + pytest.param("", "", id="empty"), + pytest.param( + "[podcast]https://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]", + '<p><audio controls=""><source src="https://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3" type="audio/mpeg"></source></audio></p>', + id="mdx podcast", + ), + pytest.param( + "~~striked out text~~", + "<p><del>striked out text</del></p>", + id="strikethrough", + ), + pytest.param( + """\ + #!python + from this +""", + """\ +<table class="codehilitetable"><tr><td class="linenos">\ +<div class="linenodiv"><pre>1</pre></div>\ +</td><td class="code"><pre class="code literal-block"><span></span>\ +<code><span class="kn">from</span> <span class="nn">this</span> +</code></pre> +</td></tr></table> +""", + id="hilite", + ), + ], +) +def test_compiling_markdown( + compiler, input_path, output_path, input_str, expected_output +): + output = markdown_compile(compiler, input_path, output_path, input_str) + assert output.strip() == expected_output.strip() - self.compiler = CompileMarkdown() - self.compiler.set_site(FakeSite()) - def compile(self, input_string): - with io.open(self.input_path, "w+", encoding="utf8") as input_file: - input_file.write(input_string) +@pytest.fixture(scope="module") +def site(): + return FakeSite() - self.compiler.compile_html(self.input_path, self.output_path) - output_str = None - with io.open(self.output_path, "r", encoding="utf8") as output_path: - output_str = output_path.read() +@pytest.fixture(scope="module") +def compiler(site): + compiler = CompileMarkdown() + compiler.set_site(site) + return compiler - return output_str - def tearDown(self): - shutil.rmtree(self.tmp_dir) +@pytest.fixture +def input_path(tmpdir): + return path.join(str(tmpdir), "input.markdown") - def test_compile_html_empty(self): - input_str = '' - actual_output = self.compile(input_str) - self.assertEquals(actual_output, '') - def test_compile_html_code_hilite(self): - input_str = '''\ - #!python - from this -''' - expected_output = '''\ -<table class="codehilitetable"><tr><td class="linenos">\ -<div class="linenodiv"><pre>1</pre></div>\ -</td><td class="code"><pre class="code literal-block"><span></span>\ -<span class="kn">from</span> <span class="nn">this</span> -</pre> -</td></tr></table> -''' +@pytest.fixture +def output_path(tmpdir): + return path.join(str(tmpdir), "output.html") - actual_output = self.compile(input_str) - self.assertEquals(actual_output.strip(), expected_output.strip()) - def test_compile_strikethrough(self): - input_str = '~~striked out text~~' - expected_output = '<p><del>striked out text</del></p>' +def markdown_compile(compiler, input_path, output_path, text): + with io.open(input_path, "w+", encoding="utf8") as input_file: + input_file.write(text) - actual_output = self.compile(input_str) - self.assertEquals(actual_output.strip(), expected_output.strip()) + compiler.compile(input_path, output_path, lang="en") -if __name__ == '__main__': - unittest.main() + with io.open(output_path, "r", encoding="utf8") as output_path: + return output_path.read() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..c67929d --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,63 @@ +import os +import re + +import pytest + +from nikola import __main__ as nikola + + +def test_simple_config(simple_config, metadata_option): + """Check whether configuration-files without ineritance are interpreted correctly.""" + assert simple_config[metadata_option]["ID"] == "conf" + + +def test_inherited_config(simple_config, metadata_option, complex_config): + """Check whether configuration-files with ineritance are interpreted correctly.""" + check_base_equality(simple_config, metadata_option, complex_config) + assert complex_config[metadata_option]["ID"] == "prod" + + +def test_config_with_illegal_filename( + simple_config, metadata_option, complex_filename_config +): + """Check whether files with illegal module-name characters can be set as config-files, too.""" + check_base_equality(simple_config, metadata_option, complex_filename_config) + assert complex_filename_config[metadata_option]["ID"] == "illegal" + + +@pytest.fixture(scope="module") +def simple_config(data_dir): + nikola.main(["--conf=" + os.path.join(data_dir, "conf.py")]) + return nikola.config + + +@pytest.fixture(scope="module") +def data_dir(test_dir): + return os.path.join(test_dir, "data", "test_config") + + +@pytest.fixture +def metadata_option(): + return "ADDITIONAL_METADATA" + + +@pytest.fixture(scope="module") +def complex_config(data_dir): + nikola.main(["--conf=" + os.path.join(data_dir, "prod.py")]) + return nikola.config + + +@pytest.fixture(scope="module") +def complex_filename_config(data_dir): + config_path = os.path.join( + data_dir, "config.with+illegal(module)name.characters.py" + ) + nikola.main(["--conf=" + config_path]) + return nikola.config + + +def check_base_equality(base_config, metadata_option, config): + """Check whether the specified `config` equals the base config.""" + for option in base_config.keys(): + if re.match("^[A-Z]+(_[A-Z]+)*$", option) and option != metadata_option: + assert base_config[option] == config[option] diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 87a3eba..0000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,549 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function, absolute_import - -import os -import sys - -import io -import locale -import shutil -import subprocess -import tempfile -import unittest - -import lxml.html -import pytest - -from nikola import __main__ -import nikola -import nikola.plugins.command -import nikola.plugins.command.init -import nikola.utils - -from .base import BaseTestCase, cd, LocaleSupportInTesting - -LocaleSupportInTesting.initialize() - - -class EmptyBuildTest(BaseTestCase): - """Basic integration testcase.""" - - dataname = None - - @classmethod - def setUpClass(cls): - """Setup a demo site.""" - # for tests that need bilingual support override language_settings - cls.language_settings() - cls.startdir = os.getcwd() - cls.tmpdir = tempfile.mkdtemp() - cls.target_dir = os.path.join(cls.tmpdir, "target") - cls.init_command = nikola.plugins.command.init.CommandInit() - cls.fill_site() - cls.patch_site() - cls.build() - - @classmethod - def language_settings(cls): - LocaleSupportInTesting.initialize_locales_for_testing("unilingual") - - @classmethod - def fill_site(self): - """Add any needed initial content.""" - self.init_command.create_empty_site(self.target_dir) - self.init_command.create_configuration(self.target_dir) - - if self.dataname: - src = os.path.join(os.path.dirname(__file__), 'data', - self.dataname) - for root, dirs, files in os.walk(src): - for src_name in files: - rel_dir = os.path.relpath(root, src) - dst_file = os.path.join(self.target_dir, rel_dir, src_name) - src_file = os.path.join(root, src_name) - shutil.copy2(src_file, dst_file) - - @classmethod - def patch_site(self): - """Make any modifications you need to the site.""" - - @classmethod - def build(self): - """Build the site.""" - with cd(self.target_dir): - __main__.main(["build"]) - - @classmethod - def tearDownClass(self): - """Remove the demo site.""" - # Don't saw off the branch you're sitting on! - os.chdir(self.startdir) - # ignore_errors=True for windows by issue #782 - shutil.rmtree(self.tmpdir, ignore_errors=(sys.platform == 'win32')) - # Fixes Issue #438 - try: - del sys.modules['conf'] - except KeyError: - pass - # clear LocaleBorg state - nikola.utils.LocaleBorg.reset() - if hasattr(self.__class__, "ol"): - delattr(self.__class__, "ol") - - def test_build(self): - """Ensure the build did something.""" - index_path = os.path.join( - self.target_dir, "output", "archive.html") - self.assertTrue(os.path.isfile(index_path)) - - -class DemoBuildTest(EmptyBuildTest): - """Test that a default build of --demo works.""" - - @classmethod - def fill_site(self): - """Fill the site with demo content.""" - self.init_command.copy_sample_site(self.target_dir) - self.init_command.create_configuration(self.target_dir) - src1 = os.path.join(os.path.dirname(__file__), 'data', '1-nolinks.rst') - dst1 = os.path.join(self.target_dir, 'posts', '1.rst') - shutil.copy(src1, dst1) - # File for Issue #374 (empty post text) - with io.open(os.path.join(self.target_dir, 'posts', 'empty.txt'), "w+", encoding="utf8") as outf: - outf.write( - ".. title: foobar\n" - ".. slug: foobar\n" - ".. date: 2013-03-06 19:08:15\n" - ) - - def test_index_in_sitemap(self): - sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml") - sitemap_data = io.open(sitemap_path, "r", encoding="utf8").read() - self.assertTrue('<loc>https://example.com/index.html</loc>' in sitemap_data) - - def test_avoid_double_slash_in_rss(self): - rss_path = os.path.join(self.target_dir, "output", "rss.xml") - rss_data = io.open(rss_path, "r", encoding="utf8").read() - self.assertFalse('https://example.com//' in rss_data) - - -class RepeatedPostsSetting(DemoBuildTest): - """Duplicate POSTS, should not read each post twice, which causes conflicts.""" - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "a", encoding="utf8") as outf: - outf.write('\nPOSTS = (("posts/*.txt", "posts", "post.tmpl"),("posts/*.txt", "posts", "post.tmpl"))\n') - - -class FuturePostTest(EmptyBuildTest): - """Test a site with future posts.""" - - @classmethod - def fill_site(self): - import datetime - from nikola.utils import current_time - self.init_command.copy_sample_site(self.target_dir) - self.init_command.create_configuration(self.target_dir) - - # Change COMMENT_SYSTEM_ID to not wait for 5 seconds - with io.open(os.path.join(self.target_dir, 'conf.py'), "a+", encoding="utf8") as outf: - outf.write('\nCOMMENT_SYSTEM_ID = "nikolatest"\n') - - with io.open(os.path.join(self.target_dir, 'posts', 'empty1.txt'), "w+", encoding="utf8") as outf: - outf.write( - ".. title: foo\n" - ".. slug: foo\n" - ".. date: %s\n" % (current_time() + datetime.timedelta(-1)).strftime('%Y-%m-%d %H:%M:%S') - ) - - with io.open(os.path.join(self.target_dir, 'posts', 'empty2.txt'), "w+", encoding="utf8") as outf: - outf.write( - ".. title: bar\n" - ".. slug: bar\n" - ".. date: %s\n" % (current_time() + datetime.timedelta(1)).strftime('%Y-%m-%d %H:%M:%S') - ) - - def test_future_post(self): - """ Ensure that the future post is not present in the index and sitemap.""" - index_path = os.path.join(self.target_dir, "output", "index.html") - sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml") - foo_path = os.path.join(self.target_dir, "output", "posts", "foo.html") - bar_path = os.path.join(self.target_dir, "output", "posts", "bar.html") - self.assertTrue(os.path.isfile(index_path)) - self.assertTrue(os.path.isfile(foo_path)) - self.assertTrue(os.path.isfile(bar_path)) - index_data = io.open(index_path, "r", encoding="utf8").read() - sitemap_data = io.open(sitemap_path, "r", encoding="utf8").read() - self.assertTrue('foo.html' in index_data) - self.assertFalse('bar.html' in index_data) - self.assertTrue('foo.html' in sitemap_data) - self.assertFalse('bar.html' in sitemap_data) - - # Run deploy command to see if future post is deleted - with cd(self.target_dir): - __main__.main(["deploy"]) - - self.assertTrue(os.path.isfile(index_path)) - self.assertTrue(os.path.isfile(foo_path)) - self.assertFalse(os.path.isfile(bar_path)) - - -class TranslatedBuildTest(EmptyBuildTest): - """Test a site with translated content.""" - - dataname = "translated_titles" - - @classmethod - def language_settings(cls): - LocaleSupportInTesting.initialize_locales_for_testing("bilingual") - # the other language - cls.ol = LocaleSupportInTesting.langlocales["other"][0] - - def test_translated_titles(self): - """Check that translated title is picked up.""" - en_file = os.path.join(self.target_dir, "output", "pages", "1.html") - pl_file = os.path.join(self.target_dir, "output", self.ol, "pages", "1.html") - # Files should be created - self.assertTrue(os.path.isfile(en_file)) - self.assertTrue(os.path.isfile(pl_file)) - # And now let's check the titles - with io.open(en_file, 'r', encoding='utf8') as inf: - doc = lxml.html.parse(inf) - self.assertEqual(doc.find('//title').text, 'Foo | Demo Site') - with io.open(pl_file, 'r', encoding='utf8') as inf: - doc = lxml.html.parse(inf) - self.assertEqual(doc.find('//title').text, 'Bar | Demo Site') - - -class TranslationsPatternTest1(TranslatedBuildTest): - """Check that the path.lang.ext TRANSLATIONS_PATTERN works too""" - - @classmethod - def patch_site(self): - """Set the TRANSLATIONS_PATTERN to the old v6 default""" - os.rename(os.path.join(self.target_dir, "pages", "1.%s.txt" % self.ol), - os.path.join(self.target_dir, "pages", "1.txt.%s" % self.ol) - ) - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}"', - 'TRANSLATIONS_PATTERN = "{path}.{ext}.{lang}"') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - - -class MissingDefaultLanguageTest(TranslatedBuildTest): - """Make sure posts only in secondary languages work.""" - - @classmethod - def fill_site(self): - super(MissingDefaultLanguageTest, self).fill_site() - os.unlink(os.path.join(self.target_dir, "pages", "1.txt")) - - def test_translated_titles(self): - """Do not test titles as we just removed the translation""" - pass - - -class TranslationsPatternTest2(TranslatedBuildTest): - """Check that the path_lang.ext TRANSLATIONS_PATTERN works too""" - - @classmethod - def patch_site(self): - """Set the TRANSLATIONS_PATTERN to the old v6 default""" - conf_path = os.path.join(self.target_dir, "conf.py") - os.rename(os.path.join(self.target_dir, "pages", "1.%s.txt" % self.ol), - os.path.join(self.target_dir, "pages", "1.txt.%s" % self.ol) - ) - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}"', - 'TRANSLATIONS_PATTERN = "{path}.{ext}.{lang}"') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - - -class RelativeLinkTest(DemoBuildTest): - """Check that SITE_URL with a path doesn't break links.""" - - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('SITE_URL = "https://example.com/"', - 'SITE_URL = "https://example.com/foo/bar/"') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - - def test_relative_links(self): - """Check that the links in output/index.html are correct""" - test_path = os.path.join(self.target_dir, "output", "index.html") - flag = False - with open(test_path, "rb") as inf: - data = inf.read() - for _, _, url, _ in lxml.html.iterlinks(data): - # Just need to be sure this one is ok - if url.endswith("css"): - self.assertFalse(url.startswith("..")) - flag = True - # But I also need to be sure it is there! - self.assertTrue(flag) - - def test_index_in_sitemap(self): - """Test that the correct path is in sitemap, and not the wrong one.""" - sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml") - sitemap_data = io.open(sitemap_path, "r", encoding="utf8").read() - self.assertFalse('<loc>https://example.com/</loc>' in sitemap_data) - self.assertTrue('<loc>https://example.com/foo/bar/index.html</loc>' in sitemap_data) - - -class TestCheck(DemoBuildTest): - """The demo build should pass 'nikola check'""" - - def test_check_links(self): - with cd(self.target_dir): - self.assertIsNone(__main__.main(['check', '-l'])) - - def test_check_files(self): - with cd(self.target_dir): - self.assertIsNone(__main__.main(['check', '-f'])) - - -class TestCheckAbsoluteSubFolder(TestCheck): - """Validate links in a site which is: - - * built in URL_TYPE="absolute" - * deployable to a subfolder (BASE_URL="https://example.com/foo/") - """ - - @classmethod - def patch_site(self): - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('SITE_URL = "https://example.com/"', - 'SITE_URL = "https://example.com/foo/"') - data = data.replace("# URL_TYPE = 'rel_path'", - "URL_TYPE = 'absolute'") - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - def test_index_in_sitemap(self): - """Test that the correct path is in sitemap, and not the wrong one.""" - sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml") - sitemap_data = io.open(sitemap_path, "r", encoding="utf8").read() - self.assertTrue('<loc>https://example.com/foo/index.html</loc>' in sitemap_data) - - -class TestCheckFullPathSubFolder(TestCheckAbsoluteSubFolder): - """Validate links in a site which is: - - * built in URL_TYPE="full_path" - * deployable to a subfolder (BASE_URL="https://example.com/foo/") - """ - - @classmethod - def patch_site(self): - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('SITE_URL = "https://example.com/"', - 'SITE_URL = "https://example.com/foo/"') - data = data.replace("# URL_TYPE = 'rel_path'", - "URL_TYPE = 'full_path'") - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - -class TestCheckFailure(DemoBuildTest): - """The demo build should pass 'nikola check'""" - - def test_check_links_fail(self): - with cd(self.target_dir): - os.unlink(os.path.join("output", "archive.html")) - try: - __main__.main(['check', '-l']) - except SystemExit as e: - self.assertNotEqual(e.code, 0) - - def test_check_files_fail(self): - with cd(self.target_dir): - with io.open(os.path.join("output", "foobar"), "w+", encoding="utf8") as outf: - outf.write("foo") - try: - __main__.main(['check', '-f']) - except SystemExit as e: - self.assertNotEqual(e.code, 0) - - -class RelativeLinkTest2(DemoBuildTest): - """Check that dropping pages to the root doesn't break links.""" - - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('("pages/*.txt", "pages", "story.tmpl"),', - '("pages/*.txt", "", "story.tmpl"),') - data = data.replace('("pages/*.rst", "pages", "story.tmpl"),', - '("pages/*.rst", "", "story.tmpl"),') - data = data.replace('# INDEX_PATH = ""', - 'INDEX_PATH = "blog"') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - def test_relative_links(self): - """Check that the links in a page are correct""" - test_path = os.path.join(self.target_dir, "output", "about-nikola.html") - flag = False - with open(test_path, "rb") as inf: - data = inf.read() - for _, _, url, _ in lxml.html.iterlinks(data): - # Just need to be sure this one is ok - if url.endswith("css"): - self.assertFalse(url.startswith("..")) - flag = True - # But I also need to be sure it is there! - self.assertTrue(flag) - - def test_index_in_sitemap(self): - """Test that the correct path is in sitemap, and not the wrong one.""" - sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml") - sitemap_data = io.open(sitemap_path, "r", encoding="utf8").read() - self.assertFalse('<loc>https://example.com/</loc>' in sitemap_data) - self.assertTrue('<loc>https://example.com/blog/index.html</loc>' in sitemap_data) - - -class MonthlyArchiveTest(DemoBuildTest): - """Check that the monthly archives build and are correct.""" - - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('# CREATE_MONTHLY_ARCHIVE = False', - 'CREATE_MONTHLY_ARCHIVE = True') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - def test_monthly_archive(self): - """See that it builds""" - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', '03', 'index.html'))) - - -class DayArchiveTest(DemoBuildTest): - """Check that per-day archives build and are correct.""" - - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('# CREATE_DAILY_ARCHIVE = False', - 'CREATE_DAILY_ARCHIVE = True') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - def test_day_archive(self): - """See that it builds""" - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', '03', '30', 'index.html'))) - - -class FullArchiveTest(DemoBuildTest): - """Check that full archives build and are correct.""" - - @classmethod - def patch_site(self): - """Set the SITE_URL to have a path""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "r", encoding="utf-8") as inf: - data = inf.read() - data = data.replace('# CREATE_FULL_ARCHIVES = False', - 'CREATE_FULL_ARCHIVES = True') - with io.open(conf_path, "w+", encoding="utf8") as outf: - outf.write(data) - outf.flush() - - def test_full_archive(self): - """See that it builds""" - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', 'archive.html'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', 'index.html'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', '03', 'index.html'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', '03', '30', 'index.html'))) - - -class SubdirRunningTest(DemoBuildTest): - """Check that running nikola from subdir works.""" - - def test_subdir_run(self): - """Check whether build works from posts/""" - - with cd(os.path.join(self.target_dir, 'posts')): - result = __main__.main(['build']) - self.assertEquals(result, 0) - - -class RedirectionsTest1(TestCheck): - """Check REDIRECTIONS""" - - @classmethod - def patch_site(self): - """""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "a", encoding="utf8") as outf: - outf.write("""\n\nREDIRECTIONS = [ ("posts/foo.html", "/foo/bar.html"), ]\n\n""") - - @classmethod - def fill_site(self): - target_path = os.path.join(self.target_dir, "files", "foo", "bar.html") - nikola.utils.makedirs(os.path.join(self.target_dir, "files", "foo")) - with io.open(target_path, "w+", encoding="utf8") as outf: - outf.write("foo") - - -class RedirectionsTest2(TestCheck): - """Check external REDIRECTIONS""" - - @classmethod - def patch_site(self): - """""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "a", encoding="utf8") as outf: - outf.write("""\n\nREDIRECTIONS = [ ("foo.html", "http://www.example.com/"), ]\n\n""") - - -class RedirectionsTest3(TestCheck): - """Check relative REDIRECTIONS""" - - @classmethod - def patch_site(self): - """""" - conf_path = os.path.join(self.target_dir, "conf.py") - with io.open(conf_path, "a", encoding="utf8") as outf: - outf.write("""\n\nREDIRECTIONS = [ ("foo.html", "foo/bar.html"), ]\n\n""") - - @classmethod - def fill_site(self): - target_path = os.path.join(self.target_dir, "files", "foo", "bar.html") - nikola.utils.makedirs(os.path.join(self.target_dir, "files", "foo")) - with io.open(target_path, "w+", encoding="utf8") as outf: - outf.write("foo") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_locale.py b/tests/test_locale.py index 8ffa112..eaeb8e1 100644 --- a/tests/test_locale.py +++ b/tests/test_locale.py @@ -1,244 +1,217 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import os -import sys - - -# needed if @unittest.expectedFailure is used -try: - import unittest2 as unittest -except: - import unittest - -import nikola.nikola -import nikola.utils -from .base import LocaleSupportInTesting - -LocaleSupportInTesting.initialize_locales_for_testing('bilingual') -lang_11, loc_11 = LocaleSupportInTesting.langlocales['default'] -lang_22, loc_22 = LocaleSupportInTesting.langlocales['other'] - - -# these are candidates to hardcoded locales, using str() for py2x setlocale -loc_C = str('C') -loc_Cutf8 = str('C.utf8') - -if sys.platform != 'win32': - nikola.nikola.workaround_empty_LC_ALL_posix() - - -class TestHarcodedFallbacks(unittest.TestCase): - def test_hardcoded_fallbacks_work(self): - # keep in sync with nikola.valid_locale_fallback - if sys.platform == 'win32': - self.assertTrue(nikola.nikola.is_valid_locale(str('English'))) - self.assertTrue(nikola.nikola.is_valid_locale(str('C'))) - else: - # the 1st is desired in Travis, not a problem if fails in user host - self.assertTrue(nikola.nikola.is_valid_locale(str('en_US.utf8'))) - # this is supposed to be always valid, and we need an universal - # fallback. Failure is not a problem in user host if he / she - # sets a valid (in his host) locale_fallback. - self.assertTrue(nikola.nikola.is_valid_locale(str('C'))) - - -class TestConfigLocale(unittest.TestCase): - - def test_implicit_fallback(self): - locale_fallback = None - sanitized_fallback = nikola.nikola.valid_locale_fallback( - desired_locale=locale_fallback) - self.assertTrue(nikola.nikola.is_valid_locale(sanitized_fallback)) - - def test_explicit_good_fallback(self): - locale_fallback = loc_22 - sanitized_fallback = nikola.nikola.valid_locale_fallback( - desired_locale=locale_fallback) - self.assertEquals(sanitized_fallback, locale_fallback) - - def test_explicit_bad_fallback(self): - locale_fallback = str('xyz') - sanitized_fallback = nikola.nikola.valid_locale_fallback( - desired_locale=locale_fallback) - self.assertTrue(nikola.nikola.is_valid_locale(sanitized_fallback)) - - def test_explicit_good_default(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - loc_11, - {}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertEquals(fallback, locale_fallback) - self.assertEquals(default, locale_default) - - def test_explicit_bad_default(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - str('xyz'), - {}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertEquals(fallback, locale_fallback) - self.assertEquals(default, fallback) - - def test_extra_locales_deleted(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - None, - {'@z': loc_22}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertTrue('@z' not in locales) - - def test_explicit_good_locale_retained(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - loc_22, - {lang_11: loc_11}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertEquals(locales[lang_11], str(LOCALES[lang_11])) - - def test_explicit_bad_locale_replaced_with_fallback(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - loc_11, - {lang_11: str('xyz')}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertEquals(locales['en'], locale_fallback) - - def test_impicit_locale_when_default_locale_defined(self): - locale_fallback, locale_default, LOCALES, translations = ( - loc_11, - loc_22, - {}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - self.assertEquals(locales['en'], locale_default) - - def test_impicit_locale_when_default_locale_is_not_defined(self): - # legacy mode, compat v6.0.4 : guess locale from lang - locale_fallback, locale_default, LOCALES, translations = ( - loc_22, - None, - {}, - {lang_11: ''}, - ) - fallback, default, locales = nikola.nikola.sanitized_locales( - locale_fallback, - locale_default, - LOCALES, - translations) - if sys.platform == 'win32': - guess_locale_for_lang = nikola.nikola.guess_locale_from_lang_windows - else: - guess_locale_for_lang = nikola.nikola.guess_locale_from_lang_posix - - self.assertEquals(locales[lang_11], guess_locale_for_lang(lang_11)) - - -class TestCalendarRelated(unittest.TestCase): - def test_type_of_month_name(self): - """validate assumption calendar month name is of type str - - Yes, both in windows and linuxTravis, py 26, 27, 33 - """ - import calendar - if sys.version_info[0] == 3: # Python 3 - with calendar.different_locale(loc_11): - s = calendar.month_name[1] - else: # Python 2 - with calendar.TimeEncoding(loc_11): - s = calendar.month_name[1] - self.assertTrue(type(s) == str) - - -class TestLocaleBorg(unittest.TestCase): - def test_initial_lang(self): - lang_11, loc_11 = LocaleSupportInTesting.langlocales['default'] - lang_22, loc_22 = LocaleSupportInTesting.langlocales['other'] - - locales = {lang_11: loc_11, lang_22: loc_22} - initial_lang = lang_22 - nikola.utils.LocaleBorg.initialize(locales, initial_lang) - self.assertEquals(initial_lang, nikola.utils.LocaleBorg().current_lang) - - def test_remembers_last_lang(self): - lang_11, loc_11 = LocaleSupportInTesting.langlocales['default'] - lang_22, loc_22 = LocaleSupportInTesting.langlocales['other'] - - locales = {lang_11: loc_11, lang_22: loc_22} - initial_lang = lang_22 - nikola.utils.LocaleBorg.initialize(locales, initial_lang) - - nikola.utils.LocaleBorg().set_locale(lang_11) - self.assertTrue(nikola.utils.LocaleBorg().current_lang, lang_11) - - def test_services_ensure_initialization(self): - nikola.utils.LocaleBorg.reset() - self.assertRaises(Exception, nikola.utils.LocaleBorg) - - def test_services_reject_dumb_wrong_call(self): - lang_11, loc_11 = LocaleSupportInTesting.langlocales['default'] - nikola.utils.LocaleBorg.reset() - self.assertRaises(Exception, nikola.utils.LocaleBorg) - self.assertRaises(Exception, nikola.utils.LocaleBorg.set_locale, lang_11) - - def test_set_locale_raises_on_invalid_lang(self): - lang_11, loc_11 = LocaleSupportInTesting.langlocales['default'] - lang_22, loc_22 = LocaleSupportInTesting.langlocales['other'] - - locales = {lang_11: loc_11, lang_22: loc_22} - initial_lang = lang_22 - nikola.utils.LocaleBorg.initialize(locales, initial_lang) - self.assertRaises(KeyError, nikola.utils.LocaleBorg().set_locale, '@z') - - -class TestTestPreconditions(unittest.TestCase): - """If this fails the other test in this module are mostly nonsense, and - probably same for tests of multilingual features. - - Failure probably means the OS support for the failing locale is not - instaled or environmet variables NIKOLA_LOCALE_DEFAULT or - NIKOLA_LOCALE_OTHER with bad values. - """ - def test_langlocale_default_availability(self): - msg = "META ERROR: The pair lang, locale : {0} {1} is invalid" - self.assertTrue(nikola.nikola.is_valid_locale(loc_11), msg.format(lang_11, loc_11)) +import datetime +import unittest.mock - def test_langlocale_other_availability(self): - msg = "META ERROR: The pair lang, locale : {0} {1} is invalid" - self.assertTrue(nikola.nikola.is_valid_locale(loc_22), msg.format(lang_22, loc_22)) +import dateutil +import pytest + +from nikola.nikola import LEGAL_VALUES +from nikola.utils import ( + LocaleBorg, + LocaleBorgUninitializedException, + TranslatableSetting, +) + +TESLA_BIRTHDAY = datetime.date(1856, 7, 10) +TESLA_BIRTHDAY_DT = datetime.datetime(1856, 7, 10, 12, 34, 56) +DT_EN_US = "July 10, 1856 at 12:34:56 PM UTC" +DT_PL = "10 lipca 1856 12:34:56 UTC" + + +@pytest.mark.parametrize("initial_lang", [None, ""]) +def test_initilalize_failure(initial_lang): + with pytest.raises(ValueError): + LocaleBorg.initialize({}, initial_lang) + + assert not LocaleBorg.initialized + + +@pytest.mark.parametrize("initial_lang", ["en", "pl"]) +def test_initialize(initial_lang): + LocaleBorg.initialize({}, initial_lang) + assert LocaleBorg.initialized + assert LocaleBorg().current_lang == initial_lang + + +def test_uninitialized_error(): + with pytest.raises(LocaleBorgUninitializedException): + LocaleBorg() + + +@pytest.mark.parametrize( + "locale, expected_current_lang", + [ + ("pl", "pl"), + pytest.param( + "xx", "xx", id="fake language" + ), # used to ensure any locale can be supported + ], +) +def test_set_locale(base_config, locale, expected_current_lang): + LocaleBorg().set_locale(locale) + assert LocaleBorg.initialized + assert LocaleBorg().current_lang == expected_current_lang + + +def test_set_locale_for_template(): + LocaleBorg.initialize({}, "en") + assert LocaleBorg().set_locale("xz") == "" # empty string for template ease of use + + +def test_format_date_webiso_basic(base_config): + with unittest.mock.patch("babel.dates.format_datetime") as m: + formatted_date = LocaleBorg().formatted_date("webiso", TESLA_BIRTHDAY_DT) + assert formatted_date == "1856-07-10T12:34:56" + m.assert_not_called() + + +@pytest.mark.parametrize("lang", ["en", "pl"]) +def test_format_date_basic(base_config, lang): + LocaleBorg.initialize({}, lang) + formatted_date = LocaleBorg().formatted_date( + "yyyy-MM-dd HH:mm:ss", TESLA_BIRTHDAY_DT + ) + assert formatted_date == "1856-07-10 12:34:56" + + +def test_format_date_long(base_config): + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT) == DT_EN_US + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT, "en") == DT_EN_US + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT, "pl") == DT_PL + LocaleBorg().set_locale("pl") + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT) == DT_PL + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT, "en") == DT_EN_US + + +def test_format_date_timezone(base_config): + tesla_150_birthday_dtz = datetime.datetime( + 2006, 7, 10, 12, 34, 56, tzinfo=dateutil.tz.gettz("America/New_York") + ) + formatted_date = LocaleBorg().formatted_date("long", tesla_150_birthday_dtz) + assert formatted_date == "July 10, 2006 at 12:34:56 PM -0400" + + nodst = datetime.datetime( + 2006, 1, 10, 12, 34, 56, tzinfo=dateutil.tz.gettz("America/New_York") + ) + formatted_date = LocaleBorg().formatted_date("long", nodst) + assert formatted_date == "January 10, 2006 at 12:34:56 PM -0500" + + +@pytest.mark.parametrize( + "english_variant, expected_date", + [ + pytest.param("en_US", DT_EN_US, id="US"), + pytest.param("en_GB", "10 July 1856 at 12:34:56 UTC", id="GB"), + ], +) +def test_format_date_locale_variants(english_variant, expected_date): + LocaleBorg.initialize({"en": english_variant}, "en") + assert LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT, "en") == expected_date + + +@pytest.mark.parametrize( + "lang, expected_string", [("en", "en July"), ("pl", "lipca pl")] +) +def test_format_date_translatablesetting(base_config, lang, expected_string): + df = TranslatableSetting( + "DATE_FORMAT", {"en": "'en' MMMM", "pl": "MMMM 'pl'"}, {"en": "", "pl": ""} + ) + assert LocaleBorg().formatted_date(df, TESLA_BIRTHDAY_DT, lang) == expected_string + + +@pytest.mark.parametrize( + "lang, expected_string", + [ + pytest.param(None, "Foo July Bar", id="default"), + pytest.param("pl", "Foo lipiec Bar", id="pl"), + ], +) +def test_format_date_in_string_month(base_config, lang, expected_string): + formatted_date = LocaleBorg().format_date_in_string( + "Foo {month} Bar", TESLA_BIRTHDAY, lang + ) + assert formatted_date == expected_string + + +@pytest.mark.parametrize( + "lang, expected_string", + [ + pytest.param(None, "Foo July 1856 Bar", id="default"), + pytest.param("pl", "Foo lipiec 1856 Bar", id="pl"), + ], +) +def test_format_date_in_string_month_year(base_config, lang, expected_string): + formatted_date = LocaleBorg().format_date_in_string( + "Foo {month_year} Bar", TESLA_BIRTHDAY, lang + ) + assert formatted_date == expected_string + + +@pytest.mark.parametrize( + "lang, expected_string", + [ + pytest.param(None, "Foo July 10, 1856 Bar", id="default"), + pytest.param("pl", "Foo 10 lipca 1856 Bar", id="pl"), + ], +) +def test_format_date_in_string_month_day_year(base_config, lang, expected_string): + formatted_date = LocaleBorg().format_date_in_string( + "Foo {month_day_year} Bar", TESLA_BIRTHDAY, lang + ) + assert formatted_date == expected_string + + +@pytest.mark.parametrize( + "lang, expected_string", + [ + pytest.param(None, "Foo 10 July 1856 Bar", id="default"), + pytest.param("pl", "Foo 10 lipca 1856 Bar", id="pl"), + ], +) +def test_format_date_in_string_month_day_year_gb(lang, expected_string): + LocaleBorg.initialize({"en": "en_GB"}, "en") + formatted_date = LocaleBorg().format_date_in_string( + "Foo {month_day_year} Bar", TESLA_BIRTHDAY, lang + ) + assert formatted_date == expected_string + + +@pytest.mark.parametrize( + "message, expected_string", + [ + ("Foo {month:'miesiąca' MMMM} Bar", "Foo miesiąca lipca Bar"), + ("Foo {month_year:MMMM yyyy} Bar", "Foo lipca 1856 Bar"), + ], +) +def test_format_date_in_string_customization(base_config, message, expected_string): + formatted_date = LocaleBorg().format_date_in_string(message, TESLA_BIRTHDAY, "pl") + assert formatted_date == expected_string + + +@pytest.mark.parametrize( + "lang, expected_format", + [("sr", "10. јул 1856. 12:34:56 UTC"), ("sr_latin", "10. jul 1856. 12:34:56 UTC")], +) +def test_locale_base(lang, expected_format): + LocaleBorg.initialize(LEGAL_VALUES["LOCALES_BASE"], "en") + formatted_date = LocaleBorg().formatted_date("long", TESLA_BIRTHDAY_DT, lang) + assert formatted_date == expected_format + + +@pytest.fixture(autouse=True) +def localeborg_reset(): + """ + Reset the LocaleBorg before and after every test. + """ + LocaleBorg.reset() + assert not LocaleBorg.initialized + try: + yield + finally: + LocaleBorg.reset() + assert not LocaleBorg.initialized + + +@pytest.fixture +def base_config(): + """A base config of LocaleBorg.""" + LocaleBorg.initialize({}, "en") diff --git a/tests/test_metadata_extractors.py b/tests/test_metadata_extractors.py new file mode 100644 index 0000000..58dd7b5 --- /dev/null +++ b/tests/test_metadata_extractors.py @@ -0,0 +1,225 @@ +"""Test metadata extractors.""" + +import os +from unittest import mock + +import pytest + +from nikola.metadata_extractors import ( + MetaCondition, + check_conditions, + default_metadata_extractors_by, + load_defaults, +) +from nikola.plugins.compile.rest import CompileRest +from nikola.plugins.compile.markdown import CompileMarkdown +from nikola.plugins.compile.ipynb import CompileIPynb +from nikola.plugins.compile.html import CompileHtml +from nikola.post import get_meta + +from .helper import FakeSite + + +@pytest.mark.parametrize( + "filecount, expected, unexpected", + [(1, "onefile", "twofile"), (2, "twofile", "onefile")], +) +@pytest.mark.parametrize( + "format_lc, format_friendly", + [("nikola", "Nikola"), ("toml", "TOML"), ("yaml", "YAML")], +) +def test_builtin_extractors_rest( + metadata_extractors_by, + testfiledir, + filecount, + expected, + unexpected, + format_lc, + format_friendly, +): + is_two_files = filecount == 2 + + source_filename = "f-rest-{0}-{1}.rst".format(filecount, format_lc) + metadata_filename = "f-rest-{0}-{1}.meta".format(filecount, format_lc) + source_path = os.path.join(testfiledir, source_filename) + metadata_path = os.path.join(testfiledir, metadata_filename) + post = FakePost(source_path, metadata_path, {}, None, metadata_extractors_by) + + assert os.path.exists(source_path) + if is_two_files: + assert os.path.exists(metadata_path) + + meta, extractor = get_meta(post, None) + + assert meta + assert extractor is metadata_extractors_by["name"][format_lc] + + assert meta["title"] == "T: reST, {0}, {1}".format(filecount, format_friendly) + assert meta["slug"] == "s-rest-{0}-{1}".format(filecount, format_lc) + assert expected in meta["tags"] + assert unexpected not in meta["tags"] + assert "meta" in meta["tags"] + assert format_friendly in meta["tags"] + assert "reST" in meta["tags"] + assert meta["date"] == "2017-07-01 00:00:00 UTC" + + +@pytest.mark.parametrize( + "filecount, expected, unexpected", + [(1, "onefile", "twofile"), (2, "twofile", "onefile")], +) +def test_nikola_meta_markdown( + metadata_extractors_by, testfiledir, filecount, expected, unexpected +): + is_two_files = filecount == 2 + + source_filename = "f-markdown-{0}-nikola.md".format(filecount) + metadata_filename = "f-markdown-{0}-nikola.meta".format(filecount) + source_path = os.path.join(testfiledir, source_filename) + metadata_path = os.path.join(testfiledir, metadata_filename) + post = FakePost(source_path, metadata_path, {}, None, metadata_extractors_by) + + assert os.path.exists(source_path) + if is_two_files: + assert os.path.exists(metadata_path) + + meta, extractor = get_meta(post, None) + assert extractor is metadata_extractors_by["name"]["nikola"] + + assert meta["title"] == "T: Markdown, {0}, Nikola".format(filecount) + assert meta["slug"] == "s-markdown-{0}-nikola".format(filecount) + assert expected in meta["tags"] + assert unexpected not in meta["tags"] + assert "meta" in meta["tags"] + assert "Nikola" in meta["tags"] + assert "Markdown" in meta["tags"] + assert meta["date"] == "2017-07-01 00:00:00 UTC" + + +@pytest.mark.parametrize( + "compiler, fileextension, compiler_lc, name", + [ + (CompileRest, "rst", "rest", "reST"), + (CompileMarkdown, "md", "markdown", "Markdown"), + (CompileIPynb, "ipynb", "ipynb", "Jupyter Notebook"), + (CompileHtml, "html", "html", "HTML"), + ], +) +def test_compiler_metadata( + metadata_extractors_by, testfiledir, compiler, fileextension, compiler_lc, name +): + source_filename = "f-{0}-1-compiler.{1}".format(compiler_lc, fileextension) + metadata_filename = "f-{0}-1-compiler.meta".format(compiler_lc) + title = "T: {0}, 1, compiler".format(name) + slug = "s-{0}-1-compiler".format(compiler_lc) + source_path = os.path.join(testfiledir, source_filename) + metadata_path = os.path.join(testfiledir, metadata_filename) + + config = { + "USE_REST_DOCINFO_METADATA": True, + "MARKDOWN_EXTENSIONS": ["markdown.extensions.meta"], + } + site = FakeSite() + site.config.update(config) + compiler_obj = compiler() + compiler_obj.set_site(site) + + post = FakePost( + source_path, metadata_path, config, compiler_obj, metadata_extractors_by + ) + + class FakeBorg: + current_lang = "en" + + def __call__(self): + return self + + with mock.patch("nikola.plugins.compile." + compiler_lc + ".LocaleBorg", FakeBorg): + meta, extractor = get_meta(post, None) + + assert meta["title"] == title + assert meta["slug"] == slug + assert "meta" in meta["tags"] + assert "onefile" in meta["tags"] + assert "compiler" in meta["tags"] + assert name in meta["tags"] + assert meta["date"] == "2017-07-01 00:00:00 UTC" + + +def test_yaml_none_handling(metadata_extractors_by): + yaml_extractor = metadata_extractors_by["name"]["yaml"] + meta = yaml_extractor.extract_text("---\ntitle: foo\nslug: null") + assert meta["title"] == "foo" + assert meta["slug"] == "" + + +@pytest.mark.parametrize( + "conditions", + [ + [(MetaCondition.config_bool, "baz"), (MetaCondition.config_present, "quux")], + pytest.param( + [(MetaCondition.config_bool, "quux")], marks=pytest.mark.xfail(strict=True) + ), + pytest.param( + [(MetaCondition.config_present, "foobar")], + marks=pytest.mark.xfail(strict=True), + ), + [(MetaCondition.extension, "bar")], + pytest.param( + [(MetaCondition.extension, "baz")], marks=pytest.mark.xfail(strict=True) + ), + [(MetaCondition.compiler, "foo")], + pytest.param( + [(MetaCondition.compiler, "foobar")], marks=pytest.mark.xfail(strict=True) + ), + pytest.param( + [(MetaCondition.never, None), (MetaCondition.config_present, "bar")], + marks=pytest.mark.xfail(strict=True), + ), + ], +) +def test_check_conditions(conditions, dummy_post): + filename = "foo.bar" + config = {"baz": True, "quux": False} + assert check_conditions(dummy_post, filename, conditions, config, "") + + +class FakePost: + def __init__( + self, source_path, metadata_path, config, compiler, metadata_extractors_by + ): + self.source_path = source_path + self.metadata_path = metadata_path + self.is_two_file = True + self.config = {"TRANSLATIONS": {"en": "./"}, "DEFAULT_LANG": "en"} + self.config.update(config) + self.default_lang = self.config["DEFAULT_LANG"] + self.metadata_extractors_by = metadata_extractors_by + if compiler: + self.compiler = compiler + + def translated_source_path(self, _): + return self.source_path + + +@pytest.fixture +def metadata_extractors_by(): + metadata_extractors = default_metadata_extractors_by() + load_defaults(None, metadata_extractors) + return metadata_extractors + + +@pytest.fixture(scope="module") +def testfiledir(test_dir): + return os.path.join(test_dir, "data", "metadata_extractors") + + +@pytest.fixture(scope="module") +def dummy_post(): + class DummyCompiler: + name = "foo" + + class DummyPost: + compiler = DummyCompiler() + + return DummyPost() diff --git a/tests/test_path_handlers.py b/tests/test_path_handlers.py new file mode 100644 index 0000000..23b6bf1 --- /dev/null +++ b/tests/test_path_handlers.py @@ -0,0 +1,114 @@ +"""Test that CATEGORIES_INDEX_PATH and TAGS_INDEX_PATH return the correct values on Unix and Windows.""" +from unittest import mock + +from nikola import Nikola +from nikola.plugins.misc.taxonomies_classifier import TaxonomiesClassifier +from nikola.plugins.task.authors import ClassifyAuthors +from nikola.plugins.task.categories import ClassifyCategories +from nikola.plugins.task.tags import ClassifyTags +from nikola.plugins.task.taxonomies import RenderTaxonomies + +import pytest + + +@pytest.fixture(params=[ClassifyAuthors, ClassifyCategories, ClassifyTags], ids=["authors", "categories", "tags"]) +def taxonomy(request): + return request.param() + + +@pytest.fixture(params=[ + "base:", "base:blog", "base:path/with/trailing/slash/", "base:/path/with/leading/slash", + "index:tags.html", "index:blog/tags.html", "index:path/to/tags.html", "index:/path/with/leading/slash.html", +]) +def path(request): + return request.param + + +@pytest.fixture +def fixture(taxonomy, path): + scheme, _, path = path.partition(':') + append_index = scheme == 'base' + if isinstance(taxonomy, ClassifyAuthors) and append_index: + site = Nikola(TRANSLATIONS={"en": ""}, AUTHOR_PATH=path) + elif isinstance(taxonomy, ClassifyAuthors) and not append_index: + pytest.skip("There is no AUTHORS_INDEX_PATH setting") + elif isinstance(taxonomy, ClassifyCategories) and append_index: + site = Nikola(TRANSLATIONS={"en": ""}, CATEGORY_PATH=path) + elif isinstance(taxonomy, ClassifyCategories) and not append_index: + site = Nikola(TRANSLATIONS={"en": ""}, CATEGORIES_INDEX_PATH=path) + elif isinstance(taxonomy, ClassifyTags) and append_index: + site = Nikola(TRANSLATIONS={"en": ""}, TAG_PATH=path) + elif isinstance(taxonomy, ClassifyTags) and not append_index: + site = Nikola(TRANSLATIONS={"en": ""}, TAGS_INDEX_PATH=path) + else: + raise TypeError("Unknown taxonomy %r" % type(taxonomy)) + + site._template_system = mock.MagicMock() + site._template_system.template_deps.return_value = [] + site._template_system.name = "dummy" + site.hierarchy_per_classification = {taxonomy.classification_name: {"en": []}} + site.posts_per_classification = {taxonomy.classification_name: {"en": {}}} + site.taxonomy_plugins = {taxonomy.classification_name: taxonomy} + + taxonomy.set_site(site) + + classifier = TaxonomiesClassifier() + classifier.set_site(site) + + expected = path.strip("/") + if append_index: + expected += "/" + if not expected.startswith("/"): + expected = "/" + expected + + return site, classifier, taxonomy, append_index, expected + + +def test_render_taxonomies_permalink(fixture): + # Arrange + site, _, taxonomy, _, expected = fixture + renderer = RenderTaxonomies() + renderer.set_site(site) + + # Act + tasks = list(renderer._generate_classification_overview(taxonomy, "en")) + + # Assert + action, args = tasks[0]["actions"][0] + context = args[2] + assert context["permalink"] == expected + + +def test_taxonomy_index_path_helper(fixture): + # Arrange + site, _, taxonomy, _, expected = fixture + + # Act + path = site.path(taxonomy.classification_name + "_index", "name", "en", is_link=True) + + # Assert + assert path == expected + + +def test_taxonomy_classifier_index_path(fixture): + # Arrange + site, classifier, taxonomy, append_index, expected = fixture + if append_index: + expected += "index.html" + + # Act + path = classifier._taxonomy_index_path("name", "en", taxonomy) + + # Assert + assert path == [x for x in expected.split('/') if x] + + +def test_taxonomy_overview_path(fixture): + # Arrange + _, _, taxonomy, append_index, expected = fixture + + # Act + result = taxonomy.get_overview_path("en") + + # Assert + assert result == ([x for x in expected.split('/') if x], "always" if append_index else "never") diff --git a/tests/test_plugin_importing.py b/tests/test_plugin_importing.py deleted file mode 100644 index f41a1fd..0000000 --- a/tests/test_plugin_importing.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - -import os -import sys - - -import unittest - - -class ImportPluginsTest(unittest.TestCase): - def test_importing_command_import_wordpress(self): - import nikola.plugins.command.import_wordpress # NOQA - - def test_importing_compile_rest(self): - import nikola.plugins.compile.rest # NOQA - - def test_importing_plugin_compile_markdown(self): - import nikola.plugins.compile.markdown # NOQA diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 0000000..6760ad5 --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,16 @@ +""" +Simple plugin tests. + +More advanced tests should be in a separate module. +""" + + +def test_command_version(): + """Test `nikola version`.""" + from nikola.plugins.command.version import CommandVersion + + CommandVersion().execute() + + +def test_importing_plugin_task_galleries(): + import nikola.plugins.task.galleries # NOQA diff --git a/tests/test_rss_feeds.py b/tests/test_rss_feeds.py index d44cd7b..d976579 100644 --- a/tests/test_rss_feeds.py +++ b/tests/test_rss_feeds.py @@ -1,158 +1,180 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals, absolute_import - import os -import sys - - +import re from collections import defaultdict from io import StringIO -import os -import re -import unittest +from unittest import mock import dateutil.tz +import pytest from lxml import etree -import mock - -from .base import LocaleSupportInTesting -import nikola -fake_conf = defaultdict(str) -fake_conf['TIMEZONE'] = 'UTC' -fake_conf['__tzinfo__'] = dateutil.tz.tzutc() -fake_conf['DEFAULT_LANG'] = 'en' -fake_conf['TRANSLATIONS'] = {'en': ''} -fake_conf['BASE_URL'] = 'http://some.blog/' -fake_conf['BLOG_AUTHOR'] = nikola.nikola.utils.TranslatableSetting('BLOG_AUTHOR', 'Nikola Tesla', ['en']) -fake_conf['TRANSLATIONS_PATTERN'] = '{path}.{lang}.{ext}' - - -class FakeCompiler(object): +from nikola.nikola import Nikola, Post +from nikola.utils import LocaleBorg, TranslatableSetting + + +def test_feed_is_valid(rss_feed_content, rss_schema): + """ + A testcase to check if the generated feed is valid. + + Validation can be tested with W3 FEED Validator that can be found + at http://feedvalidator.org + """ + document = etree.parse(StringIO(rss_feed_content)) + + assert rss_schema.validate(document) + + +@pytest.fixture +def rss_schema(rss_schema_filename): + with open(rss_schema_filename, "r") as rss_schema_file: + xmlschema_doc = etree.parse(rss_schema_file) + + return etree.XMLSchema(xmlschema_doc) + + +@pytest.fixture +def rss_schema_filename(test_dir): + return os.path.join(test_dir, "data", "rss-2_0.xsd") + + +@pytest.mark.parametrize("element", ["guid", "link"]) +def test_feed_items_have_valid_URLs(rss_feed_content, blog_url, element): + """ + The items in the feed need to have valid urls in link and guid. + + As stated by W3 FEED Validator: + * "link must be a full and valid URL" + * "guid must be a full URL, unless isPermaLink attribute is false: /weblog/posts/the-minimal-server.html" + """ + # This validation regex is taken from django.core.validators + url_validation_regex = re.compile( + r"^(?:http|ftp)s?://" # http:// or https:// + # domain... + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" + r"localhost|" # localhost... + # ...or ipv4 + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" + # ...or ipv6 + r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" r"(?::\d+)?" r"(?:/?|[/?]\S+)$", # optional port + re.IGNORECASE, + ) + + def is_valid_URL(url): + return url_validation_regex.match(url) is not None + + et = etree.parse(StringIO(rss_feed_content)) + channel = et.find("channel") + item = channel.find("item") + element = item.find(element) + + assert is_valid_URL(element.text) + assert blog_url in element.text + + +@pytest.fixture(autouse=True) +def localeborg(default_locale): + """ + LocaleBorg with default settings + """ + LocaleBorg.reset() + LocaleBorg.initialize({}, default_locale) + try: + yield + finally: + LocaleBorg.reset() + + +@pytest.fixture +def rss_feed_content(blog_url, config, default_locale): + default_post = { + "title": "post title", + "slug": "awesome_article", + "date": "2012-10-01 22:41", + "author": None, + "tags": "tags", + "link": "link", + "description": "description", + "enclosure": "http://www.example.org/foo.mp3", + "enclosure_length": "5", + } + meta_mock = mock.Mock(return_value=(defaultdict(str, default_post), None)) + with mock.patch("nikola.post.get_meta", meta_mock): + with \ + mock.patch( + "nikola.nikola.utils.os.path.isdir", mock.Mock(return_value=True)), \ + mock.patch( + "nikola.nikola.Post.text", mock.Mock(return_value="some long text") + ): + with mock.patch( + "nikola.post.os.path.isfile", mock.Mock(return_value=True)): + example_post = Post( + "source.file", + config, + "blog_folder", + True, + {"en": ""}, + "post.tmpl", + FakeCompiler(), + ) + + filename = "testfeed.rss" + opener_mock = mock.mock_open() + + with mock.patch("nikola.nikola.io.open", opener_mock, create=True): + Nikola().generic_rss_renderer( + default_locale, + "blog_title", + blog_url, + "blog_description", + [example_post, ], + filename, + True, + False, + ) + + opener_mock.assert_called_once_with(filename, "w+", encoding="utf-8") + + # Python 3 / unicode strings workaround + # lxml will complain if the encoding is specified in the + # xml when running with unicode strings. + # We do not include this in our content. + file_content = [call[1][0] for call in opener_mock.mock_calls[2:-1]][0] + splitted_content = file_content.split("\n") + # encoding_declaration = splitted_content[0] + content_without_encoding_declaration = splitted_content[1:] + yield "\n".join(content_without_encoding_declaration) + + +@pytest.fixture +def config(blog_url, default_locale): + fake_conf = defaultdict(str) + fake_conf["TIMEZONE"] = "UTC" + fake_conf["__tzinfo__"] = dateutil.tz.tzutc() + fake_conf["DEFAULT_LANG"] = default_locale + fake_conf["TRANSLATIONS"] = {default_locale: ""} + fake_conf["BASE_URL"] = blog_url + fake_conf["BLOG_AUTHOR"] = TranslatableSetting( + "BLOG_AUTHOR", "Nikola Tesla", [default_locale] + ) + fake_conf["TRANSLATIONS_PATTERN"] = "{path}.{lang}.{ext}" + + return fake_conf + + +@pytest.fixture +def blog_url(): + return "http://some.blog" + + +class FakeCompiler: demote_headers = False - compile_html = None - extension = lambda self: '.html' - name = "fake" + compile = None + + def extension(self): + return ".html" def read_metadata(*args, **kwargs): return {} def register_extra_dependencies(self, post): pass - - -class RSSFeedTest(unittest.TestCase): - def setUp(self): - LocaleSupportInTesting.initialize_locales_for_testing('unilingual') - self.blog_url = "http://some.blog" - - with mock.patch('nikola.post.get_meta', - mock.Mock(return_value=( - ({'title': 'post title', - 'slug': 'awesome_article', - 'date': '2012-10-01 22:41', - 'author': None, - 'tags': 'tags', - 'link': 'link', - 'description': 'description', - 'enclosure': 'http://www.example.org/foo.mp3', - 'enclosure_length': '5'}, - True) - ))): - with mock.patch('nikola.nikola.utils.os.path.isdir', - mock.Mock(return_value=True)): - with mock.patch('nikola.nikola.Post.text', - mock.Mock(return_value='some long text')): - - example_post = nikola.nikola.Post('source.file', - fake_conf, - 'blog_folder', - True, - {'en': ''}, - 'post.tmpl', - FakeCompiler()) - - opener_mock = mock.mock_open() - - with mock.patch('nikola.nikola.io.open', opener_mock, create=True): - nikola.nikola.Nikola().generic_rss_renderer('en', - "blog_title", - self.blog_url, - "blog_description", - [example_post, - ], - 'testfeed.rss', - True, - False) - - opener_mock.assert_called_once_with( - 'testfeed.rss', 'w+', encoding='utf-8') - - # Python 3 / unicode strings workaround - # lxml will complain if the encoding is specified in the - # xml when running with unicode strings. - # We do not include this in our content. - file_content = [ - call[1][0] - for call in opener_mock.mock_calls[2:-1]][0] - splitted_content = file_content.split('\n') - self.encoding_declaration = splitted_content[0] - content_without_encoding_declaration = splitted_content[1:] - self.file_content = '\n'.join( - content_without_encoding_declaration) - - def tearDown(self): - pass - - def test_feed_items_have_valid_URLs(self): - '''The items in the feed need to have valid urls in link and guid.''' - # This validation regex is taken from django.core.validators - url_validation_regex = re.compile(r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... - r'localhost|' # localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 - r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 - r'(?::\d+)?' # optional port - r'(?:/?|[/?]\S+)$', re.IGNORECASE) - - def is_valid_URL(url): - return url_validation_regex.match(url) is not None - - et = etree.parse(StringIO(self.file_content)) - channel = et.find('channel') - item = channel.find('item') - guid = item.find('guid') - link = item.find('link') - - # As stated by W3 FEED Validator: "link must be a full and valid URL" - self.assertTrue(is_valid_URL(link.text), - 'The following URL is not valid: %s' % link.text) - self.assertTrue(self.blog_url in link.text) - - # "guid must be a full URL, unless isPermaLink attribute - # is false: /weblog/posts/the-minimal-server.html " - self.assertTrue(is_valid_URL(guid.text), - 'The following URL is not valid: %s' % - guid.text) - self.assertTrue(self.blog_url in guid.text) - - def test_feed_is_valid(self): - ''' - A testcase to check if the generated feed is valid. - - Validation can be tested with W3 FEED Validator that can be found - at http://feedvalidator.org - ''' - rss_schema_filename = os.path.join(os.path.dirname(__file__), - 'rss-2_0.xsd') - with open(rss_schema_filename, 'r') as rss_schema_file: - xmlschema_doc = etree.parse(rss_schema_file) - - xmlschema = etree.XMLSchema(xmlschema_doc) - document = etree.parse(StringIO(self.file_content)) - - self.assertTrue(xmlschema.validate(document)) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_rst_compiler.py b/tests/test_rst_compiler.py index 5b33db7..803fd79 100644 --- a/tests/test_rst_compiler.py +++ b/tests/test_rst_compiler.py @@ -1,253 +1,215 @@ -# coding: utf8 -# Author: Rodrigo Bistolfi -# Date: 03/2013 - +""" +Test cases for Nikola ReST extensions. -""" Test cases for Nikola ReST extensions. -A base class ReSTExtensionTestCase provides the tests basic behaviour. -Subclasses must override the "sample" class attribute with the ReST markup. -The sample will be rendered as HTML using publish_parts() by setUp(). +A sample will be rendered by CompileRest. One method is provided for checking the resulting HTML: - * assertHTMLContains(element, attributes=None, text=None) - -The HTML is parsed with lxml for checking against the data you provide. The -method takes an element argument, a string representing the *name* of an HTML -tag, like "script" or "iframe". We will try to find this tag in the document -and perform the tests on it. You can pass a dictionary to the attributes kwarg -representing the name and the value of the tag attributes. The text kwarg takes -a string argument, which will be tested against the contents of the HTML -element. -One last caveat: you need to url unquote your urls if you are going to test -attributes like "src" or "link", since the HTML rendered by docutils will be -always unquoted. + * assert_html_contains(html, element, attributes=None, text=None) """ +from io import StringIO -from __future__ import unicode_literals, absolute_import - -import os -import sys - - -import io -try: - from io import StringIO -except ImportError: - from StringIO import StringIO # NOQA -import tempfile - -import docutils -from lxml import html import pytest -import unittest +from lxml import html as lxml_html import nikola.plugins.compile.rest -from nikola.plugins.compile.rest import vimeo import nikola.plugins.compile.rest.listing -from nikola.plugins.compile.rest.doc import Plugin as DocPlugin -from nikola.utils import _reload -from .base import BaseTestCase, FakeSite, FakePost - - -class ReSTExtensionTestCase(BaseTestCase): - """ Base class for testing ReST extensions """ - - sample = 'foo' - deps = None - - def setUp(self): - self.compiler = nikola.plugins.compile.rest.CompileRest() - self.compiler.set_site(FakeSite()) - return super(ReSTExtensionTestCase, self).setUp() - - def basic_test(self): - """ Parse cls.sample into a HTML document tree """ - self.setHtmlFromRst(self.sample) - - def setHtmlFromRst(self, rst): - """ Create html output from rst string """ - tmpdir = tempfile.mkdtemp() - inf = os.path.join(tmpdir, 'inf') - outf = os.path.join(tmpdir, 'outf') - with io.open(inf, 'w+', encoding='utf8') as f: - f.write(rst) - p = FakePost('', '') - p._depfile[outf] = [] - self.compiler.site.post_per_input_file[inf] = p - self.html = self.compiler.compile_html(inf, outf) - with io.open(outf, 'r', encoding='utf8') as f: - self.html = f.read() - os.unlink(inf) - os.unlink(outf) - depfile = [p for p in p._depfile[outf] if p != outf] - depfile = '\n'.join(depfile) - if depfile: - self.assertEqual(self.deps.strip(), depfile) - os.rmdir(tmpdir) - self.html_doc = html.parse(StringIO(self.html)) - - def assertHTMLContains(self, element, attributes=None, text=None): - """ Test if HTML document includes an element with the given - attributes and text content - - """ - try: - tag = next(self.html_doc.iter(element)) - except StopIteration: - raise Exception("<{0}> not in {1}".format(element, self.html)) - else: - if attributes: - arg_attrs = set(attributes.items()) - tag_attrs = set(tag.items()) - self.assertTrue(arg_attrs.issubset(tag_attrs)) - if text: - self.assertIn(text, tag.text) - - -class ReSTExtensionTestCaseTestCase(ReSTExtensionTestCase): - """ Simple test for our base class :) """ - - sample = '.. raw:: html\n\n <iframe src="foo" height="bar">spam</iframe>' - - def test_test(self): - self.basic_test() - self.assertHTMLContains("iframe", attributes={"src": "foo"}, - text="spam") - self.assertRaises(Exception, self.assertHTMLContains, "eggs", {}) - - -class MathTestCase(ReSTExtensionTestCase): - sample = ':math:`e^{ix} = \cos x + i\sin x`' - - def test_math(self): - """ Test that math is outputting TeX code.""" - self.basic_test() - self.assertHTMLContains("span", attributes={"class": "math"}, - text="\(e^{ix} = \cos x + i\sin x\)") - +from nikola.plugins.compile.rest import vimeo +from nikola.utils import _reload, LocaleBorg -class SlidesTestCase(ReSTExtensionTestCase): - """ Slides test case """ +from .helper import FakeSite - sample = '.. slides:: IMG.jpg\n' - def test_slides(self): - """ Test the slides js generation and img tag creation """ - self.basic_test() - self.assertHTMLContains("img", attributes={"src": "IMG.jpg"}) +def test_ReST_extension(): + sample = '.. raw:: html\n\n <iframe src="foo" height="bar">spam</iframe>' + html = get_html_from_rst(sample) + + assert_html_contains(html, "iframe", attributes={"src": "foo"}, text="spam") + + with pytest.raises(Exception): + assert_html_contains("eggs", {}) + + +def test_math_extension_outputs_tex(): + """Test that math is outputting TeX code.""" + sample = r":math:`e^{ix} = \cos x + i\sin x`" + html = get_html_from_rst(sample) + + assert_html_contains( + html, + "span", + attributes={"class": "math"}, + text=r"\(e^{ix} = \cos x + i\sin x\)", + ) + + +def test_soundcloud_iframe(): + """Test SoundCloud iframe tag generation""" + + sample = ".. soundcloud:: SID\n :height: 400\n :width: 600" + html = get_html_from_rst(sample) + assert_html_contains( + html, + "iframe", + attributes={ + "src": ( + "https://w.soundcloud.com/player/" + "?url=http://api.soundcloud.com/" + "tracks/SID" + ), + "height": "400", + "width": "600", + }, + ) + + +def test_youtube_iframe(): + """Test Youtube iframe tag generation""" + + sample = ".. youtube:: YID\n :height: 400\n :width: 600" + html = get_html_from_rst(sample) + assert_html_contains( + html, + "iframe", + attributes={ + "src": ( + "https://www.youtube-nocookie.com" + "/embed/YID?rel=0&" + "wmode=transparent" + ), + "height": "400", + "width": "600", + "frameborder": "0", + "allowfullscreen": "", + "allow": "encrypted-media", + }, + ) + + +def test_vimeo(disable_vimeo_api_query): + """Test Vimeo iframe tag generation""" + + sample = ".. vimeo:: VID\n :height: 400\n :width: 600" + html = get_html_from_rst(sample) + assert_html_contains( + html, + "iframe", + attributes={ + "src": ("https://player.vimeo.com/" "video/VID"), + "height": "400", + "width": "600", + }, + ) + + +@pytest.mark.parametrize( + "sample", + [ + ".. code-block:: python\n\n import antigravity", + ".. sourcecode:: python\n\n import antigravity", + ], +) +def test_rendering_codeblock_alias(sample): + """Test CodeBlock aliases""" + get_html_from_rst(sample) + + +def test_doc_doesnt_exist(): + with pytest.raises(Exception): + assert_html_contains("anything", {}) + + +def test_doc(): + sample = "Sample for testing my :doc:`fake-post`" + html = get_html_from_rst(sample) + assert_html_contains( + html, "a", text="Fake post", attributes={"href": "/posts/fake-post"} + ) + + +def test_doc_titled(): + sample = "Sample for testing my :doc:`titled post <fake-post>`" + html = get_html_from_rst(sample) + assert_html_contains( + html, "a", text="titled post", attributes={"href": "/posts/fake-post"} + ) + + +@pytest.fixture(autouse=True, scope="module") +def localeborg_base(): + """A base config of LocaleBorg.""" + LocaleBorg.reset() + assert not LocaleBorg.initialized + LocaleBorg.initialize({}, "en") + assert LocaleBorg.initialized + assert LocaleBorg().current_lang == "en" + try: + yield + finally: + LocaleBorg.reset() + assert not LocaleBorg.initialized + + +def get_html_from_rst(rst): + """Create html output from rst string""" + + compiler = nikola.plugins.compile.rest.CompileRest() + compiler.set_site(FakeSite()) + return compiler.compile_string(rst)[0] + + +class FakePost: + def __init__(self, outfile): + self._depfile = {outfile: []} + + +def assert_html_contains(html, element, attributes=None, text=None): + """ + Test if HTML document includes an element with the given attributes + and text content. + + The HTML is parsed with lxml for checking against the data you + provide. The method takes an element argument, a string representing + the *name* of an HTML tag, like "script" or "iframe". + We will try to find this tag in the document and perform the tests + on it. You can pass a dictionary to the attributes kwarg + representing the name and the value of the tag attributes. The text + kwarg takes a string argument, which will be tested against the + contents of the HTML element. + + One last caveat: you need to url unquote your urls if you are going + to test attributes like "src" or "link", since the HTML rendered by + docutils will be always unquoted. + """ + html_doc = lxml_html.parse(StringIO(html)) + try: + tag = next(html_doc.iter(element)) + except StopIteration: + raise Exception("<{0}> not in {1}".format(element, html)) -class SoundCloudTestCase(ReSTExtensionTestCase): - """ SoundCloud test case """ + if attributes: + arg_attrs = set(attributes.items()) + tag_attrs = set(tag.items()) + assert arg_attrs.issubset(tag_attrs) - sample = '.. soundcloud:: SID\n :height: 400\n :width: 600' + if text: + assert text in tag.text - def test_soundcloud(self): - """ Test SoundCloud iframe tag generation """ - self.basic_test() - self.assertHTMLContains("iframe", - attributes={"src": ("https://w.soundcloud.com" - "/player/?url=http://" - "api.soundcloud.com/" - "tracks/SID"), - "height": "400", "width": "600"}) +@pytest.fixture +def disable_vimeo_api_query(): + """ + Disable query of the vimeo api over the wire -class VimeoTestCase(ReSTExtensionTestCase): - """Vimeo test. Set Vimeo.request_size to False for avoiding querying the Vimeo api - over the network - + over the network. """ - sample = '.. vimeo:: VID\n :height: 400\n :width: 600' - - def setUp(self): - """ Disable query of the vimeo api over the wire """ - vimeo.Vimeo.request_size = False - super(VimeoTestCase, self).setUp() + before = vimeo.Vimeo.request_size + vimeo.Vimeo.request_size = False + try: _reload(nikola.plugins.compile.rest) - - def test_vimeo(self): - """ Test Vimeo iframe tag generation """ - self.basic_test() - self.assertHTMLContains("iframe", - attributes={"src": ("https://player.vimeo.com/" - "video/VID"), - "height": "400", "width": "600"}) - - -class YoutubeTestCase(ReSTExtensionTestCase): - """ Youtube test case """ - - sample = '.. youtube:: YID\n :height: 400\n :width: 600' - - def test_youtube(self): - """ Test Youtube iframe tag generation """ - self.basic_test() - self.assertHTMLContains("iframe", - attributes={"src": ("https://www.youtube.com/" - "embed/YID?rel=0&hd=1&" - "wmode=transparent"), - "height": "400", "width": "600"}) - - -class ListingTestCase(ReSTExtensionTestCase): - """ Listing test case and CodeBlock alias tests """ - - deps = None - sample1 = '.. listing:: nikola.py python\n\n' - sample2 = '.. code-block:: python\n\n import antigravity' - sample3 = '.. sourcecode:: python\n\n import antigravity' - - # def test_listing(self): - # """ Test that we can render a file object contents without errors """ - # with cd(os.path.dirname(__file__)): - # self.deps = 'listings/nikola.py' - # self.setHtmlFromRst(self.sample1) - - def test_codeblock_alias(self): - """ Test CodeBlock aliases """ - self.deps = None - self.setHtmlFromRst(self.sample2) - self.setHtmlFromRst(self.sample3) - - -class DocTestCase(ReSTExtensionTestCase): - """ Ref role test case """ - - sample = 'Sample for testing my :doc:`doesnt-exist-post`' - sample1 = 'Sample for testing my :doc:`fake-post`' - sample2 = 'Sample for testing my :doc:`titled post <fake-post>`' - - def setUp(self): - # Initialize plugin, register role - self.plugin = DocPlugin() - self.plugin.set_site(FakeSite()) - # Hack to fix leaked state from integration tests - try: - f = docutils.parsers.rst.roles.role('doc', None, None, None)[0] - f.site = FakeSite() - except AttributeError: - pass - return super(DocTestCase, self).setUp() - - def test_doc_doesnt_exist(self): - self.assertRaises(Exception, self.assertHTMLContains, 'anything', {}) - - def test_doc(self): - self.setHtmlFromRst(self.sample1) - self.assertHTMLContains('a', - text='Fake post', - attributes={'href': '/posts/fake-post'}) - - def test_doc_titled(self): - self.setHtmlFromRst(self.sample2) - self.assertHTMLContains('a', - text='titled post', - attributes={'href': '/posts/fake-post'}) - - -if __name__ == "__main__": - unittest.main() + yield + finally: + vimeo.Vimeo.request_size = before diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py index fddb4ff..af89afa 100644 --- a/tests/test_scheduling.py +++ b/tests/test_scheduling.py @@ -1,132 +1,137 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import +""" +Scheduling tests. + +These tests rely on a fixed time to work. +In order to achieve this the fixture for `now` sets the expected time. +""" import datetime -import locale -import os -import sys import dateutil.parser import dateutil.tz import pytest -from .base import BaseTestCase - -try: - from freezegun import freeze_time - _freeze_time = True -except ImportError: - _freeze_time = False - freeze_time = lambda x: lambda y: y - -_NOW = datetime.datetime( # Thursday - 2013, 8, 22, 10, 0, 0, tzinfo=dateutil.tz.tzutc()) - - -@pytest.mark.skipif(not _freeze_time, reason="freezegun not installed.") -class TestScheduling(BaseTestCase): - - @classmethod - def setUp(cls): - d = [name for name in sys.modules if name.startswith("six.moves.")] - cls.deleted = {} - for name in d: - cls.deleted[name] = sys.modules[name] - del sys.modules[name] - - @classmethod - def tearDown(cls): - for name, mod in cls.deleted.items(): - sys.modules[name] = mod - - @freeze_time(_NOW) - def test_get_date(self): - from nikola.plugins.command.new_post import get_date - - FMT = '%Y-%m-%d %H:%M:%S %Z' - NOW = _NOW.strftime(FMT) - TODAY = dateutil.parser.parse(NOW) - RULE_TH = 'RRULE:FREQ=WEEKLY;BYDAY=TH' - RULE_FR = 'RRULE:FREQ=WEEKLY;BYDAY=FR' - UTC = dateutil.tz.tzutc() - - # NOW does not match rule ######################################### - # No last date - expected = TODAY.replace(day=23).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_FR, tz=UTC)) - self.assertEqual(expected, get_date(True, RULE_FR, tz=UTC)) - - # Last date in the past; doesn't match rule - date = TODAY.replace(hour=7) - expected = TODAY.replace(day=23, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_FR, date, tz=UTC)) - - # Last date in the future; doesn't match rule - date = TODAY.replace(day=24, hour=7) - expected = TODAY.replace(day=30, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_FR, date, tz=UTC)) - - # Last date in the past; matches rule - date = TODAY.replace(day=16, hour=8) - expected = TODAY.replace(day=23, hour=8).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_FR, date, tz=UTC)) - - # Last date in the future; matches rule - date = TODAY.replace(day=23, hour=18) - expected = TODAY.replace(day=30, hour=18).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_FR, date, tz=UTC)) - - # NOW matches rule ################################################ - # Not scheduling should return NOW - self.assertEqual(NOW, get_date(False, RULE_TH, tz=UTC)) - # No last date - self.assertEqual(NOW, get_date(True, RULE_TH, tz=UTC)) - self.assertEqual(NOW, get_date(True, RULE_TH, tz=UTC)) - - # Last date in the past; doesn't match rule - # Corresponding time has already passed, today - date = TODAY.replace(day=21, hour=7) - expected = TODAY.replace(day=29, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - # Corresponding time has not passed today - date = TODAY.replace(day=21, hour=18) - expected = TODAY.replace(day=22, hour=18).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - - # Last date in the future; doesn't match rule - # Corresponding time has already passed, today - date = TODAY.replace(day=24, hour=7) - expected = TODAY.replace(day=29, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - # Corresponding time has not passed today - date = TODAY.replace(day=24, hour=18) - expected = TODAY.replace(day=29, hour=18).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - - # Last date in the past; matches rule - # Corresponding time has already passed, today - date = TODAY.replace(day=15, hour=7) - expected = TODAY.replace(day=29, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - # Corresponding time has already passed, today; rule specifies HOUR - date = TODAY.replace(day=15, hour=7) - expected = TODAY.replace(day=29, hour=9).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH + ';BYHOUR=9', date, tz=UTC)) - # Corresponding time has not passed today - date = TODAY.replace(day=15, hour=18) - expected = TODAY.replace(day=22, hour=18).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - - # Last date in the future; matches rule - # Corresponding time has already passed, today - date = TODAY.replace(day=29, hour=7) - expected = TODAY.replace(day=5, month=9, hour=7).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - # Corresponding time has not passed today - date = TODAY.replace(day=22, hour=18) - expected = TODAY.replace(day=29, hour=18).strftime(FMT) - self.assertEqual(expected, get_date(True, RULE_TH, date, tz=UTC)) - -if __name__ == '__main__': - import unittest - unittest.main() +from nikola.plugins.command.new_post import get_date + +freezegun = pytest.importorskip("freezegun") +freeze_time = freezegun.freeze_time + +UTC = dateutil.tz.tzutc() +RULE_THURSDAYS = "RRULE:FREQ=WEEKLY;BYDAY=TH" +RULE_FRIDAYS = "RRULE:FREQ=WEEKLY;BYDAY=FR" + + +def test_current_time_not_matching_rule(today): + """`today` does not match rule.""" + # No last date + expected = today.replace(day=23) + assert expected == get_date(True, RULE_FRIDAYS, tz=UTC)[1] + assert expected == get_date(True, RULE_FRIDAYS, tz=UTC)[1] + + # Last date in the past; doesn't match rule + date = today.replace(hour=7) + expected = today.replace(day=23, hour=7) + assert expected == get_date(True, RULE_FRIDAYS, date, tz=UTC)[1] + + # Last date in the future; doesn't match rule + date = today.replace(day=24, hour=7) + expected = today.replace(day=30, hour=7) + assert expected == get_date(True, RULE_FRIDAYS, date, tz=UTC)[1] + + +def test_current_time_matching_rule(today): + # Last date in the past; matches rule + date = today.replace(day=16, hour=8) + expected = today.replace(day=23, hour=8) + assert expected == get_date(True, RULE_FRIDAYS, date, tz=UTC)[1] + + # Last date in the future; matches rule + date = today.replace(day=23, hour=18) + expected = today.replace(day=30, hour=18) + assert expected == get_date(True, RULE_FRIDAYS, date, tz=UTC)[1] + + +@pytest.mark.parametrize("scheduling", [True, False]) +def test_current_time_matching_rule_no_given_date(now, scheduling): + """ + No last date given means we should always get the current time. + + `now` matches the rule. + """ + assert now == get_date(scheduling, RULE_THURSDAYS, tz=UTC)[1] + + +def test_last_date_in_the_past_not_matching_rule(today): + """Last date in the past; doesn't match rule.""" + # Corresponding time has already passed, today + date = today.replace(day=21, hour=7) + expected = today.replace(day=29, hour=7) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + # Corresponding time has not passed today + date = today.replace(day=21, hour=18) + expected = today.replace(day=22, hour=18) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + +def test_last_date_in_the_future_not_matching_rule(today): + """Last date in the future; doesn't match rule.""" + # Corresponding time has already passed, today + date = today.replace(day=24, hour=7) + expected = today.replace(day=29, hour=7) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + # Corresponding time has not passed today + date = today.replace(day=24, hour=18) + expected = today.replace(day=29, hour=18) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + +def test_last_date_in_the_past_matching_rule(today): + """Last date in the past; matches rule.""" + # Corresponding time has already passed, today + date = today.replace(day=15, hour=7) + expected = today.replace(day=29, hour=7) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + # Corresponding time has already passed, today; rule specifies HOUR + date = today.replace(day=15, hour=7) + expected = today.replace(day=29, hour=9) + assert expected == get_date(True, RULE_THURSDAYS + ";BYHOUR=9", date, tz=UTC)[1] + + # Corresponding time has not passed today + date = today.replace(day=15, hour=18) + expected = today.replace(day=22, hour=18) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + +def test_last_date_in_the_future_matching_rule(today): + """Last date in the future; matches rule.""" + # Corresponding time has already passed, today + date = today.replace(day=29, hour=7) + expected = today.replace(day=5, month=9, hour=7) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + # Corresponding time has not passed today + date = today.replace(day=22, hour=18) + expected = today.replace(day=29, hour=18) + assert expected == get_date(True, RULE_THURSDAYS, date, tz=UTC)[1] + + +@pytest.fixture +def today(now): + current_time = now.strftime("%Y-%m-%d %H:%M:%S %Z") + yield dateutil.parser.parse(current_time) + + +@pytest.fixture +def now() -> datetime: + """ + Get the current time. + + datetime is frozen to this point in time. + """ + _NOW = datetime.datetime(2013, 8, 22, 10, 0, 0, tzinfo=UTC) # Thursday + + with freeze_time(_NOW): + yield _NOW diff --git a/tests/test_shortcodes.py b/tests/test_shortcodes.py index 22f603e..8383176 100644 --- a/tests/test_shortcodes.py +++ b/tests/test_shortcodes.py @@ -1,76 +1,200 @@ -# -*- coding: utf-8 -*- -# vim: set wrap textwidth=300 +"""Test shortcodes.""" -u"""Test shortcodes.""" - -from __future__ import unicode_literals import pytest + +import nikola.utils from nikola import shortcodes -from .base import FakeSite, BaseTestCase -import sys -def noargs(site, data='', lang=''): - return "noargs {0} success!".format(data) -def arg(*args, **kwargs): - # don’t clutter the kwargs dict - _ = kwargs.pop('site') - data = kwargs.pop('data') - lang = kwargs.pop('lang') - # TODO hack for Python 2.7 -- remove when possible - if sys.version_info[0] == 2: - args = tuple(i.encode('utf-8') for i in args) - kwargs = {k.encode('utf-8'): v.encode('utf-8') for k, v in kwargs.items()} - return "arg {0}/{1}/{2}".format(args, sorted(kwargs.items()), data) +@pytest.mark.parametrize( + "template, expected_result", + [ + ("test({{% noargs %}})", "test(noargs success!)"), + ( + "test({{% noargs %}}\\hello world/{{% /noargs %}})", + "test(noargs \\hello world/ success!)", + ), + ], +) +def test_noargs(site, template, expected_result): + applied_shortcode = shortcodes.apply_shortcodes(template, site.shortcode_registry)[0] + assert applied_shortcode == expected_result + + +@pytest.mark.parametrize( + "template, expected_result", + [ + ("test({{% arg 1 %}})", "test(arg ('1',)/[]/)"), + ("test({{% arg 1 2aa %}})", "test(arg ('1', '2aa')/[]/)"), + ('test({{% arg "hello world" %}})', "test(arg ('hello world',)/[]/)"), + ("test({{% arg back\\ slash arg2 %}})", "test(arg ('back slash', 'arg2')/[]/)"), + ('test({{% arg "%}}" %}})', "test(arg ('%}}',)/[]/)"), + ], +) +def test_positional_arguments(site, template, expected_result): + applied_shortcode = shortcodes.apply_shortcodes(template, site.shortcode_registry)[0] + assert applied_shortcode == expected_result + + +@pytest.mark.parametrize( + "template, expected_result", + [ + ("test({{% arg 1a=2b %}})", "test(arg ()/[('1a', '2b')]/)"), + ( + 'test({{% arg 1a="2b 3c" 4d=5f %}})', + "test(arg ()/[('1a', '2b 3c'), ('4d', '5f')]/)", + ), + ( + 'test({{% arg 1a="2b 3c" 4d=5f back=slash\\ slash %}})', + "test(arg ()/[('1a', '2b 3c'), ('4d', '5f'), ('back', 'slash slash')]/)", + ), + ], +) +def test_arg_keyword(site, template, expected_result): + applied_shortcode = shortcodes.apply_shortcodes(template, site.shortcode_registry)[0] + assert applied_shortcode == expected_result + + +@pytest.mark.parametrize( + "template, expected_result", + [ + ("test({{% arg 123 %}}Hello!{{% /arg %}})", "test(arg ('123',)/[]/Hello!)"), + ( + "test({{% arg 123 456 foo=bar %}}Hello world!{{% /arg %}})", + "test(arg ('123', '456')/[('foo', 'bar')]/Hello world!)", + ), + ( + 'test({{% arg 123 456 foo=bar baz="quotes rock." %}}Hello test suite!{{% /arg %}})', + "test(arg ('123', '456')/[('baz', 'quotes rock.'), ('foo', 'bar')]/Hello test suite!)", + ), + ( + 'test({{% arg "123 foo" foobar foo=bar baz="quotes rock." %}}Hello test suite!!{{% /arg %}})', + "test(arg ('123 foo', 'foobar')/[('baz', 'quotes rock.'), ('foo', 'bar')]/Hello test suite!!)", + ), + ], +) +def test_data(site, template, expected_result): + applied_shortcode = shortcodes.apply_shortcodes(template, site.shortcode_registry)[0] + assert applied_shortcode == expected_result + + +@pytest.mark.parametrize( + "template, expected_error_pattern", + [ + ( + "{{% start", + "^Shortcode 'start' starting at .* is not terminated correctly with '%}}'!", + ), + ( + "{{% wrong ending %%}", + "^Syntax error in shortcode 'wrong' at .*: expecting whitespace!", + ), + ( + "{{% start %}} {{% /end %}}", + "^Found shortcode ending '{{% /end %}}' which isn't closing a started shortcode", + ), + ('{{% start "asdf %}}', "^Unexpected end of unquoted string"), + ("{{% start =b %}}", "^String starting at .* must be non-empty!"), + ('{{% start "a\\', "^Unexpected end of data while escaping"), + ("{{% start a\\", "^Unexpected end of data while escaping"), + ('{{% start a"b" %}}', "^Unexpected quotation mark in unquoted string"), + ( + '{{% start "a"b %}}', + "^Syntax error in shortcode 'start' at .*: expecting whitespace!", + ), + ("{{% %}}", "^Syntax error: '{{%' must be followed by shortcode name"), + ("{{%", "^Syntax error: '{{%' must be followed by shortcode name"), + ("{{% ", "^Syntax error: '{{%' must be followed by shortcode name"), + ( + "{{% / %}}", + "^Found shortcode ending '{{% / %}}' which isn't closing a started shortcode", + ), + ("{{% / a %}}", "^Syntax error: '{{% /' must be followed by ' %}}'"), + ( + "==> {{% <==", + "^Shortcode '<==' starting at .* is not terminated correctly with '%}}'!", + ), + ], +) +def test_errors(site, template, expected_error_pattern): + with pytest.raises(shortcodes.ParsingError, match=expected_error_pattern): + shortcodes.apply_shortcodes( + template, site.shortcode_registry, raise_exceptions=True + ) + + +@pytest.mark.parametrize( + "input, expected", + [ + ("{{% foo %}}", (u"SC1", {u"SC1": u"{{% foo %}}"})), + ( + "{{% foo %}} bar {{% /foo %}}", + (u"SC1", {u"SC1": u"{{% foo %}} bar {{% /foo %}}"}), + ), + ( + "AAA{{% foo %}} bar {{% /foo %}}BBB", + (u"AAASC1BBB", {u"SC1": u"{{% foo %}} bar {{% /foo %}}"}), + ), + ( + "AAA{{% foo %}} {{% bar %}} {{% /foo %}}BBB", + (u"AAASC1BBB", {u"SC1": u"{{% foo %}} {{% bar %}} {{% /foo %}}"}), + ), + ( + "AAA{{% foo %}} {{% /bar %}} {{% /foo %}}BBB", + (u"AAASC1BBB", {u"SC1": u"{{% foo %}} {{% /bar %}} {{% /foo %}}"}), + ), + ( + "AAA{{% foo %}} {{% bar %}} quux {{% /bar %}} {{% /foo %}}BBB", + ( + u"AAASC1BBB", + {u"SC1": u"{{% foo %}} {{% bar %}} quux {{% /bar %}} {{% /foo %}}"}, + ), + ), + ( + "AAA{{% foo %}} BBB {{% bar %}} quux {{% /bar %}} CCC", + ( + u"AAASC1 BBB SC2 CCC", + {u"SC1": u"{{% foo %}}", u"SC2": u"{{% bar %}} quux {{% /bar %}}"}, + ), + ), + ], +) +def test_extract_shortcodes(input, expected, monkeypatch): + i = iter("SC%d" % i for i in range(1, 100)) + monkeypatch.setattr(shortcodes, "_new_sc_id", i.__next__) + extracted = shortcodes.extract_shortcodes(input) + assert extracted == expected @pytest.fixture(scope="module") -def fakesite(): - s = FakeSite() - s.register_shortcode('noargs', noargs) - s.register_shortcode('arg', arg) +def site(): + s = FakeSiteWithShortcodeRegistry() + s.register_shortcode("noargs", noargs) + s.register_shortcode("arg", arg) return s -def test_noargs(fakesite): - assert shortcodes.apply_shortcodes('test({{% noargs %}})', fakesite.shortcode_registry) == 'test(noargs success!)' - assert shortcodes.apply_shortcodes('test({{% noargs %}}\\hello world/{{% /noargs %}})', fakesite.shortcode_registry) == 'test(noargs \\hello world/ success!)' - -def test_arg_pos(fakesite): - assert shortcodes.apply_shortcodes('test({{% arg 1 %}})', fakesite.shortcode_registry) == "test(arg ('1',)/[]/)" - assert shortcodes.apply_shortcodes('test({{% arg 1 2aa %}})', fakesite.shortcode_registry) == "test(arg ('1', '2aa')/[]/)" - assert shortcodes.apply_shortcodes('test({{% arg "hello world" %}})', fakesite.shortcode_registry) == "test(arg ('hello world',)/[]/)" - assert shortcodes.apply_shortcodes('test({{% arg back\ slash arg2 %}})', fakesite.shortcode_registry) == "test(arg ('back slash', 'arg2')/[]/)" - assert shortcodes.apply_shortcodes('test({{% arg "%}}" %}})', fakesite.shortcode_registry) == "test(arg ('%}}',)/[]/)" - -def test_arg_keyword(fakesite): - assert shortcodes.apply_shortcodes('test({{% arg 1a=2b %}})', fakesite.shortcode_registry) == "test(arg ()/[('1a', '2b')]/)" - assert shortcodes.apply_shortcodes('test({{% arg 1a="2b 3c" 4d=5f %}})', fakesite.shortcode_registry) == "test(arg ()/[('1a', '2b 3c'), ('4d', '5f')]/)" - assert shortcodes.apply_shortcodes('test({{% arg 1a="2b 3c" 4d=5f back=slash\ slash %}})', fakesite.shortcode_registry) == "test(arg ()/[('1a', '2b 3c'), ('4d', '5f'), ('back', 'slash slash')]/)" - -def test_data(fakesite): - assert shortcodes.apply_shortcodes('test({{% arg 123 %}}Hello!{{% /arg %}})', fakesite.shortcode_registry) == "test(arg ('123',)/[]/Hello!)" - assert shortcodes.apply_shortcodes('test({{% arg 123 456 foo=bar %}}Hello world!{{% /arg %}})', fakesite.shortcode_registry) == "test(arg ('123', '456')/[('foo', 'bar')]/Hello world!)" - assert shortcodes.apply_shortcodes('test({{% arg 123 456 foo=bar baz="quotes rock." %}}Hello test suite!{{% /arg %}})', fakesite.shortcode_registry) == "test(arg ('123', '456')/[('baz', 'quotes rock.'), ('foo', 'bar')]/Hello test suite!)" - assert shortcodes.apply_shortcodes('test({{% arg "123 foo" foobar foo=bar baz="quotes rock." %}}Hello test suite!!{{% /arg %}})', fakesite.shortcode_registry) == "test(arg ('123 foo', 'foobar')/[('baz', 'quotes rock.'), ('foo', 'bar')]/Hello test suite!!)" - - -class TestErrors(BaseTestCase): - def setUp(self): - self.fakesite = fakesite() - - def test_errors(self): - self.assertRaisesRegexp(shortcodes.ParsingError, "^Shortcode 'start' starting at .* is not terminated correctly with '%}}'!", shortcodes.apply_shortcodes, '{{% start', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error in shortcode 'wrong' at .*: expecting whitespace!", shortcodes.apply_shortcodes, '{{% wrong ending %%}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Found shortcode ending '{{% /end %}}' which isn't closing a started shortcode", shortcodes.apply_shortcodes, '{{% start %}} {{% /end %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Unexpected end of unquoted string", shortcodes.apply_shortcodes, '{{% start "asdf %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^String starting at .* must be non-empty!", shortcodes.apply_shortcodes, '{{% start =b %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Unexpected end of data while escaping", shortcodes.apply_shortcodes, '{{% start "a\\', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Unexpected end of data while escaping", shortcodes.apply_shortcodes, '{{% start a\\', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Unexpected quotation mark in unquoted string", shortcodes.apply_shortcodes, '{{% start a"b" %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error in shortcode 'start' at .*: expecting whitespace!", shortcodes.apply_shortcodes, '{{% start "a"b %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error: '{{%' must be followed by shortcode name", shortcodes.apply_shortcodes, '{{% %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error: '{{%' must be followed by shortcode name", shortcodes.apply_shortcodes, '{{%', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error: '{{%' must be followed by shortcode name", shortcodes.apply_shortcodes, '{{% ', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Found shortcode ending '{{% / %}}' which isn't closing a started shortcode", shortcodes.apply_shortcodes, '{{% / %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Syntax error: '{{% /' must be followed by ' %}}'", shortcodes.apply_shortcodes, '{{% / a %}}', self.fakesite.shortcode_registry, raise_exceptions=True) - self.assertRaisesRegexp(shortcodes.ParsingError, "^Shortcode '<==' starting at .* is not terminated correctly with '%}}'!", shortcodes.apply_shortcodes, '==> {{% <==', self.fakesite.shortcode_registry, raise_exceptions=True) + +class FakeSiteWithShortcodeRegistry: + def __init__(self): + self.shortcode_registry = {} + self.debug = True + + # this code duplicated in nikola/nikola.py + def register_shortcode(self, name, f): + """Register function f to handle shortcode "name".""" + if name in self.shortcode_registry: + nikola.utils.LOGGER.warn("Shortcode name conflict: %s", name) + return + self.shortcode_registry[name] = f + + +def noargs(site, data="", lang=""): + return "noargs {0} success!".format(data) + + +def arg(*args, **kwargs): + # don’t clutter the kwargs dict + kwargs.pop("site") + data = kwargs.pop("data") + kwargs.pop("lang") + return "arg {0}/{1}/{2}".format(args, sorted(kwargs.items()), data) diff --git a/tests/test_slugify.py b/tests/test_slugify.py index c910aaa..a60896f 100644 --- a/tests/test_slugify.py +++ b/tests/test_slugify.py @@ -1,65 +1,73 @@ -# -*- coding: utf-8 -*- +"""Test slugify.""" -u"""Test slugify.""" +import pytest -from __future__ import unicode_literals import nikola.utils -def test_ascii(): - """Test an ASCII-only string.""" - o = nikola.utils.slugify(u'hello', lang='en') - assert o == u'hello' - assert isinstance(o, nikola.utils.unicode_str) - - -def test_ascii_dash(): - """Test an ASCII string, with dashes.""" - o = nikola.utils.slugify(u'hello-world', lang='en') - assert o == u'hello-world' - assert isinstance(o, nikola.utils.unicode_str) - - -def test_ascii_fancy(): - """Test an ASCII string, with fancy characters.""" - o = nikola.utils.slugify(u'The quick brown fox jumps over the lazy dog!-123.456', lang='en') - assert o == u'the-quick-brown-fox-jumps-over-the-lazy-dog-123456' - assert isinstance(o, nikola.utils.unicode_str) - - -def test_pl(): - """Test a string with Polish diacritical characters.""" - o = nikola.utils.slugify(u'zażółćgęśląjaźń', lang='pl') - assert o == u'zazolcgeslajazn' - assert isinstance(o, nikola.utils.unicode_str) - - -def test_pl_dash(): - """Test a string with Polish diacritical characters and dashes.""" - o = nikola.utils.slugify(u'zażółć-gęślą-jaźń', lang='pl') - assert o == u'zazolc-gesla-jazn' - - -def test_pl_fancy(): - """Test a string with Polish diacritical characters and fancy characters.""" - o = nikola.utils.slugify(u'Zażółć gęślą jaźń!-123.456', lang='pl') - assert o == u'zazolc-gesla-jazn-123456' - assert isinstance(o, nikola.utils.unicode_str) - - -def test_disarmed(): +@pytest.mark.parametrize( + "title, language, expected_slug", + [ + pytest.param("hello", "en", "hello", id="ASCII"), + pytest.param("hello-world", "en", "hello-world", id="ASCII with dashes"), + pytest.param("hello world", "en", "hello-world", id="ASCII two words"), + pytest.param("Hello World", "en", "hello-world", id="ASCII uppercase"), + pytest.param( + "The quick brown fox jumps over the lazy dog!-123.456", + "en", + "the-quick-brown-fox-jumps-over-the-lazy-dog-123456", + id="ASCII with fancy characters", + ), + pytest.param( + "zażółćgęśląjaźń", + "pl", + "zazolcgeslajazn", + id="Polish diacritical characters", + ), + pytest.param( + "zażółć-gęślą-jaźń", + "pl", + "zazolc-gesla-jazn", + id="Polish diacritical characters and dashes", + ), + pytest.param( + "Zażółć gęślą jaźń!-123.456", + "pl", + "zazolc-gesla-jazn-123456", + id="Polish diacritical characters and fancy characters", + ), + ], +) +def test_slugify(title, language, expected_slug): + o = nikola.utils.slugify(title, lang=language) + assert o == expected_slug + assert isinstance(o, str) + + +@pytest.mark.parametrize( + "title, expected_slug", + [ + pytest.param( + u"Zażółć gęślą jaźń!-123.456", u"Zażółć gęślą jaźń!-123.456", id="polish" + ), + pytest.param( + u'Zażółć gęślą jaźń!-123.456 "Hello World"?#H<e>l/l\\o:W\'o\rr*l\td|!\n', + u"Zażółć gęślą jaźń!-123.456 -Hello World---H-e-l-l-o-W-o-r-l-d-!-", + id="polish with banned characters", + ), + ], +) +def test_disarmed(disarm_slugify, title, expected_slug): """Test disarmed slugify.""" - nikola.utils.USE_SLUGIFY = False - o = nikola.utils.slugify(u'Zażółć gęślą jaźń!-123.456', lang='pl') - assert o == u'Zażółć gęślą jaźń!-123.456' - assert isinstance(o, nikola.utils.unicode_str) - nikola.utils.USE_SLUGIFY = True + o = nikola.utils.slugify(title, lang="pl") + assert o == expected_slug + assert isinstance(o, str) -def test_disarmed_weird(): - """Test disarmed slugify with banned characters.""" +@pytest.fixture +def disarm_slugify(): nikola.utils.USE_SLUGIFY = False - o = nikola.utils.slugify(u'Zażółć gęślą jaźń!-123.456 "Hello World"?#H<e>l/l\\o:W\'o\rr*l\td|!\n', lang='pl') - assert o == u'Zażółć gęślą jaźń!-123.456 -Hello World---H-e-l-l-o-W-o-r-l-d-!-' - assert isinstance(o, nikola.utils.unicode_str) - nikola.utils.USE_SLUGIFY = True + try: + yield + finally: + nikola.utils.USE_SLUGIFY = True diff --git a/tests/test_task_scale_images.py b/tests/test_task_scale_images.py new file mode 100644 index 0000000..46a39a4 --- /dev/null +++ b/tests/test_task_scale_images.py @@ -0,0 +1,126 @@ +import os +from tempfile import NamedTemporaryFile + +import pytest +from PIL import Image, ImageDraw + +from nikola.plugins.task import scale_images + +# These tests don't require valid profiles. They need only to verify +# that profile data is/isn't saved with images. +# It would be nice to use PIL.ImageCms to create valid profiles, but +# in many Pillow distributions ImageCms is a stub. +# ICC file data format specification: +# http://www.color.org/icc32.pdf +PROFILE = b"invalid profile data" + + +def test_handling_icc_profiles(test_images, destination_dir): + filename, expected_profile = test_images + + pathname = os.path.join(str(destination_dir), filename) + assert os.path.exists(pathname), pathname + + img = Image.open(pathname) + actual_profile = img.info.get("icc_profile") + assert actual_profile == expected_profile + + +@pytest.fixture( + params=[ + pytest.param(True, id="with icc filename"), + pytest.param(False, id="without icc filename"), + ] +) +def test_images(request, preserve_icc_profiles, source_dir, site): + image_filename = create_src_image(str(source_dir), request.param) + run_task(site) + + if request.param: + yield image_filename, PROFILE if preserve_icc_profiles else None + else: + yield image_filename, None + + +@pytest.fixture( + params=[ + pytest.param(True, id="profiles preserved"), + pytest.param(False, id="profiles not preserved"), + ] +) +def preserve_icc_profiles(request): + return request.param + + +@pytest.fixture +def source_dir(tmpdir_factory): + return tmpdir_factory.mktemp("image_source") + + +@pytest.fixture +def site(preserve_icc_profiles, source_dir, destination_dir): + config = { + "IMAGE_FOLDERS": {str(source_dir): ""}, + "OUTPUT_FOLDER": str(destination_dir), + "IMAGE_THUMBNAIL_SIZE": 128, + "IMAGE_THUMBNAIL_FORMAT": "{name}.thumbnail{ext}", + "MAX_IMAGE_SIZE": 512, + "FILTERS": {}, + "PRESERVE_EXIF_DATA": False, + "EXIF_WHITELIST": {}, + "PRESERVE_ICC_PROFILES": preserve_icc_profiles, + } + return FakeSite(config) + + +class FakeSite: + def __init__(self, config): + self.config = config + self.debug = True + + +@pytest.fixture +def destination_dir(tmpdir_factory): + return tmpdir_factory.mktemp("image_output") + + +def run_task(site): + task_instance = get_task_instance(site) + for task in task_instance.gen_tasks(): + for action, args in task.get("actions", []): + action(*args) + + +def get_task_instance(site): + result = scale_images.ScaleImage() + result.set_site(site) + return result + + +def create_src_image(testdir, use_icc_profile): + img = create_test_image() + pathname = tmp_img_name(testdir) + + # Test two variants: with and without an associated icc_profile + if use_icc_profile: + img.save(pathname, icc_profile=PROFILE) + else: + img.save(pathname) + + return os.path.basename(pathname) + + +def create_test_image(): + # Make a white image with a red stripe on the diagonal. + width = 64 + height = 64 + img = Image.new("RGB", (width, height), (255, 255, 255)) + draw = ImageDraw.Draw(img) + draw.line((0, 0, width, height), fill=(255, 128, 128)) + draw.line((width, 0, 0, height), fill=(128, 128, 255)) + return img + + +def tmp_img_name(dirname): + pathname = NamedTemporaryFile(suffix=".jpg", dir=dirname, delete=False) + return pathname.name diff --git a/tests/test_template_shortcodes.py b/tests/test_template_shortcodes.py index a1d3a91..c6c948d 100644 --- a/tests/test_template_shortcodes.py +++ b/tests/test_template_shortcodes.py @@ -1,50 +1,26 @@ -# -*- coding: utf-8 -*- -# vim: set wrap textwidth=100 """Test template-based shortcodes.""" -from __future__ import unicode_literals - import pytest -from nikola import Nikola - - -class ShortcodeFakeSite(Nikola): - def _get_template_system(self): - if self._template_system is None: - # Load template plugin - self._template_system = self.plugin_manager.getPluginByName( - 'jinja', "TemplateSystem").plugin_object - self._template_system.set_directories('.', 'cache') - self._template_system.set_site(self) - - return self._template_system - - template_system = property(_get_template_system) - -@pytest.fixture(scope="module") -def fakesite(): - s = ShortcodeFakeSite() - s.init_plugins() - s._template_system = None - return s +from nikola import Nikola -def test_mixedargs(fakesite): - TEST_TMPL = """ +def test_mixedargs(site): + test_template = """ arg1: {{ _args[0] }} arg2: {{ _args[1] }} kwarg1: {{ kwarg1 }} kwarg2: {{ kwarg2 }} """ - fakesite.shortcode_registry['test1'] = \ - fakesite._make_renderfunc(TEST_TMPL) - fakesite.shortcode_registry['test2'] = \ - fakesite._make_renderfunc('Something completely different') + site.shortcode_registry["test1"] = site._make_renderfunc(test_template) + site.shortcode_registry["test2"] = site._make_renderfunc( + "Something completely different" + ) - res = fakesite.apply_shortcodes( - '{{% test1 kwarg1=spamm arg1 kwarg2=foo,bar arg2 %}}') + res = site.apply_shortcodes("{{% test1 kwarg1=spamm arg1 kwarg2=foo,bar arg2 %}}")[ + 0 + ] assert res.strip() == """ arg1: arg1 @@ -53,34 +29,50 @@ kwarg1: spamm kwarg2: foo,bar""".strip() -def test_onearg(fakesite): - fakesite.shortcode_registry['test1'] = \ - fakesite._make_renderfunc('arg={{ _args[0] }}') +@pytest.mark.parametrize( + "template, data, expected_result", + [ + # one argument + ("arg={{ _args[0] }}", "{{% test1 onearg %}}", "arg=onearg"), + ("arg={{ _args[0] }}", '{{% test1 "one two" %}}', "arg=one two"), + # keyword arguments + ("foo={{ foo }}", "{{% test1 foo=bar %}}", "foo=bar"), + ("foo={{ foo }}", '{{% test1 foo="bar baz" %}}', "foo=bar baz"), + ("foo={{ foo }}", '{{% test1 foo="bar baz" spamm=ham %}}', "foo=bar baz"), + # data + ( + "data={{ data }}", + "{{% test1 %}}spamm spamm{{% /test1 %}}", + "data=spamm spamm", + ), + ("data={{ data }}", "{{% test1 spamm %}}", "data="), + ("data={{ data }}", "{{% test1 data=dummy %}}", "data="), + ], +) +def test_applying_shortcode(site, template, data, expected_result): + site.shortcode_registry["test1"] = site._make_renderfunc(template) + + assert site.apply_shortcodes(data)[0] == expected_result - assert fakesite.apply_shortcodes('{{% test1 onearg %}}') == 'arg=onearg' - assert fakesite.apply_shortcodes('{{% test1 "one two" %}}') == 'arg=one two' +@pytest.fixture(scope="module") +def site(): + s = ShortcodeFakeSite() + s.init_plugins() + s._template_system = None + return s -def test_kwarg(fakesite): - fakesite.shortcode_registry['test1'] = \ - fakesite._make_renderfunc('foo={{ foo }}') - - res = fakesite.apply_shortcodes('{{% test1 foo=bar %}}') - assert res == 'foo=bar' - res = fakesite.apply_shortcodes('{{% test1 foo="bar baz" %}}') - assert res == 'foo=bar baz' - res = fakesite.apply_shortcodes('{{% test1 foo="bar baz" spamm=ham %}}') - assert res == 'foo=bar baz' +class ShortcodeFakeSite(Nikola): + def _get_template_system(self): + if self._template_system is None: + # Load template plugin + self._template_system = self.plugin_manager.getPluginByName( + "jinja", "TemplateSystem" + ).plugin_object + self._template_system.set_directories(".", "cache") + self._template_system.set_site(self) -def test_data(fakesite): - fakesite.shortcode_registry['test1'] = \ - fakesite._make_renderfunc('data={{ data }}') + return self._template_system - res = fakesite.apply_shortcodes('{{% test1 %}}spamm spamm{{% /test1 %}}') - assert res == 'data=spamm spamm' - res = fakesite.apply_shortcodes('{{% test1 spamm %}}') - assert res == 'data=' - # surprise! - res = fakesite.apply_shortcodes('{{% test1 data=dummy %}}') - assert res == 'data=' + template_system = property(_get_template_system) diff --git a/tests/test_utils.py b/tests/test_utils.py index 225ff13..997d520 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,331 +1,591 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -import unittest -import mock +""" +Testing Nikolas utility functions. +""" + +import os +from unittest import mock + +import pytest import lxml.html + +from nikola import metadata_extractors +from nikola.plugins.task.sitemap import get_base_path as sitemap_get_base_path from nikola.post import get_meta -from nikola.utils import demote_headers, TranslatableSetting - - -class dummy(object): - default_lang = 'en' - - -class GetMetaTest(unittest.TestCase): - def test_getting_metadata_from_content(self): - file_metadata = ".. title: Nikola needs more tests!\n"\ - ".. slug: write-tests-now\n"\ - ".. date: 2012/09/15 19:52:05\n"\ - ".. tags:\n"\ - ".. link:\n"\ - ".. description:\n"\ - "Post content\n" - - opener_mock = mock.mock_open(read_data=file_metadata) - - post = dummy() - post.source_path = 'file_with_metadata' - post.metadata_path = 'file_with_metadata.meta' - - with mock.patch('nikola.post.io.open', opener_mock, create=True): - meta, nsm = get_meta(post) - - self.assertEqual('Nikola needs more tests!', meta['title']) - self.assertEqual('write-tests-now', meta['slug']) - self.assertEqual('2012/09/15 19:52:05', meta['date']) - self.assertFalse('tags' in meta) - self.assertFalse('link' in meta) - self.assertFalse('description' in meta) - self.assertTrue(nsm) - - def test_get_title_from_rest(self): - file_metadata = ".. slug: write-tests-now\n"\ - ".. date: 2012/09/15 19:52:05\n"\ - ".. tags:\n"\ - ".. link:\n"\ - ".. description:\n\n"\ - "Post Title\n"\ - "----------\n" - - opener_mock = mock.mock_open(read_data=file_metadata) - - post = dummy() - post.source_path = 'file_with_metadata' - post.metadata_path = 'file_with_metadata.meta' - - with mock.patch('nikola.post.io.open', opener_mock, create=True): - meta, nsm = get_meta(post) - - self.assertEqual('Post Title', meta['title']) - self.assertEqual('write-tests-now', meta['slug']) - self.assertEqual('2012/09/15 19:52:05', meta['date']) - self.assertFalse('tags' in meta) - self.assertFalse('link' in meta) - self.assertFalse('description' in meta) - self.assertTrue(nsm) - - def test_get_title_from_fname(self): - file_metadata = ".. slug: write-tests-now\n"\ - ".. date: 2012/09/15 19:52:05\n"\ - ".. tags:\n"\ - ".. link:\n"\ - ".. description:\n" - - opener_mock = mock.mock_open(read_data=file_metadata) - - post = dummy() - post.source_path = 'file_with_metadata' - post.metadata_path = 'file_with_metadata.meta' - - with mock.patch('nikola.post.io.open', opener_mock, create=True): - meta, nsm = get_meta(post, 'file_with_metadata') - - self.assertEqual('file_with_metadata', meta['title']) - self.assertEqual('write-tests-now', meta['slug']) - self.assertEqual('2012/09/15 19:52:05', meta['date']) - self.assertFalse('tags' in meta) - self.assertFalse('link' in meta) - self.assertFalse('description' in meta) - self.assertTrue(nsm) - - def test_use_filename_as_slug_fallback(self): - file_metadata = ".. title: Nikola needs more tests!\n"\ - ".. date: 2012/09/15 19:52:05\n"\ - ".. tags:\n"\ - ".. link:\n"\ - ".. description:\n\n"\ - "Post content\n" - - opener_mock = mock.mock_open(read_data=file_metadata) - - post = dummy() - post.source_path = 'Slugify this' - post.metadata_path = 'Slugify this.meta' - - with mock.patch('nikola.post.io.open', opener_mock, create=True): - meta, nsm = get_meta(post, 'Slugify this') - self.assertEqual('Nikola needs more tests!', meta['title']) - self.assertEqual('slugify-this', meta['slug']) - self.assertEqual('2012/09/15 19:52:05', meta['date']) - self.assertFalse('tags' in meta) - self.assertFalse('link' in meta) - self.assertFalse('description' in meta) - self.assertTrue(nsm) - - def test_extracting_metadata_from_filename(self): - post = dummy() - post.source_path = '2013-01-23-the_slug-dubdubtitle.md' - post.metadata_path = '2013-01-23-the_slug-dubdubtitle.meta' - with mock.patch('nikola.post.io.open', create=True): - meta, _ = get_meta( - post, - '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md') - - self.assertEqual('dubdubtitle', meta['title']) - self.assertEqual('the_slug', meta['slug']) - self.assertEqual('2013-01-23', meta['date']) - - def test_get_meta_slug_only_from_filename(self): - post = dummy() - post.source_path = 'some/path/the_slug.md' - post.metadata_path = 'some/path/the_slug.meta' - with mock.patch('nikola.post.io.open', create=True): - meta, _ = get_meta(post) - - self.assertEqual('the_slug', meta['slug']) - - -class HeaderDemotionTest(unittest.TestCase): - def demote_by_zero(self): - input_str = '''\ - <h1>header 1</h1> - <h2>header 2</h2> - <h3>header 3</h3> - <h4>header 4</h4> - <h5>header 5</h5> - <h6>header 6</h6> - ''' - expected_output = '''\ - <h1>header 1</h1> - <h2>header 2</h2> - <h3>header 3</h3> - <h4>header 4</h4> - <h5>header 5</h5> - <h6>header 6</h6> - ''' - doc = lxml.html.fromstring(input_str) - outdoc = lxml.html.fromstring(expected_output) - demote_headers(doc, 0) - self.assertEquals(lxml.html.tostring(outdoc), lxml.html.tostring(doc)) - - def demote_by_one(self): - input_str = '''\ - <h1>header 1</h1> - <h2>header 2</h2> - <h3>header 3</h3> - <h4>header 4</h4> - <h5>header 5</h5> - <h6>header 6</h6> - ''' - expected_output = '''\ - <h2>header 1</h2> - <h3>header 2</h3> - <h4>header 3</h4> - <h5>header 4</h5> - <h6>header 5</h6> - <h6>header 6</h6> - ''' - doc = lxml.html.fromstring(input_str) - outdoc = lxml.html.fromstring(expected_output) - demote_headers(doc, 1) - self.assertEquals(lxml.html.tostring(outdoc), lxml.html.tostring(doc)) - - def demote_by_two(self): - input_str = '''\ - <h1>header 1</h1> - <h2>header 2</h2> - <h3>header 3</h3> - <h4>header 4</h4> - <h5>header 5</h5> - <h6>header 6</h6> - ''' - expected_output = '''\ - <h3>header 1</h3> - <h4>header 2</h4> - <h5>header 3</h5> - <h6>header 4</h6> - <h6>header 5</h6> - <h6>header 6</h6> - ''' - doc = lxml.html.fromstring(input_str) - outdoc = lxml.html.fromstring(expected_output) - demote_headers(doc, 2) - self.assertEquals(lxml.html.tostring(outdoc), lxml.html.tostring(doc)) - - def demote_by_minus_one(self): - input_str = '''\ - <h1>header 1</h1> - <h2>header 2</h2> - <h3>header 3</h3> - <h4>header 4</h4> - <h5>header 5</h5> - <h6>header 6</h6> - ''' - expected_output = '''\ - <h1>header 1</h1> - <h1>header 2</h1> - <h2>header 3</h2> - <h3>header 4</h3> - <h4>header 5</h4> - <h5>header 6</h5> - ''' - doc = lxml.html.fromstring(input_str) - outdoc = lxml.html.fromstring(expected_output) - demote_headers(doc, -1) - self.assertEquals(lxml.html.tostring(outdoc), lxml.html.tostring(doc)) - - -class TranslatableSettingsTest(unittest.TestCase): - """Tests for translatable settings.""" - - def test_string_input(self): - """Tests for string input.""" - inp = 'Fancy Blog' - S = TranslatableSetting('S', inp, {'xx': ''}) - S.default_lang = 'xx' - S.lang = 'xx' - - try: - u = unicode(S) - except NameError: # Python 3 - u = str(S) - - cn = S() # no language specified - cr = S('xx') # real language specified - cf = S('zz') # fake language specified - - self.assertEqual(inp, u) - self.assertEqual(inp, cn) - self.assertEqual(inp, cr) - self.assertEqual(inp, cf) - self.assertEqual(S.lang, 'xx') - self.assertEqual(S.default_lang, 'xx') - - def test_dict_input(self): - """Tests for dict input.""" - inp = {'xx': 'Fancy Blog', - 'zz': 'Schmancy Blog'} - - S = TranslatableSetting('S', inp, {'xx': '', 'zz': ''}) - S.default_lang = 'xx' - S.lang = 'xx' - - try: - u = unicode(S) - except NameError: # Python 3 - u = str(S) - - cn = S() - cx = S('xx') - cz = S('zz') - cf = S('ff') - - self.assertEqual(inp['xx'], u) - self.assertEqual(inp['xx'], cn) - self.assertEqual(inp['xx'], cx) - self.assertEqual(inp['zz'], cz) - self.assertEqual(inp['xx'], cf) - - def test_dict_input_lang(self): - """Test dict input, with a language change along the way.""" - inp = {'xx': 'Fancy Blog', - 'zz': 'Schmancy Blog'} - - S = TranslatableSetting('S', inp, {'xx': '', 'zz': ''}) - S.default_lang = 'xx' - S.lang = 'xx' - - try: - u = unicode(S) - except NameError: # Python 3 - u = str(S) - - cn = S() - - self.assertEqual(inp['xx'], u) - self.assertEqual(inp['xx'], cn) - - # Change the language. - # WARNING: DO NOT set lang locally in real code! Set it globally - # instead! (TranslatableSetting.lang = ...) - # WARNING: TranslatableSetting.lang is used to override the current - # locale settings returned by LocaleBorg! Use with care! - S.lang = 'zz' - - try: - u = unicode(S) - except NameError: # Python 3 - u = str(S) - - cn = S() - - self.assertEqual(inp['zz'], u) - self.assertEqual(inp['zz'], cn) - - -def test_get_metadata_from_file(): - # These were doctests and not running :-P - from nikola.post import _get_metadata_from_file - g = _get_metadata_from_file - assert list(g([]).values()) == [] - assert str(g(["======", "FooBar", "======"])["title"]) == 'FooBar' - assert str(g(["FooBar", "======"])["title"]) == 'FooBar' - assert str(g(["#FooBar"])["title"]) == 'FooBar' - assert str(g([".. title: FooBar"])["title"]) == 'FooBar' - assert 'title' not in g(["", "", ".. title: FooBar"]) - assert 'title' in g(["", ".. title: FooBar"]) - assert 'title' in g([".. foo: bar", "", "FooBar", "------"]) - - -if __name__ == '__main__': - unittest.main() +from nikola.utils import ( + TemplateHookRegistry, + TranslatableSetting, + demote_headers, + get_asset_path, + get_crumbs, + get_theme_chain, + get_translation_candidate, + write_metadata, +) + + +def test_getting_metadata_from_content(post): + post.source_path = "file_with_metadata" + post.metadata_path = "file_with_metadata.meta" + + file_content = """\ +.. title: Nikola needs more tests! +.. slug: write-tests-now +.. date: 2012/09/15 19:52:05 +.. tags: +.. link: +.. description: + +Post content +""" + opener_mock = mock.mock_open(read_data=file_content) + with mock.patch("nikola.post.io.open", opener_mock, create=True): + meta = get_meta(post, None)[0] + + assert "Nikola needs more tests!" == meta["title"] + assert "write-tests-now" == meta["slug"] + assert "2012/09/15 19:52:05" == meta["date"] + assert "tags" not in meta + assert "link" not in meta + assert "description" not in meta + + +def test_get_title_from_fname(post): + post.source_path = "file_with_metadata" + post.metadata_path = "file_with_metadata.meta" + + file_content = """\ +.. slug: write-tests-now +.. date: 2012/09/15 19:52:05 +.. tags: +.. link: +.. description: +""" + opener_mock = mock.mock_open(read_data=file_content) + with mock.patch("nikola.post.io.open", opener_mock, create=True): + meta = get_meta(post, None)[0] + + assert "file_with_metadata" == meta["title"] + assert "write-tests-now" == meta["slug"] + assert "2012/09/15 19:52:05" == meta["date"] + assert "tags" not in meta + assert "link" not in meta + assert "description" not in meta + + +def test_use_filename_as_slug_fallback(post): + post.source_path = "Slugify this" + post.metadata_path = "Slugify this.meta" + + file_content = """\ +.. title: Nikola needs more tests! +.. date: 2012/09/15 19:52:05 +.. tags: +.. link: +.. description: + +Post content +""" + opener_mock = mock.mock_open(read_data=file_content) + with mock.patch("nikola.post.io.open", opener_mock, create=True): + meta = get_meta(post, None)[0] + + assert "Nikola needs more tests!" == meta["title"] + assert "slugify-this" == meta["slug"] + assert "2012/09/15 19:52:05" == meta["date"] + assert "tags" not in meta + assert "link" not in meta + assert "description" not in meta + + +@pytest.mark.parametrize( + "unslugify, expected_title", [(True, "Dub dub title"), (False, "dub_dub_title")] +) +def test_extracting_metadata_from_filename(post, unslugify, expected_title): + post.source_path = "2013-01-23-the_slug-dub_dub_title.md" + post.metadata_path = "2013-01-23-the_slug-dub_dub_title.meta" + + post.config[ + "FILE_METADATA_REGEXP" + ] = r"(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md" + post.config["FILE_METADATA_UNSLUGIFY_TITLES"] = unslugify + + no_metadata_opener = mock.mock_open(read_data="No metadata in the file!") + with mock.patch("nikola.post.io.open", no_metadata_opener, create=True): + meta = get_meta(post, None)[0] + + assert expected_title == meta["title"] + assert "the_slug" == meta["slug"] + assert "2013-01-23" == meta["date"] + + +def test_get_meta_slug_only_from_filename(post): + post.source_path = "some/path/the_slug.md" + post.metadata_path = "some/path/the_slug.meta" + + no_metadata_opener = mock.mock_open(read_data="No metadata in the file!") + with mock.patch("nikola.post.io.open", no_metadata_opener, create=True): + meta = get_meta(post, None)[0] + + assert "the_slug" == meta["slug"] + + +@pytest.mark.parametrize( + "level, input_str, expected_output", + [ + pytest.param( + 0, + """ + <h1>header 1</h1> + <h2>header 2</h2> + <h3>header 3</h3> + <h4>header 4</h4> + <h5>header 5</h5> + <h6>header 6</h6> + """, + """ + <h1>header 1</h1> + <h2>header 2</h2> + <h3>header 3</h3> + <h4>header 4</h4> + <h5>header 5</h5> + <h6>header 6</h6> + """, + id="by zero", + ), + pytest.param( + 1, + """ + <h1>header 1</h1> + <h2>header 2</h2> + <h3>header 3</h3> + <h4>header 4</h4> + <h5>header 5</h5> + <h6>header 6</h6> + """, + """ + <h2>header 1</h2> + <h3>header 2</h3> + <h4>header 3</h4> + <h5>header 4</h5> + <h6>header 5</h6> + <h6>header 6</h6> + """, + id="by one", + ), + pytest.param( + 2, + """ + <h1>header 1</h1> + <h2>header 2</h2> + <h3>header 3</h3> + <h4>header 4</h4> + <h5>header 5</h5> + <h6>header 6</h6> + """, + """ + <h3>header 1</h3> + <h4>header 2</h4> + <h5>header 3</h5> + <h6>header 4</h6> + <h6>header 5</h6> + <h6>header 6</h6> + """, + id="by two", + ), + pytest.param( + -1, + """ + <h1>header 1</h1> + <h2>header 2</h2> + <h3>header 3</h3> + <h4>header 4</h4> + <h5>header 5</h5> + <h6>header 6</h6> + """, + """ + <h1>header 1</h1> + <h1>header 2</h1> + <h2>header 3</h2> + <h3>header 4</h3> + <h4>header 5</h4> + <h5>header 6</h5> + """, + id="by minus one", + ), + ], +) +def test_demoting_headers(level, input_str, expected_output): + doc = lxml.html.fromstring(input_str) + outdoc = lxml.html.fromstring(expected_output) + demote_headers(doc, level) + assert lxml.html.tostring(outdoc) == lxml.html.tostring(doc) + + +def test_TranslatableSettingsTest_with_string_input(): + """Test ing translatable settings with string input.""" + inp = "Fancy Blog" + setting = TranslatableSetting("TestSetting", inp, {"xx": ""}) + setting.default_lang = "xx" + setting.lang = "xx" + + assert inp == str(setting) + assert inp == setting() # no language specified + assert inp == setting("xx") # real language specified + assert inp == setting("zz") # fake language specified + assert setting.lang == "xx" + assert setting.default_lang == "xx" + + +def test_TranslatableSetting_with_dict_input(): + """Tests for translatable setting with dict input.""" + inp = {"xx": "Fancy Blog", "zz": "Schmancy Blog"} + + setting = TranslatableSetting("TestSetting", inp, {"xx": "", "zz": ""}) + setting.default_lang = "xx" + setting.lang = "xx" + + assert inp["xx"] == str(setting) + assert inp["xx"] == setting() # no language specified + assert inp["xx"] == setting("xx") # real language specified + assert inp["zz"] == setting("zz") # fake language specified + assert inp["xx"] == setting("ff") + + +def test_TranslatableSetting_with_language_change(): + """Test translatable setting with language change along the way.""" + inp = {"xx": "Fancy Blog", "zz": "Schmancy Blog"} + + setting = TranslatableSetting("TestSetting", inp, {"xx": "", "zz": ""}) + setting.default_lang = "xx" + setting.lang = "xx" + + assert inp["xx"] == str(setting) + assert inp["xx"] == setting() + + # Change the language. + # WARNING: DO NOT set lang locally in real code! Set it globally + # instead! (TranslatableSetting.lang = ...) + # WARNING: TranslatableSetting.lang is used to override the current + # locale settings returned by LocaleBorg! Use with care! + setting.lang = "zz" + + assert inp["zz"] == str(setting) + assert inp["zz"] == setting() + + +@pytest.mark.parametrize( + "path, files_folders, expected_path_end", + [ + ( + "assets/css/nikola_rst.css", + {"files": ""}, # default files_folders + "nikola/data/themes/base/assets/css/nikola_rst.css", + ), + ( + "assets/css/theme.css", + {"files": ""}, # default files_folders + "nikola/data/themes/bootstrap4/assets/css/theme.css", + ), + ("nikola.py", {"nikola": ""}, "nikola/nikola.py"), + ("nikola/nikola.py", {"nikola": "nikola"}, "nikola/nikola.py"), + ("nikola.py", {"nikola": "nikola"}, None), + ], +) +def test_get_asset_path(path, files_folders, expected_path_end): + theme_chain = get_theme_chain("bootstrap4", ["themes"]) + asset_path = get_asset_path(path, theme_chain, files_folders) + + if expected_path_end: + asset_path = asset_path.replace("\\", "/") + assert asset_path.endswith(expected_path_end) + else: + assert asset_path is None + + +@pytest.mark.parametrize( + "path, is_file, expected_crumbs", + [ + ("galleries", False, [["#", "galleries"]]), + ( + os.path.join("galleries", "demo"), + False, + [["..", "galleries"], ["#", "demo"]], + ), + ( + os.path.join("listings", "foo", "bar"), + True, + [["..", "listings"], [".", "foo"], ["#", "bar"]], + ), + ], +) +def test_get_crumbs(path, is_file, expected_crumbs): + crumbs = get_crumbs(path, is_file=is_file) + assert len(crumbs) == len(expected_crumbs) + for crumb, expected_crumb in zip(crumbs, expected_crumbs): + assert crumb == expected_crumb + + +@pytest.mark.parametrize( + "pattern, path, lang, expected_path", + [ + ("{path}.{lang}.{ext}", "*.rst", "es", "*.es.rst"), + ("{path}.{lang}.{ext}", "fancy.post.rst", "es", "fancy.post.es.rst"), + ("{path}.{lang}.{ext}", "*.es.rst", "es", "*.es.rst"), + ("{path}.{lang}.{ext}", "*.es.rst", "en", "*.rst"), + ( + "{path}.{lang}.{ext}", + "cache/posts/fancy.post.es.html", + "en", + "cache/posts/fancy.post.html", + ), + ( + "{path}.{lang}.{ext}", + "cache/posts/fancy.post.html", + "es", + "cache/posts/fancy.post.es.html", + ), + ( + "{path}.{lang}.{ext}", + "cache/pages/charts.html", + "es", + "cache/pages/charts.es.html", + ), + ( + "{path}.{lang}.{ext}", + "cache/pages/charts.html", + "en", + "cache/pages/charts.html", + ), + ("{path}.{ext}.{lang}", "*.rst", "es", "*.rst.es"), + ("{path}.{ext}.{lang}", "*.rst.es", "es", "*.rst.es"), + ("{path}.{ext}.{lang}", "*.rst.es", "en", "*.rst"), + ( + "{path}.{ext}.{lang}", + "cache/posts/fancy.post.html.es", + "en", + "cache/posts/fancy.post.html", + ), + ( + "{path}.{ext}.{lang}", + "cache/posts/fancy.post.html", + "es", + "cache/posts/fancy.post.html.es", + ), + ], +) +def test_get_translation_candidate(pattern, path, lang, expected_path): + config = { + "TRANSLATIONS_PATTERN": pattern, + "DEFAULT_LANG": "en", + "TRANSLATIONS": {"es": "1", "en": 1}, + } + assert get_translation_candidate(config, path, lang) == expected_path + + +def test_TemplateHookRegistry(): + r = TemplateHookRegistry("foo", None) + r.append("Hello!") + r.append(lambda x: "Hello " + x + "!", False, "world") + assert r() == "Hello!\nHello world!" + + +@pytest.mark.parametrize( + "base, expected_path", + [ + ("http://some.site", "/"), + ("http://some.site/", "/"), + ("http://some.site/some/sub-path", "/some/sub-path/"), + ("http://some.site/some/sub-path/", "/some/sub-path/"), + ], +) +def test_sitemap_get_base_path(base, expected_path): + assert expected_path == sitemap_get_base_path(base) + + +@pytest.mark.parametrize( + "metadata_format, expected_result", + [ + ( + "nikola", + """\ +.. title: Hello, world! +.. slug: hello-world +.. a: 1 +.. b: 2 + +""", + ), + ( + "yaml", + """\ +--- +a: '1' +b: '2' +slug: hello-world +title: Hello, world! +--- +""", + ), + ], +) +def test_write_metadata_with_formats(metadata_format, expected_result): + """ + Test writing metadata with different formats. + + YAML is expected to be sorted alphabetically. + Nikola sorts by putting the defaults first and then sorting the rest + alphabetically. + """ + data = {"slug": "hello-world", "title": "Hello, world!", "b": "2", "a": "1"} + assert write_metadata(data, metadata_format) == expected_result + + +def test_write_metadata_with_format_toml(): + """ + Test writing metadata in TOML format. + + TOML is sorted randomly in Python 3.5 or older and by insertion + order since Python 3.6. + """ + data = {"slug": "hello-world", "title": "Hello, world!", "b": "2", "a": "1"} + + toml = write_metadata(data, "toml") + assert toml.startswith("+++\n") + assert toml.endswith("+++\n") + assert 'slug = "hello-world"' in toml + assert 'title = "Hello, world!"' in toml + assert 'b = "2"' in toml + assert 'a = "1"' in toml + + +@pytest.mark.parametrize( + "wrap, expected_result", + [ + ( + False, + """\ +.. title: Hello, world! +.. slug: hello-world + +""", + ), + ( + True, + """\ +<!-- +.. title: Hello, world! +.. slug: hello-world +--> + +""", + ), + ( + ("111", "222"), + """\ +111 +.. title: Hello, world! +.. slug: hello-world +222 + +""", + ), + ], +) +def test_write_metadata_comment_wrap(wrap, expected_result): + data = {"title": "Hello, world!", "slug": "hello-world"} + assert write_metadata(data, "nikola", wrap) == expected_result + + +@pytest.mark.parametrize( + "metadata_format, expected_results", + [ + ( + "rest_docinfo", + [ + """============= +Hello, world! +============= + +:slug: hello-world +""" + ], + ), + ( + "markdown_meta", + [ + """title: Hello, world! +slug: hello-world + +""", + """slug: hello-world +title: Hello, world! + +""", + ], + ), + ], +) +def test_write_metadata_compiler(metadata_format, expected_results): + """ + Test writing metadata with different formats. + + We test for multiple results because some compilers might produce + unordered output. + """ + data = {"title": "Hello, world!", "slug": "hello-world"} + assert write_metadata(data, metadata_format) in expected_results + + +@pytest.mark.parametrize( + "post_format, expected_metadata", + [ + ("rest", "==\nxx\n==\n\n"), + ("markdown", "title: xx\n\n"), + ("html", ".. title: xx\n\n"), + ], +) +def test_write_metadata_pelican_detection(post, post_format, expected_metadata): + post.name = post_format + + data = {"title": "xx"} + assert write_metadata(data, "pelican", compiler=post) == expected_metadata + + +def test_write_metadata_pelican_detection_default(): + data = {"title": "xx"} + assert write_metadata(data, "pelican", compiler=None) == ".. title: xx\n\n" + + +def test_write_metadata_from_site(post): + post.config = {"METADATA_FORMAT": "yaml"} + data = {"title": "xx"} + assert write_metadata(data, site=post) == "---\ntitle: xx\n---\n" + + +def test_write_metadata_default(post): + data = {"title": "xx"} + assert write_metadata(data) == ".. title: xx\n\n" + + +@pytest.mark.parametrize("arg", ["foo", "filename_regex"]) +def test_write_metadata_fallbacks(post, arg): + data = {"title": "xx"} + assert write_metadata(data, arg) == ".. title: xx\n\n" + + +@pytest.fixture +def post(): + return FakePost() + + +class FakePost: + default_lang = "en" + metadata_extractors_by = metadata_extractors.default_metadata_extractors_by() + config = { + "TRANSLATIONS_PATTERN": "{path}.{lang}.{ext}", + "TRANSLATIONS": {"en": "./"}, + "DEFAULT_LANG": "en", + } + + def __init__(self): + metadata_extractors.load_defaults(self, self.metadata_extractors_by) |
