summaryrefslogtreecommitdiff
path: root/markdown
diff options
context:
space:
mode:
authorWaylan Limberg <waylan.limberg@icloud.com>2015-03-23 18:59:07 -0400
committerWaylan Limberg <waylan.limberg@icloud.com>2015-04-22 19:53:48 -0400
commita08c7c100503ca48855df35e29afb45964894af1 (patch)
tree20e0fa23df7e97e8bec7e91b0e52996031f89018 /markdown
parent360c0f58a4599ae1dc08ab26ddd83b4c1fb1fd98 (diff)
downloadpython-markdown-a08c7c100503ca48855df35e29afb45964894af1.tar.gz
Moved testing framework into library.
The testing framework can now be used by third party extensions to run local tests using Python-Markdown's testing framework. All syntax tests are assumed to be in `cwd/tests/` (where 'cwd' is the current working directory) and its subdirectories. As previosly, UnitTests use normal discovery methods. Note that the actual tests themselves are not in the lib, only the framework. To run tests, from the appropriate directory (the parent dir of `tests/`) run `python -m markdown.test`. Note that the HTMLOutput plugin has been removed as I haven't used in in years.
Diffstat (limited to 'markdown')
-rw-r--r--markdown/test.py232
1 files changed, 232 insertions, 0 deletions
diff --git a/markdown/test.py b/markdown/test.py
new file mode 100644
index 0000000..28d9e71
--- /dev/null
+++ b/markdown/test.py
@@ -0,0 +1,232 @@
+import os
+import sys
+import markdown
+import codecs
+import difflib
+try:
+ import nose
+ from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
+except ImportError as e: # pragma: no cover
+ msg = e.args[0]
+ msg = msg + ". The nose testing framework is required to run the Python-" \
+ "Markdown tests. Run `pip install nose` to install the latest version."
+ e.args = (msg,) + e.args[1:]
+ raise
+try:
+ import tidylib
+except ImportError: # pragma: no cover
+ tidylib = None
+try:
+ import yaml
+except ImportError as e: # pragma: no cover
+ msg = e.args[0]
+ msg = msg + ". A YAML library is required to run the Python-Markdown " \
+ "tests. Run `pip install pyyaml` to install the latest version."
+ e.args = (msg,) + e.args[1:]
+ raise
+
+
+test_dir = os.path.abspath(os.path.join(os.getcwd(), 'tests'))
+
+
+class MarkdownSyntaxError(Exception):
+ pass
+
+
+class MarkdownPlugin(ErrorClassPlugin):
+ """ Add MarkdownSyntaxError and ensure proper formatting. """
+ mdsyntax = ErrorClass(
+ MarkdownSyntaxError,
+ label='MarkdownSyntaxError',
+ isfailure=True
+ )
+ enabled = True
+
+ def configure(self, options, conf):
+ self.conf = conf
+
+ def addError(self, test, err): # pragma: no cover
+ """ Ensure other plugins see the error by returning nothing here. """
+ pass
+
+ def formatError(self, test, err): # pragma: no cover
+ """ Remove unnessecary and unhelpful traceback from error report. """
+ et, ev, tb = err
+ if et.__name__ == 'MarkdownSyntaxError':
+ return et, ev, ''
+ return err
+
+
+class YamlConfig():
+ def __init__(self, defaults, filename):
+ """ Set defaults and load config file if it exists. """
+ self.DEFAULT_SECTION = 'DEFAULT'
+ self._defaults = defaults
+ self._config = {}
+ if os.path.exists(filename):
+ with codecs.open(filename, encoding="utf-8") as f:
+ self._config = yaml.load(f)
+
+ def get(self, section, option):
+ """ Get config value for given section and option key. """
+ if section in self._config and option in self._config[section]:
+ return self._config[section][option]
+ return self._defaults[option]
+
+ def get_section(self, file):
+ """ Get name of config section for given file. """
+ filename = os.path.basename(file)
+ if filename in self._config:
+ return filename
+ else:
+ return self.DEFAULT_SECTION
+
+ def get_args(self, file):
+ """ Get args to pass to markdown from config for a given file. """
+ args = {}
+ section = self.get_section(file)
+ if section in self._config:
+ for key in self._config[section].keys():
+ # Filter out args unique to testing framework
+ if key not in self._defaults.keys():
+ args[key] = self.get(section, key)
+ return args
+
+
+def get_config(dir_name):
+ """ Get config for given directory name. """
+ defaults = {
+ 'normalize': False,
+ 'skip': False,
+ 'input_ext': '.txt',
+ 'output_ext': '.html'
+ }
+ config = YamlConfig(defaults, os.path.join(dir_name, 'test.cfg'))
+ return config
+
+
+def normalize(text):
+ """ Normalize whitespace for a string of html using tidylib. """
+ output, errors = tidylib.tidy_fragment(text, options={
+ 'drop_empty_paras': 0,
+ 'fix_backslash': 0,
+ 'fix_bad_comments': 0,
+ 'fix_uri': 0,
+ 'join_styles': 0,
+ 'lower_literals': 0,
+ 'merge_divs': 0,
+ 'output_xhtml': 1,
+ 'quote_ampersand': 0,
+ 'newline': 'LF'
+ })
+ return output
+
+
+class CheckSyntax(object):
+ def __init__(self, description=None):
+ if description:
+ self.description = 'TestSyntax: "%s"' % description
+
+ def __call__(self, file, config):
+ """ Compare expected output to actual output and report result. """
+ cfg_section = config.get_section(file)
+ if config.get(cfg_section, 'skip'):
+ raise nose.plugins.skip.SkipTest('Test skipped per config.')
+ input_file = file + config.get(cfg_section, 'input_ext')
+ with codecs.open(input_file, encoding="utf-8") as f:
+ input = f.read()
+ output_file = file + config.get(cfg_section, 'output_ext')
+ with codecs.open(output_file, encoding="utf-8") as f:
+ # Normalize line endings
+ # (on windows, git may have altered line endings).
+ expected_output = f.read().replace("\r\n", "\n")
+ output = markdown.markdown(input, **config.get_args(file))
+ if tidylib and config.get(cfg_section, 'normalize'):
+ # Normalize whitespace with tidylib before comparing.
+ expected_output = normalize(expected_output)
+ output = normalize(output)
+ elif config.get(cfg_section, 'normalize'): # pragma: no cover
+ # Tidylib is not available. Skip this test.
+ raise nose.plugins.skip.SkipTest(
+ 'Test skipped. Tidylib not available on system.'
+ )
+ diff = [l for l in difflib.unified_diff(
+ expected_output.splitlines(True),
+ output.splitlines(True),
+ output_file,
+ 'actual_output.html',
+ n=3
+ )]
+ if diff: # pragma: no cover
+ raise MarkdownSyntaxError(
+ 'Output from "%s" failed to match expected '
+ 'output.\n\n%s' % (input_file, ''.join(diff))
+ )
+
+
+def TestSyntax():
+ for dir_name, sub_dirs, files in os.walk(test_dir):
+ # Get dir specific config settings.
+ config = get_config(dir_name)
+ # Loop through files and generate tests.
+ for file in files:
+ root, ext = os.path.splitext(file)
+ if ext == config.get(config.get_section(file), 'input_ext'):
+ path = os.path.join(dir_name, root)
+ check_syntax = CheckSyntax(
+ description=os.path.relpath(path, test_dir)
+ )
+ yield check_syntax, path, config
+
+
+def generate(file, config): # pragma: no cover
+ """ Write expected output file for given input. """
+ cfg_section = config.get_section(file)
+ if config.get(cfg_section, 'skip') or config.get(cfg_section, 'normalize'):
+ print('Skipping:', file)
+ return None
+ input_file = file + config.get(cfg_section, 'input_ext')
+ output_file = file + config.get(cfg_section, 'output_ext')
+ if not os.path.isfile(output_file) or \
+ os.path.getmtime(output_file) < os.path.getmtime(input_file):
+ print('Generating:', file)
+ markdown.markdownFromFile(input=input_file, output=output_file,
+ encoding='utf-8', **config.get_args(file))
+ else:
+ print('Already up-to-date:', file)
+
+
+def generate_all(): # pragma: no cover
+ """ Generate expected output for all outdated tests. """
+ for dir_name, sub_dirs, files in os.walk(test_dir):
+ # Get dir specific config settings.
+ config = get_config(dir_name)
+ # Loop through files and generate tests.
+ for file in files:
+ root, ext = os.path.splitext(file)
+ if ext == config.get(config.get_section(file), 'input_ext'):
+ generate(os.path.join(dir_name, root), config)
+
+
+def main():
+ if len(sys.argv) > 1 and sys.argv[1] == "update": # pragma: no cover
+ if len(sys.argv) > 2:
+ config = get_config(os.path.dirname(sys.argv[2]))
+ root, ext = os.path.splitext(sys.argv[2])
+ if ext == config.get(
+ config.get_section(os.path.basename(root)), 'input_ext'
+ ):
+ generate(root, config)
+ else:
+ print(
+ sys.argv[2],
+ 'does not have a valid file extension. Check config.'
+ )
+ else:
+ generate_all()
+ else:
+ nose.main(addplugins=[MarkdownPlugin()])
+
+
+if __name__ == '__main__': # pragma: no cover
+ main()