Merge pull request #13 from RealOrangeOne/tests

Add tests
This commit is contained in:
Jake Howard 2017-06-10 12:22:52 +01:00 committed by GitHub
commit 46b9dc31af
28 changed files with 819 additions and 52 deletions

1
.gitignore vendored
View file

@ -245,3 +245,4 @@ ENV/
# End of https://www.gitignore.io/api/node,linux,python,jetbrains,archlinuxpackages
out/
.mypy_cache

View file

@ -18,9 +18,12 @@ dependencies:
test:
post:
override:
- npm test
- flake8 md_pdf/ --ignore=E128,E501
- mypy --ignore-missing-imports md_pdf
- safety check
- bandit -r md_pdf/
- mdp --update-csl
- mdp --update-csl -vvv
- cd test-files/ && mdp -vvv
- scripts/run-tests.sh

View file

@ -1,3 +1,6 @@
bandit==1.4.0
coverage==4.4.1
flake8==3.3.0
freezegun==0.3.9
mypy==0.511
safety==0.5.1

View file

@ -2,10 +2,10 @@ import argparse
from md_pdf import __version__
def parse_args():
def parse_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="Set verbosity level (repeat argument)", action="count", default=0)
parser.add_argument("--update-csl", help="Update CSL files", action="store_true")
parser.add_argument("--version", action="version", version="%(prog)s {}".format(__version__))
parser.add_help = True
return parser.parse_args()
return parser.parse_args(args=args)

View file

@ -11,11 +11,11 @@ import time
logger = logging.getLogger(__file__)
def build(config):
def build(config: dict):
logger.debug("Starting Build...")
start_time = time.time()
data = read_files(os.path.abspath(config['input']))
doc = build_document(data, config.get('bibliography'), config.get('context'))
doc = build_document(data, config.get('bibliography'))
parsed_template = parse_template(doc, config)
if 'html' in config['output_formats']:
output_html(parsed_template, os.path.abspath(config['output_dir']))

View file

@ -19,7 +19,7 @@ def fix_references_title(content, config):
return soup.prettify()
def add_base_tag(doc, config):
def make_images_relative(doc, config):
logger.debug("Adding Base Tag...")
soup = BeautifulSoup(doc, 'html.parser')
for img in soup.findAll('img'):
@ -32,6 +32,8 @@ def add_base_tag(doc, config):
def add_body_class(doc, config):
logger.debug("Adding Body Class...")
soup = BeautifulSoup(doc, 'html.parser')
if not soup.body:
return doc
soup.body['class'] = 'content'
return soup.prettify()
@ -46,7 +48,7 @@ def parse_template(doc, config):
parsed_doc = doc
for parser in [
fix_references_title,
add_base_tag,
make_images_relative,
add_body_class,
]:
parsed_doc = parser(parsed_doc, config)

View file

@ -19,11 +19,14 @@ EXTRA_CONTEXT = {
}
def get_context(config, content):
def get_context(config: dict, content: str) -> dict:
config = config.copy()
context = config['context'].copy()
del config['context']
context = dict(
if 'context' in config:
context = config['context'].copy()
del config['context']
else:
context = {}
merged_context = dict(
config,
**EXTRA_CONTEXT,
**context,
@ -32,11 +35,11 @@ def get_context(config, content):
}
)
if config.get('show_word_count'):
context['word_count'] = word_count(get_plain_text(content))
merged_context['word_count'] = word_count(get_plain_text(content))
if config.get('submission_date'):
if type(config['submission_date']) in [datetime.date, datetime.datetime, datetime.time]:
submission_date = config['submission_date']
else:
submission_date = parser.parse(config['submission_date'])
context['submission_date'] = submission_date.strftime(DATE_FORMAT)
return context
merged_context['submission_date'] = submission_date.strftime(DATE_FORMAT)
return merged_context

View file

@ -10,7 +10,6 @@ def render_content(content, context):
'jinja2.ext.with_',
'jinja2.ext.loopcontrols'
]
)
template = env.from_string(content)
return template.render(**context)

View file

@ -1,12 +1,13 @@
import glob
from typing import Generator, List
def get_files_content(filenames):
def get_files_content(filenames: List[str]) -> Generator[str, None, None]:
for filename in filenames:
with open(filename) as f:
yield f.read()
def read_files(files_glob):
def read_files(files_glob: str) -> str:
filenames = sorted(glob.glob(files_glob))
return '\n'.join(list(get_files_content(filenames)))

View file

@ -6,13 +6,13 @@ import logging
logger = logging.getLogger(__file__)
def output_html(html, out_dir):
def output_html(html: str, out_dir: str):
logger.info("Outputting HTML...")
with open(os.path.join(out_dir, 'output.html'), 'w') as f:
f.write(html)
def build_document(files_content, bibliography, context):
def build_document(files_content: str, bibliography: dict) -> str:
args = [
'-s',
]

View file

@ -1,6 +1,7 @@
import pdfkit
from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR
from md_pdf.build.templates import FILE_NAME_FORMAT
from md_pdf.exceptions import PDFRenderException
import os
import logging
@ -12,8 +13,8 @@ DEFAULT_MARGIN_VERTICAL = '1.5cm'
DEFAULT_MARGIN_HORIZONTAL = '2.5cm'
STYLE_FILE = os.path.join(STATIC_DIR, 'style.css')
HEADER_FILE = os.path.join(TEMPLATES_DIR, 'header.html')
FOOTER_FILE = os.path.join(TEMPLATES_DIR, 'footer.html')
HEADER_FILE = FILE_NAME_FORMAT.format('header')
FOOTER_FILE = FILE_NAME_FORMAT.format('footer')
TOC_OPTIONS = {
'xsl-style-sheet': os.path.join(TEMPLATES_DIR, 'toc.xsl')
@ -32,23 +33,28 @@ PDF_OPTIONS = {
}
def export_pdf(content, config):
def export_pdf(content: str, config: dict) -> dict:
if logger.getEffectiveLevel() > logging.DEBUG:
PDF_OPTIONS['quiet'] = ""
PDF_OPTIONS['title'] = config.get('title', 'Output')
PDF_OPTIONS['replace'] = [(key, str(value)) for key, value in config['context'].items()]
context = config.get('context', {})
PDF_OPTIONS['margin-top'] = config['context'].get('margin_vertical', DEFAULT_MARGIN_VERTICAL)
PDF_OPTIONS['margin-bottom'] = config['context'].get('margin_vertical', DEFAULT_MARGIN_VERTICAL)
PDF_OPTIONS['margin-left'] = config['context'].get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL)
PDF_OPTIONS['margin-right'] = config['context'].get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL)
PDF_OPTIONS['replace'] = [(key, str(value)) for key, value in context.items()]
PDF_OPTIONS['margin-top'] = context.get('margin_vertical', DEFAULT_MARGIN_VERTICAL)
PDF_OPTIONS['margin-bottom'] = context.get('margin_vertical', DEFAULT_MARGIN_VERTICAL)
PDF_OPTIONS['margin-left'] = context.get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL)
PDF_OPTIONS['margin-right'] = context.get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL)
logger.info("Rendering PDF...")
return pdfkit.from_string(
content,
os.path.join(os.path.abspath(config['output_dir']), 'output.pdf'),
options=PDF_OPTIONS,
cover=FILE_NAME_FORMAT.format('cover'),
toc=TOC_OPTIONS if config.get('toc') else {},
cover_first=True
)
try:
pdfkit.from_string(
content,
os.path.join(os.path.abspath(config['output_dir']), 'output.pdf'),
options=PDF_OPTIONS,
cover=FILE_NAME_FORMAT.format('cover'),
toc=TOC_OPTIONS if config.get('toc') else {},
cover_first=True
)
except OSError as e:
raise PDFRenderException('Failed to render PDF. ' + str(e))
return PDF_OPTIONS # mostly for testing

View file

@ -11,7 +11,7 @@ FILE_NAME_FORMAT = os.path.join(TEMPLATES_DIR, "{}.html")
TEMPLATE_FORMAT = os.path.join(INTERNAL_TEMPLATES_DIR, "{}-template.html")
def render_page(input_file, output_file, context):
def render_page(input_file: str, output_file: str, context: dict) -> str:
logger.debug("Rendering {}...".format(os.path.splitext(os.path.basename(output_file))[0].title()))
with open(input_file) as f:
content = render_content(f.read(), context)
@ -20,7 +20,7 @@ def render_page(input_file, output_file, context):
return content
def render_templates(config, content):
def render_templates(config: dict, content: str):
context = get_context(config, content)
for template in [
'cover',

View file

@ -1,12 +1,11 @@
import yaml
import os
from md_pdf.consts import CONFIG_FILE
from md_pdf.exceptions import ConfigValidationException
def load_config():
def load_config(location: str=CONFIG_FILE) -> str:
try:
with open(os.path.join(CONFIG_FILE)) as f:
with open(location) as f:
return yaml.safe_load(f)
except FileNotFoundError:
raise ConfigValidationException("Can't find config file at {}".format(CONFIG_FILE))

View file

@ -36,6 +36,9 @@ def test_input(config):
abs_input = os.path.abspath(config['input'])
if len(glob.glob(abs_input)) == 0:
raise ConfigValidationException("No files found at {}".format(abs_input))
for file in glob.iglob(abs_input):
if not os.path.isfile(file):
raise ConfigValidationException("Input must be a glob of files")
def validate_bibliography(config):
@ -49,9 +52,8 @@ def validate_bibliography(config):
abs_bibliography = os.path.abspath(config['bibliography']['references'])
if not os.path.isfile(abs_bibliography):
raise ConfigValidationException("Invalid bibliography path: '{}'".format(abs_bibliography))
if 'csl' in config['bibliography']:
if not os.path.isfile(os.path.join(CSL_DIR, "{}.csl".format(config['bibliography']['csl']))):
raise ConfigValidationException("Could not find CSL '{}'".format(config.bibliography.csl))
if not os.path.isfile(os.path.join(CSL_DIR, "{}.csl".format(config['bibliography']['csl']))):
raise ConfigValidationException("Could not find CSL '{}' in {}".format(config['bibliography']['csl'], CSL_DIR))
def validate_context(config):
@ -63,11 +65,11 @@ def validate_context(config):
non_str_keys = [key for key in config['context'].keys() if type(key) != str]
if non_str_keys:
raise ConfigValidationException("Context keys must be strings. Non-strings: {}".format(", ".join(non_str_keys)))
raise ConfigValidationException("Context keys must be strings. Non-strings: {}".format(non_str_keys))
invalid_values = [value for value in config['context'].values() if type(value) in [list, dict]]
if invalid_values:
raise ConfigValidationException("Context keys must be plain. Invalid values: {}".format(", ".join(invalid_values)))
raise ConfigValidationException("Context keys must be plain. Invalid values: {}".format(invalid_values))
def validate_toc(config):
@ -92,7 +94,7 @@ def validate_submission_date(config):
try:
parser.parse(config['submission_date'])
except ValueError:
raise ConfigValidationException("Invalid Submission Date format")
raise ConfigValidationException("Invalid Submission Date format {}".format(config['submission_date']))
def validate_config(config):

View file

@ -39,7 +39,7 @@ def download_csl():
with open(download_location, 'rb') as downloaded_file:
with zipfile.ZipFile(downloaded_file) as csl_zip:
member_list = csl_zip.namelist()
logger.info("Extracting CSL...")
logger.info("Extracting CSL to {}".format(ASSETS_DIR))
bar.start(max_value=len(member_list))
for i, member in enumerate(member_list):

View file

@ -8,3 +8,7 @@ class PrematureExit(BaseException):
class ConfigValidationException(BaseException):
pass
class PDFRenderException(BaseException):
pass

View file

@ -2,11 +2,13 @@ import shutil
import os
import logging
from bs4 import BeautifulSoup
from typing import List
logger = logging.getLogger(__file__)
def remove_dir(dir):
def remove_dir(dir: str):
logger.debug("Removing directory {}.".format(dir))
try:
shutil.rmtree(dir)
@ -15,16 +17,18 @@ def remove_dir(dir):
pass
def safe_list_get(l, idx, default):
def safe_list_get(l: List, idx: int, default):
try:
return l[idx]
except IndexError:
return default
def get_plain_text(content):
def get_plain_text(content: str) -> str:
soup = BeautifulSoup(content, 'html.parser')
body = soup.find('body')
if body is None:
return content
try:
body.find('h1', class_='references-title').extract()
body.find('div', class_='references').extract()

8
scripts/run-tests.sh Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
coverage run --source=md_pdf -m unittest -v $@
coverage report
coverage html

View file

@ -17,7 +17,7 @@ setup(
"word-count==0.1.0"
],
setup_requires=['setuptools_scm'],
packages=find_packages(),
packages=find_packages(exclude=['tests']),
include_package_data=True,
zip_safe=False,
entry_points="""

64
tests/__init__.py Normal file
View file

@ -0,0 +1,64 @@
import unittest
import os
from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR
from md_pdf.build.templates import FILE_NAME_FORMAT
from bs4 import BeautifulSoup
class BaseTestCase(unittest.TestCase):
def setUp(self):
super().setUp()
self.BASE_VALID_CONFIG = {
'title': 'test title',
'input': 'test-files/*.md',
'output_formats': [
'html', 'pdf'
],
'output_dir': 'out/',
}
def parse_html(self, html):
return BeautifulSoup(html, 'html.parser')
def remove_file(self, file):
try:
os.remove(file)
except OSError:
pass
def touch_file(self, file):
open(file, 'w').close()
def create_fake_templates(self):
for template in [
'header',
'footer',
'cover'
]:
self.touch_file(FILE_NAME_FORMAT.format(template))
def extend_config(self, *args):
base_config = self.BASE_VALID_CONFIG.copy()
for arg in args:
base_config = dict(base_config, **arg)
return base_config
def delete_templates(self):
for template in [
'header.html',
'footer.html',
'cover.html',
'toc.xsl',
]:
self.remove_file(os.path.join(TEMPLATES_DIR, template))
def tearDown(self):
self.delete_templates()
self.remove_file(os.path.join(STATIC_DIR, 'style.css'))
def call_to_args(self, call):
args = tuple(call.call_args)[0]
kwargs = tuple(call.call_args)[1]
return args, kwargs

22
tests/test_args.py Normal file
View file

@ -0,0 +1,22 @@
from tests import BaseTestCase
from md_pdf.args import parse_args
class ArgParserTestCase(BaseTestCase):
def test_allows_no_args(self):
args = parse_args([])
self.assertFalse(args.update_csl)
self.assertEqual(args.verbose, 0)
def test_adds_verbosity(self):
args = parse_args(['-v'])
self.assertEqual(args.verbose, 1)
def test_chains_verbosity(self):
for i in range(1, 10):
args = parse_args(['-' + ('v' * i)])
self.assertEqual(args.verbose, i)
def test_csl_update(self):
args = parse_args(['--update-csl'])
self.assertTrue(args.update_csl)

239
tests/test_config.py Normal file
View file

@ -0,0 +1,239 @@
from tests import BaseTestCase
from md_pdf.config import read, validate
from md_pdf.exceptions import ConfigValidationException
from md_pdf.consts import CSL_DIR
from md_pdf.utils import remove_dir
import os
import datetime
from unittest import skipIf
class ReadConfigTestCase(BaseTestCase):
def test_reads_config(self):
config = read.load_config(os.path.abspath('test-files/mdp.yml'))
self.assertIsInstance(config, dict)
def test_throws_at_missing_config(self):
with self.assertRaises(ConfigValidationException):
read.load_config(os.path.abspath('non-existant'))
class ConfigValidatorBaseTestCase(BaseTestCase):
def setUp(self):
super().setUp()
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateSubmissionDateTestCase(ConfigValidatorBaseTestCase):
def test_transparent_to_datetime(self):
self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now()
validate.validate_config(self.BASE_VALID_CONFIG)
def test_transparent_to_date(self):
self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now().date()
validate.validate_config(self.BASE_VALID_CONFIG)
def test_transparent_to_time(self):
self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now().time()
validate.validate_config(self.BASE_VALID_CONFIG)
def test_valid_date_format(self):
for date in [
'2017-01-01',
'01-01-2017',
'1st jan 2017',
'1 january 2017'
]:
self.BASE_VALID_CONFIG['submission_date'] = date
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_date_format(self):
for date in [
'nothing',
'31-02-2017',
'01-2017-01',
'1st smarch 2017'
]:
self.BASE_VALID_CONFIG['submission_date'] = date
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateWordcountTestCase(ConfigValidatorBaseTestCase):
def test_boolean_values_only(self):
self.BASE_VALID_CONFIG['show_word_count'] = True
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['show_word_count'] = False
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_values(self):
for value in [
'True',
'False',
0,
1
]:
self.BASE_VALID_CONFIG['show_word_count'] = value
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateTOCTestCase(ConfigValidatorBaseTestCase):
def test_boolean_values_only(self):
self.BASE_VALID_CONFIG['toc'] = True
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['toc'] = False
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_values(self):
for value in [
'True',
'False',
0,
1
]:
self.BASE_VALID_CONFIG['toc'] = value
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateContextTestCase(ConfigValidatorBaseTestCase):
def test_should_be_dict(self):
for value in [
[],
'dict',
1
]:
self.BASE_VALID_CONFIG['context'] = value
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['context'] = {}
validate.validate_config(self.BASE_VALID_CONFIG)
def test_non_string_keys(self):
self.BASE_VALID_CONFIG['context'] = {
1: 'test'
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
def test_string_keys(self):
self.BASE_VALID_CONFIG['context'] = {
'1': 'test'
}
validate.validate_config(self.BASE_VALID_CONFIG)
def test_valid_values(self):
for value in [
'test',
1
]:
self.BASE_VALID_CONFIG['context'] = {
'test': value
}
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_values(self):
for value in [
[],
{}
]:
self.BASE_VALID_CONFIG['context'] = {
'test': value
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateBibliographyTestCase(ConfigValidatorBaseTestCase):
def test_contains_all_keys(self):
self.BASE_VALID_CONFIG['bibliography'] = {}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['bibliography'] = {
'references': 'test'
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['bibliography'] = {
'csl': 'test'
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
def test_valid_references(self):
self.BASE_VALID_CONFIG['bibliography'] = {
'references': 'non-existant',
'csl': 'chicago-author-date'
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['bibliography']['references'] = 'test-files/bib.yaml'
validate.validate_config(self.BASE_VALID_CONFIG)
@skipIf(not os.path.isdir(CSL_DIR), 'Missing CSL Files')
def test_valid_csl(self):
self.BASE_VALID_CONFIG['bibliography'] = {
'references': 'test-files/bib.yaml',
'csl': 'nothing'
}
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
self.BASE_VALID_CONFIG['bibliography']['csl'] = 'chicago-author-date'
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateInputTestCase(ConfigValidatorBaseTestCase):
def test_no_matches(self):
self.BASE_VALID_CONFIG['input'] = 'test-files/*.mp4'
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_glob(self):
self.BASE_VALID_CONFIG['input'] = 'test-files/'
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateOutputTestCase(ConfigValidatorBaseTestCase):
def tearDown(self):
super().tearDown()
remove_dir('test-files/test')
def test_creates_output_dir(self):
self.assertFalse(os.path.isdir('test-files/test'))
self.BASE_VALID_CONFIG['output_dir'] = 'test-files/test'
validate.validate_config(self.BASE_VALID_CONFIG)
self.assertTrue(os.path.isdir('test-files/test'))
def test_valid_output_formats(self):
for format in [
'html',
'pdf'
]:
self.BASE_VALID_CONFIG['output_formats'] = [format]
validate.validate_config(self.BASE_VALID_CONFIG)
def test_invalid_output_formats(self):
for format in [
'text',
'foo'
]:
self.BASE_VALID_CONFIG['output_formats'] = [format]
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
def test_part_invalid_format(self):
self.BASE_VALID_CONFIG['output_formats'] = ['html', 'foo']
with self.assertRaises(ConfigValidationException):
validate.validate_config(self.BASE_VALID_CONFIG)
class ValidateRequiredKeysTestCase(ConfigValidatorBaseTestCase):
def test_required_keys(self):
for key in validate.REQUIRED_KEYS:
base_config = self.BASE_VALID_CONFIG.copy()
del base_config[key]
with self.assertRaises(ConfigValidationException):
validate.validate_config(base_config)

98
tests/test_consts.py Normal file
View file

@ -0,0 +1,98 @@
from tests import BaseTestCase
from md_pdf import consts
from unittest import skipIf
import os
from urllib.parse import urlparse
import datetime
from freezegun import freeze_time
class ConstsTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.this_dir = os.path.dirname(__file__)
self.project_root = os.path.normpath(os.path.join(self.this_dir, '..'))
def test_project_dir(self):
self.assertEqual(
consts.PROJECT_DIR,
os.path.normpath(os.path.join(self.this_dir, '..', 'md_pdf'))
)
self.assertIn(consts.WORKING_DIR, consts.PROJECT_DIR)
def test_working_dir(self):
self.assertEqual(consts.WORKING_DIR, self.project_root)
@skipIf('APPDATA' not in os.environ, 'not on windows')
def test_windows_asset_dir(self):
self.assertIn(os.environ['APPDATA'], consts.ASSETS_DIR)
@skipIf('HOME' not in os.environ, 'not on windows')
def test_asset_dir(self):
self.assertEqual(consts.ASSETS_DIR, os.path.expanduser('~/.mdp'))
def test_csl_dir(self):
self.assertIn(consts.ASSETS_DIR, consts.CSL_DIR)
self.assertIn('csl', consts.CSL_DIR)
def test_templates_dir(self):
self.assertIn(consts.ASSETS_DIR, consts.TEMPLATES_DIR)
self.assertIn('templates', consts.TEMPLATES_DIR)
def test_static_dir(self):
self.assertIn(consts.ASSETS_DIR, consts.STATIC_DIR)
self.assertIn('static', consts.STATIC_DIR)
def test_internal_asset_dir(self):
self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_ASSETS_DIR)
self.assertIn('assets', consts.INTERNAL_ASSETS_DIR)
def test_internal_static_dir(self):
self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_STATIC_DIR)
self.assertIn('static', consts.INTERNAL_STATIC_DIR)
def test_internal_templates_dir(self):
self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_TEMPLATES_DIR)
self.assertIn('templates', consts.INTERNAL_TEMPLATES_DIR)
def test_config_file(self):
self.assertIn(consts.WORKING_DIR, consts.CONFIG_FILE)
self.assertIn('mdp.yml', consts.CONFIG_FILE)
def test_csl_download_link(self):
url = urlparse(consts.CSL_DOWNLOAD_LINK)
self.assertEqual(url.netloc, 'github.com')
self.assertTrue(url.path.endswith('master.zip'))
self.assertIn('citation-style-language/styles', url.path)
@freeze_time('2017-01-01')
def test_date_format(self):
now = datetime.datetime.now()
self.assertEqual(
now.strftime(consts.DATE_FORMAT),
'01 January 2017'
)
@freeze_time('2017-01-01T12:34')
def test_time_format(self):
now = datetime.datetime.now()
self.assertEqual(
now.strftime(consts.TIME_FORMAT),
'12:34'
)
@freeze_time('2017-01-01T12:34')
def test_time_format(self):
now = datetime.datetime.now()
self.assertEqual(
now.strftime(consts.DATETIME_FORMAT),
'01 January 2017 12:34'
)
def test_dirs_exist(self):
self.assertTrue(os.path.isdir(consts.ASSETS_DIR))
self.assertTrue(os.path.isdir(consts.TEMPLATES_DIR))
self.assertTrue(os.path.isdir(consts.STATIC_DIR))

77
tests/test_context.py Normal file
View file

@ -0,0 +1,77 @@
from tests import BaseTestCase
from md_pdf.build.context import get_context, EXTRA_CONTEXT
from md_pdf import consts
from datetime import datetime
from md_pdf import __version__
import os
class ExtraContextTestCase(BaseTestCase):
def test_directories(self):
self.assertEqual(EXTRA_CONTEXT['templates_dir'], consts.TEMPLATES_DIR)
self.assertEqual(EXTRA_CONTEXT['static_dir'], consts.STATIC_DIR)
self.assertEqual(EXTRA_CONTEXT['internal_templates_dir'], consts.INTERNAL_TEMPLATES_DIR)
self.assertEqual(EXTRA_CONTEXT['internal_static_dir'], consts.INTERNAL_STATIC_DIR)
def test_datetimes(self):
now = datetime.now()
self.assertEqual(EXTRA_CONTEXT['date'], now.strftime(consts.DATE_FORMAT))
self.assertEqual(EXTRA_CONTEXT['time'], now.strftime(consts.TIME_FORMAT))
self.assertEqual(EXTRA_CONTEXT['datetime'], now.strftime(consts.DATETIME_FORMAT))
def test_version(self):
self.assertEqual(EXTRA_CONTEXT['mdp_version'], __version__)
class ContextTestCase(BaseTestCase):
def test_context_contains_extra(self):
context = get_context(self.BASE_VALID_CONFIG, 'test')
for key in EXTRA_CONTEXT.keys():
self.assertIn(key, context)
def test_context_contains_context(self):
config = self.extend_config({
'context': {
'1': '2'
}
})
context = get_context(config, 'test')
self.assertEqual(context['1'], '2')
self.assertNotIn('context', context)
def test_context_contains_config(self):
context = get_context(self.BASE_VALID_CONFIG, 'test')
for key in self.BASE_VALID_CONFIG.keys():
self.assertIn(key, context)
def test_has_output_dir(self):
context = get_context(self.BASE_VALID_CONFIG, 'test')
self.assertEqual(context['output_dir'], os.path.abspath(self.BASE_VALID_CONFIG['output_dir']))
def test_word_count(self):
config = self.extend_config({
'show_word_count': True
})
context = get_context(config, 'testy test test')
self.assertEqual(context['word_count'], 3)
def test_transparent_datetime_for_submission_date(self):
for value in [
datetime.now().date(),
datetime.now().time(),
datetime.now()
]:
config = self.extend_config({
'submission_date': value
})
context = get_context(config, 'test')
self.assertEqual(context['submission_date'], value.strftime(consts.DATE_FORMAT))
def test_date_format(self):
config = self.extend_config({
'submission_date': '2017-01-01'
})
context = get_context(config, 'test')
self.assertEqual(context['submission_date'], '01 January 2017')

27
tests/test_jinja.py Normal file
View file

@ -0,0 +1,27 @@
from tests import BaseTestCase
from md_pdf.build.jinja import render_content
class ContentRendererTestCase(BaseTestCase):
def test_renders_template(self):
html = 'test {{ test }}'
output = render_content(html, self.extend_config({
'test': 'content'
}))
self.assertEqual(output, 'test content')
def test_changes_nothing(self):
html = 'test test'
output = render_content(html, self.extend_config({
'test': 'content'
}))
self.assertEqual(output, html)
def test_with_block(self):
html = """
{% with test = 'test' %}
{{ test }} thing
{% endwith %}
"""
output = render_content(html, self.BASE_VALID_CONFIG)
self.assertIn('test thing', output)

67
tests/test_pandoc.py Normal file
View file

@ -0,0 +1,67 @@
from tests import BaseTestCase
from md_pdf.build.pandoc import output_html, build_document
from md_pdf.build.md import read_files
from md_pdf.utils import remove_dir
import os
import glob
class ReadFileTestCase(BaseTestCase):
file_glob = 'test-files/*.md'
def test_reads_files(self):
files = read_files(self.file_glob)
self.assertNotEqual(files, '')
def test_contains_all_files(self):
files = read_files(self.file_glob)
for file in glob.iglob(self.file_glob):
with open(file) as f:
self.assertIn(f.read(), files)
class OutputHTMLTestCase(BaseTestCase):
output_dir = 'test-output'
def setUp(self):
super().setUp()
os.makedirs(self.output_dir)
def tearDown(self):
super().tearDown()
remove_dir(self.output_dir)
def test_outputs_file(self):
self.assertFalse(os.path.isfile(os.path.join(self.output_dir, 'output.html')))
output_html('test', self.output_dir)
self.assertTrue(os.path.isfile(os.path.join(self.output_dir, 'output.html')))
def test_outputs_correct_data(self):
output_html('test', self.output_dir)
with open(os.path.join(self.output_dir, 'output.html')) as f:
self.assertEqual(f.read(), 'test')
class BuildDocumentTestCase(BaseTestCase):
def test_parses_markdown(self):
doc = build_document('# test', None)
self.assertIn('<h1 id="test">test</h1>', doc)
def converts_nothing_if_plain(self):
doc = build_document('test', None)
self.assertIn('test', doc)
def test_bibliography(self):
bibliography = {
'references': 'test-files/bib.yaml',
'csl': 'chicago-author-date'
}
with open('test-files/2-pandoc.md') as f:
test_content = f.read()
doc = build_document(test_content, bibliography)
self.assertIn(
'<span class="citation">Doe (2005, 2006, 30; see also Doe and Roe 2007)</span> says blah.',
doc
)
self.assertIn('Doe, John. 2005.', doc)

50
tests/test_parser.py Normal file
View file

@ -0,0 +1,50 @@
from tests import BaseTestCase
from md_pdf.build import content
import os
class FixReferencesTitleTestCase(BaseTestCase):
def test_adds_reference_title(self):
html = '<div class="references"></div>'
output = content.fix_references_title(html, self.BASE_VALID_CONFIG)
self.assertIn('references-title', output)
self.assertIn('References', output)
def test_doesnt_modify_if_no_references(self):
html = 'test text'
output = content.fix_references_title(html, self.BASE_VALID_CONFIG)
self.assertNotIn('references-title', output)
self.assertNotIn('References', output)
class RelativeImageTestCase(BaseTestCase):
def test_makes_image_relative(self):
html = '<img src="test-files/test-image.png" />'
output = self.parse_html(content.make_images_relative(html, self.BASE_VALID_CONFIG))
self.assertEqual(output.find('img').attrs['src'], os.path.abspath('test-files/test-image.png'))
def test_leaves_remote_images(self):
html = '<img src="http://example.com/image.png" />'
output = self.parse_html(content.make_images_relative(html, self.BASE_VALID_CONFIG))
self.assertEqual(output.find('img').attrs['src'], 'http://example.com/image.png')
class AddBodyClassTestCase(BaseTestCase):
def test_adds_class(self):
html = '<body></body>'
output = self.parse_html(content.add_body_class(html, self.BASE_VALID_CONFIG))
self.assertEqual(output.body.attrs['class'], ['content'])
def test_doesnt_change(self):
html = 'test content'
output = content.add_body_class(html, self.BASE_VALID_CONFIG)
self.assertEqual(output, html)
class RenderTemplateTestCase(BaseTestCase):
def test_renders_template(self):
html = 'test {{ test }}'
output = content.render_template(html, self.extend_config({
'test': 'content'
}))
self.assertEqual(output, 'test content')

88
tests/test_pdf.py Normal file
View file

@ -0,0 +1,88 @@
from tests import BaseTestCase
import os
from md_pdf.build.pdf import export_pdf, TOC_OPTIONS, DEFAULT_MARGIN_VERTICAL, DEFAULT_MARGIN_HORIZONTAL
from unittest.mock import patch
from md_pdf.build.templates import FILE_NAME_FORMAT
from md_pdf.exceptions import PDFRenderException
import pdfkit
class PDFRendererTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.content = 'test content'
self.output_file_path = os.path.join(self.BASE_VALID_CONFIG['output_dir'], 'output.pdf')
self.assertFalse(os.path.isfile(self.output_file_path))
self.create_fake_templates()
def tearDown(self):
super().tearDown()
self.remove_file(self.output_file_path)
def test_renders(self):
export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertTrue(os.path.isfile(self.output_file_path))
def test_title(self):
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertEqual(context['title'], self.BASE_VALID_CONFIG['title'])
def test_replace_context(self):
self.BASE_VALID_CONFIG['context'] = {
'1': 2,
'2': '1'
}
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertEqual(context['replace'], [
('1', '2'),
('2', '1'),
])
def test_default_margins(self):
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertEqual(context['margin-top'], DEFAULT_MARGIN_VERTICAL)
self.assertEqual(context['margin-bottom'], DEFAULT_MARGIN_VERTICAL)
self.assertEqual(context['margin-left'], DEFAULT_MARGIN_HORIZONTAL)
self.assertEqual(context['margin-right'], DEFAULT_MARGIN_HORIZONTAL)
def test_override_margin(self):
self.BASE_VALID_CONFIG['context'] = {
'margin_vertical': '1cm',
'margin_horizontal': '2cm'
}
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertEqual(context['margin-top'], '1cm')
self.assertEqual(context['margin-bottom'], '1cm')
self.assertEqual(context['margin-left'], '2cm')
self.assertEqual(context['margin-right'], '2cm')
@patch.object(pdfkit, 'from_string')
def test_kit_call(self, pdf_render):
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertTrue(pdf_render.called)
args, kwargs = self.call_to_args(pdf_render)
self.assertEqual(args[0], self.content)
self.assertIn(self.output_file_path, args[1])
self.assertEqual(kwargs['options'], context)
self.assertTrue(kwargs['cover_first'])
self.assertEqual(kwargs['cover'], FILE_NAME_FORMAT.format('cover'))
self.assertEqual(kwargs['toc'], {})
@patch.object(pdfkit, 'from_string')
def test_toc(self, pdf_render):
self.BASE_VALID_CONFIG['toc'] = True
export_pdf(self.content, self.BASE_VALID_CONFIG)
args, kwargs = self.call_to_args(pdf_render)
self.assertEqual(kwargs['toc'], TOC_OPTIONS)
def test_fails_if_missing_templates(self):
self.remove_file(FILE_NAME_FORMAT.format('cover'))
with self.assertRaises(PDFRenderException):
export_pdf(self.content, self.BASE_VALID_CONFIG)
def test_files_exist(self):
context = export_pdf(self.content, self.BASE_VALID_CONFIG)
self.assertTrue(os.path.isfile(context['header-html']))
self.assertTrue(os.path.isfile(context['footer-html']))
self.assertEqual(context['header-html'], FILE_NAME_FORMAT.format('header'))
self.assertEqual(context['footer-html'], FILE_NAME_FORMAT.format('footer'))