Skip to content
Snippets Groups Projects
Commit 7df94bf9 authored by Raniere Silva's avatar Raniere Silva
Browse files

Merge pull request #35 from abought/gh-pages

Merge of new validator with tests
parents 541ccc25 0d5c61e0
No related merge requests found
......@@ -11,8 +11,8 @@ Paragraph of introductory material.
## Topics
1. [Topic Title 1](01-one.html)
2. [Topic Title 2](02-two.html)
1. [Topic Title One](01-one.html)
2. [Topic Title Two](02-two.html)
## Other Resources
......
This diff is collapsed.
#! /usr/bin/env python
import imp, logging, os, unittest
check = imp.load_source("check", # Import non-.py file
os.path.join(os.path.dirname(__file__), "check"))
# Make log messages visible to help audit test failures
check.start_logging(level=logging.DEBUG)
class BaseTemplateTest(unittest.TestCase):
"""Common methods for testing template validators"""
SAMPLE_FILE = "" # Path to a file that should pass all tests
VALIDATOR = check.MarkdownValidator
def setUp(self):
self.sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
def _create_validator(self, markdown):
"""Create validator object from markdown string; useful for failures"""
return self.VALIDATOR(markdown=markdown)
class TestAstHelpers(BaseTemplateTest):
SAMPLE_FILE = '../pages/index.md'
VALIDATOR = check.MarkdownValidator
def test_link_text_extracted(self):
"""Verify that link text and destination are extracted correctly"""
validator = self._create_validator("""[This is a link](discussion.html)""")
links = validator.ast.find_external_links(validator.ast.children[0])
dest, link_text = validator.ast.get_link_info(links[0])
self.assertEqual(dest, "discussion.html")
self.assertEqual(link_text, "This is a link")
class TestIndexPage(BaseTemplateTest):
"""Test the ability to correctly identify and validate specific sections
of a markdown file"""
SAMPLE_FILE = "../pages/index.md"
VALIDATOR = check.IndexPageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
def test_headers_missing_hrs(self):
validator = self._create_validator("""Blank row
layout: lesson
title: Lesson Title
keywords: ["some", "key terms", "in a list"]
Another section that isn't an HR
""")
self.assertFalse(validator._validate_doc_headers())
def test_headers_missing_a_line(self):
"""One of the required headers is missing"""
validator = self._create_validator("""---
layout: lesson
keywords: ["some", "key terms", "in a list"]
---""")
self.assertFalse(validator._validate_doc_headers())
# TESTS INVOLVING DOCUMENT HEADER SECTION
def test_headers_fail_with_other_content(self):
validator = self._create_validator("""---
layout: lesson
title: Lesson Title
keywords: ["some", "key terms", "in a list"]
otherline: Nothing
---""")
self.assertFalse(validator._validate_doc_headers())
def test_headers_fail_because_invalid_content(self):
validator = self._create_validator("""---
layout: lesson
title: Lesson Title
keywords: this is not a list
---""")
self.assertFalse(validator._validate_doc_headers())
# TESTS INVOLVING SECTION TITLES/HEADINGS
def test_index_has_valid_section_headings(self):
"""The provided index page"""
res = self.sample_validator._validate_section_heading_order()
self.assertTrue(res)
def test_index_fail_when_section_heading_absent(self):
res = self.sample_validator.ast.has_section_heading("Fake heading")
self.assertFalse(res)
def test_fail_when_section_heading_is_wrong_level(self):
"""All headings must be exactly level 2"""
validator = self._create_validator("""---
layout: page
title: Lesson Title
---
Paragraph of introductory material.
> ## Prerequisites
>
> A short paragraph describing what learners need to know
> before tackling this lesson.
### Topics
1. [Topic Title 1](01-one.html)
2. [Topic Title 2](02-two.html)
## Other Resources
* [Motivation](motivation.html)
* [Reference Guide](reference.html)
* [Next Steps](discussion.html)
* [Instructor's Guide](instructors.html)""")
self.assertFalse(validator._validate_section_heading_order())
def test_fail_when_section_headings_in_wrong_order(self):
validator = self._create_validator("""---
layout: lesson
title: Lesson Title
keywords: ["some", "key terms", "in a list"]
---
Paragraph of introductory material.
> ## Prerequisites
>
> A short paragraph describing what learners need to know
> before tackling this lesson.
## Other Resources
* [Motivation](motivation.html)
* [Reference Guide](reference.html)
* [Instructor's Guide](instructors.html)
## Topics
* [Topic Title 1](01-one.html)
* [Topic Title 2](02-two.html)""")
self.assertFalse(validator._validate_section_heading_order())
def test_pass_when_prereq_section_has_correct_heading_level(self):
validator = self._create_validator("""---
layout: lesson
title: Lesson Title
keywords: ["some", "key terms", "in a list"]
---
Paragraph of introductory material.
> ## Prerequisites
>
> A short paragraph describing what learners need to know
> before tackling this lesson.
""")
self.assertTrue(validator._validate_intro_section())
def test_fail_when_prereq_section_has_incorrect_heading_level(self):
validator = self._create_validator("""---
layout: lesson
title: Lesson Title
keywords: ["some", "key terms", "in a list"]
---
Paragraph of introductory material.
> # Prerequisites
>
> A short paragraph describing what learners need to know
> before tackling this lesson.
""")
self.assertFalse(validator._validate_intro_section())
# TESTS INVOLVING LINKS TO OTHER CONTENT
def test_file_links_validate(self):
res = self.sample_validator._validate_links()
self.assertTrue(res)
def test_html_link_to_extant_md_file_passes(self):
"""Verify that an HTML link with corresponding MD file will pass"""
validator = self._create_validator("""[Topic Title One](01-one.html)""")
self.assertTrue(validator._validate_links())
def test_html_link_with_anchor_to_extant_md_passes(self):
"""Verify that link is identified correctly even if to page anchor
For now this just tests that the regex handles #anchors.
It doesn't validate that the named anchor exists in the md file
"""
validator = self._create_validator("""[Topic Title One](01-one.html#anchor)""")
self.assertTrue(validator._validate_links())
def test_inpage_anchor_passes_validation(self):
"""Links that reference anchors within the page should be ignored"""
# TODO: Revisit once anchor rules are available
validator = self._create_validator("""Most databases also support Booleans and date/time values;
SQLite uses the integers 0 and 1 for the former, and represents the latter as discussed [earlier](#a:dates).""")
self.assertTrue(validator._validate_links())
def test_missing_markdown_file_fails_validation(self):
"""Fail validation when an html file is linked without corresponding
markdown file"""
validator = self._create_validator("""[Broken link](nonexistent.html)""")
self.assertFalse(validator._validate_links())
def test_website_link_ignored_by_validator(self):
"""Don't look for markdown if the file linked isn't local-
remote website links are ignored"""
validator = self._create_validator("""[Broken link](http://website.com/filename.html)""")
self.assertTrue(validator._validate_links())
def test_malformed_website_link_fails_validator(self):
"""If the link isn't prefixed by http(s):// or ftp://, fail.
This is because there are a lot of edge cases in distinguishing
between filenames and URLs: err on the side of certainty."""
validator = self._create_validator("""[Broken link](www.website.com/filename.html)""")
self.assertFalse(validator._validate_links())
def test_finds_image_asset(self):
"""Image asset is found"""
validator = self._create_validator(
"""![this is the image's title](fig/example.svg "this is the image's alt text")""")
self.assertTrue(validator._validate_links())
def test_image_asset_not_found(self):
"""Image asset can't be found if path is invalid"""
validator = self._create_validator(
"""![this is the image's title](fig/exemple.svg "this is the image's alt text")""")
self.assertFalse(validator._validate_links())
def test_non_html_link_finds_csv(self):
"""Look for CSV file in appropriate folder"""
validator = self._create_validator(
"""Use [this CSV](data/data.csv) for the exercise.""")
self.assertTrue(validator._validate_links())
def test_non_html_links_are_path_sensitive(self):
"""Fails to find CSV file with wrong path."""
validator = self._create_validator(
"""Use [this CSV](data.csv) for the exercise.""")
self.assertFalse(validator._validate_links())
class TestTopicPage(BaseTemplateTest):
"""Verifies that the topic page validator works as expected"""
SAMPLE_FILE = "../pages/01-one.md"
VALIDATOR = check.TopicPageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
class TestMotivationPage(BaseTemplateTest):
"""Verifies that the instructors page validator works as expected"""
SAMPLE_FILE = "../pages/motivation.md"
VALIDATOR = check.MotivationPageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
class TestReferencePage(BaseTemplateTest):
"""Verifies that the reference page validator works as expected"""
SAMPLE_FILE = "../pages/reference.md"
VALIDATOR = check.ReferencePageValidator
def test_missing_glossary_definition(self):
validator = self._create_validator("")
self.assertFalse(validator._validate_glossary_entry(
["Key word"]))
def test_missing_colon_at_glossary_definition(self):
validator = self._create_validator("")
self.assertFalse(validator._validate_glossary_entry(
["Key word", "Definition of term"]))
def test_wrong_indentation_at_glossary_definition(self):
validator = self._create_validator("")
self.assertFalse(validator._validate_glossary_entry(
["Key word", ": Definition of term"]))
def test_wrong_continuation_at_glossary_definition(self):
validator = self._create_validator("")
self.assertFalse(validator._validate_glossary_entry(
["Key word", ": Definition of term", "continuation"]))
def test_valid_glossary_definition(self):
validator = self._create_validator("")
self.assertTrue(validator._validate_glossary_entry(
["Key word", ": Definition of term", " continuation"]))
def test_only_definitions_can_appear_after_glossary_heading(self):
validator = self._create_validator("""## Glossary
Key Word 1
: Definition of first term
Paragraph
Key Word 2
: Definition of second term
""")
self.assertFalse(validator._validate_glossary())
def test_glossary(self):
validator = self._create_validator("""## Glossary
Key Word 1
: Definition of first term
Key Word 2
: Definition of second term
""")
self.assertTrue(validator._validate_glossary())
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
class TestInstructorPage(BaseTemplateTest):
"""Verifies that the instructors page validator works as expected"""
SAMPLE_FILE = "../pages/instructors.md"
VALIDATOR = check.InstructorPageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
class TestLicensePage(BaseTemplateTest):
SAMPLE_FILE = '../pages/LICENSE.md'
VALIDATOR = check.LicensePageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
def test_modified_file_fails_validation(self):
with open(self.SAMPLE_FILE, 'rU') as f:
orig_text = f.read()
mod_text = orig_text.replace("The", "the")
validator = self._create_validator(mod_text)
self.assertFalse(validator.validate())
class TestDiscussionPage(BaseTemplateTest):
SAMPLE_FILE = '../pages/discussion.md'
VALIDATOR = check.DiscussionPageValidator
def test_sample_file_passes_validation(self):
res = self.sample_validator.validate()
self.assertTrue(res)
if __name__ == "__main__":
unittest.main()
#! /usr/bin/env python
import json
import logging
import re
import sys
try: # Hack to make codebase compatible with python 2 and 3
basestring
except NameError:
basestring = str
# Common validation functions
def is_list(text):
"""Validate whether the provided string can be converted to python list"""
text = text.strip()
try:
text_as_list = json.loads(text)
except ValueError:
logging.debug("Could not convert string to python object: {0}".format(text))
return False
return isinstance(text_as_list, list)
def is_str(text):
"""Validate whether the input is a non-blank python string"""
return isinstance(text, basestring) and len(text) > 0
def is_numeric(text):
"""Validate whether the string represents a number (including unicode)"""
try:
float(text)
return True
except ValueError:
return False
#### Text cleanup functions, pre-validation
def strip_attrs(s):
"""Strip attributes of the form {.name} from a markdown title string"""
return re.sub(r"\s\{\..*?\}", "", s)
def get_css_class(s):
"""Return any and all CSS classes (when a line is suffixed by {.classname})
Returns empty list when """
return re.findall("\{\.(.*?)\}", s)
### Helper objects
class CommonMarkHelper(object):
"""Basic helper functions for working with the internal abstract syntax
tree produced by CommonMark parser"""
def __init__(self, ast):
self.data = ast
self.children = self.data.children
def get_doc_header_title(self):
"""Helper method for SWC templates: get the document title from
the YAML headers"""
doc_headers = self.data.children[1] # Throw index error if none found
for s in doc_headers.strings:
label, contents = s.split(":", 1)
if label.lower() == "title":
return contents.strip()
# If title not found, return an empty string for display purposes
return ''
def get_doc_header_subtitle(self):
"""Helper method for SWC templates: get the document title from
the YAML headers"""
doc_headers = self.data.children[1] # Throw index error if none found
for s in doc_headers.strings:
label, contents = s.split(":", 1)
if label.lower() == "subtitle":
return contents.strip()
# If title not found, return an empty string for display purposes
return ''
def get_block_titled(self, title, heading_level=2, ast_node=None):
"""Examine children. Return all children of the given node that:
a) are blockquoted elements, and
b) contain a heading with the specified text, at the specified level.
For example, this can be used to find the "Prerequisites" section
in index.md
Returns empty list if no appropriate node is found"""
if ast_node is None:
ast_node = self.data
return [n for n in ast_node.children
if self.is_block(n) and
self.has_section_heading(
title,
ast_node=n,
heading_level=heading_level,
show_msg=False)]
def get_section_headings(self, ast_node=None):
"""Returns a list of ast nodes that are headings"""
if ast_node is None:
ast_node = self.data
return [n for n in ast_node.children if self.is_heading(n)]
def get_link_info(self, link_node):
"""Given a link node, return the link title and destination"""
if not self.is_external(link_node):
raise TypeError("Cannot apply this method to something that is not a link")
dest = link_node.destination
try:
link_text = link_node.label[0].c
except:
link_text = None
return dest, link_text
def find_external_links(self, ast_node=None):
"""Recursive function that locates all references to external content
under specified node. (links or images)"""
ast_node = ast_node or self.data
# Link can be node itself, or hiding in inline content
links = [n for n in ast_node.inline_content
if self.is_external(n)]
if self.is_external(ast_node):
links.append(ast_node)
# Also look for links in sub-nodes
for n in ast_node.children:
links.extend(self.find_external_links(n))
return links
def has_section_heading(self, section_title, ast_node=None,
heading_level=2, limit=sys.maxsize, show_msg=True):
"""Does the file contain (<= x copies of) specified heading text?
Will strip off any CSS attributes when looking for the section title"""
if ast_node is None:
ast_node = self.data
num_nodes = len([n for n in self.get_section_headings(ast_node)
if (strip_attrs(n.strings[0]) == section_title)
and (n.level == heading_level)])
# Suppress error msg if used as a helper method
if show_msg and num_nodes == 0:
logging.error("Document does not contain the specified "
"heading: {0}".format(section_title))
elif show_msg and num_nodes > limit:
logging.error("Document must not contain more than {0} copies of"
" the heading {1}".format(limit, section_title or 0))
elif show_msg:
logging.info("Verified that document contains the specified"
" heading: {0}".format(section_title))
return (0 < num_nodes <= limit)
def has_number_children(self, ast_node,
exact=None, minc=0, maxc=sys.maxsize):
"""Does the specified node (such as a bulleted list) have the expected
number of children?"""
if exact: # If specified, must have exactly this number of children
minc = maxc = exact
return (minc <= len(ast_node.children) <= maxc)
# Helpers, in case the evolving CommonMark spec changes the names of nodes
def is_hr(self, ast_node):
"""Is the node a horizontal rule (hr)?"""
return ast_node.t == 'HorizontalRule'
def is_heading(self, ast_node):
"""Is the node a heading/ title?"""
return ast_node.t == "ATXHeader"
def is_paragraph(self, ast_node):
"""Is the node a paragraph?"""
return ast_node.t == "Paragraph"
def is_list(self, ast_node):
"""Is the node a list? (ordered or unordered)"""
return ast_node.t == "List"
def is_link(self, ast_node):
"""Is the node a link?"""
return ast_node.t == "Link"
def is_external(self, ast_node):
"""Does the node reference content outside the file? (image or link)"""
return ast_node.t in ("Link", "Image")
def is_block(self, ast_node):
"""Is the node a BlockQuoted element?"""
return ast_node.t == "BlockQuote"
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment