diff --git a/Makefile b/Makefile index 87be9b73b1358d6a686938474d99b903e9dcf7d7..61a301c630326cd89e1f8fdeab1e3b6faa6571f4 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ motivation.html : motivation.md _layouts/slides.html -o $@ $< ## unittest : Run unit test (for Python 2 and 3) -unittest: tools/check tools/validation_helpers.py tools/test_check.py +unittest: tools/check.py tools/validation_helpers.py tools/test_check.py cd tools/ && python2 test_check.py cd tools/ && python3 test_check.py diff --git a/css/swc.css b/css/swc.css index b29dc3f6d57c62c09586a80f522345467c2f84d2..73bb7268126974569e317d8a989fd5873d81607c 100644 --- a/css/swc.css +++ b/css/swc.css @@ -18,11 +18,6 @@ div.chapter h2 { font-style: italic; } -/* Objectives and key points */ -.objectives, .keypoints { - background-color: azure; -} - /* Things to fix. */ .fixme { text-decoration: underline; @@ -124,6 +119,14 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { padding: 0em 1em; } +/* Challenges */ +.challenge { + background-color: #CCFFCC; + border: 5px solid #CCFFCC; + margin: 1em 0; + padding: 0em 1em; +} + /* Things to fix. */ .fixme { text-decoration: underline; diff --git a/requirements.txt b/requirements.txt index 8551c266dad9c585188ffaeff5ac0d0d9e4b6b4c..695ebe4e86d8ae222a62b8cf06ac937f61720a9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ CommonMark -pandocfilters \ No newline at end of file +pandocfilters +PyYAML diff --git a/tools/check b/tools/check.py similarity index 93% rename from tools/check rename to tools/check.py index ecfaaba9724a5971dc5f08007fb0bca1b6f3a644..873ed2522290dd9f7ec94dc646026e0ec5474ab9 100755 --- a/tools/check +++ b/tools/check.py @@ -20,21 +20,8 @@ import os import re import sys -try: - # Code tested with CommonMark version 0.5.4; API may change - import CommonMark -except ImportError: - ERROR_MESSAGE = """This program requires the CommonMark python package. -Install using - - # pip install commonmark - -or - - # easy_install commonmark -""" - print(ERROR_MESSAGE) - sys.exit(1) +import CommonMark +import yaml import validation_helpers as vh @@ -101,9 +88,8 @@ class MarkdownValidator(object): valid = False return valid - def _validate_one_doc_header_row(self, text): + def _validate_one_doc_header_row(self, label, content): """Validate a single row of the document header section""" - label, content = text.split(":", 1) if label not in self.DOC_HEADERS: logging.warning( "In {0}: " @@ -116,8 +102,8 @@ class MarkdownValidator(object): if not validate_header: logging.error( "In {0}: " - "Document header field for label {1} " - "does not follow expected format".format(self.filename, label)) + "Contents of document header field for label {1} " + "do not follow expected format".format(self.filename, label)) return validate_header # Methods related to specific validation. Can override specific tests. @@ -127,21 +113,37 @@ class MarkdownValidator(object): Pass only if the header of the document contains the specified sections with the expected contents""" - # Header section should be wrapped in hrs + # Test: Header section should be wrapped in hrs has_hrs = self._validate_hrs() - # Labeled sections in the actual headers should match expected format header_node = self.ast.children[1] - test_headers = [self._validate_one_doc_header_row(s) - for s in header_node.strings] + header_text = '\n'.join(header_node.strings) + + # Parse headers as YAML. Don't check if parser returns None or str. + header_yaml = yaml.load(header_text) + if not isinstance(header_yaml, dict): + logging.error("In {0}: " + "Expected YAML markup with labels " + "{1}".format(self.filename, self.DOC_HEADERS.keys())) + return False + + # Test: Labeled YAML should match expected format + test_headers = [self._validate_one_doc_header_row(k, v) + for k, v in header_yaml.items()] - # Must have all expected header lines, and no others. - only_headers = (len(header_node.strings) == len(self.DOC_HEADERS)) + # Test: Must have all expected header lines, and no others. + only_headers = (len(header_yaml) == len(self.DOC_HEADERS)) - # Headings must appear in the order expected - valid_order = self._validate_section_heading_order() + # If expected headings are missing, print an informative message + missing_headings = [h for h in self.DOC_HEADERS + if h not in header_yaml] - return has_hrs and all(test_headers) and only_headers and valid_order + for h in missing_headings: + logging.error("In {0}: " + "Header section is missing expected " + "row {1}".format(self.filename, h)) + + return has_hrs and all(test_headers) and only_headers def _validate_section_heading_order(self, ast_node=None, headings=None): """Verify that section headings appear, and in the order expected""" @@ -344,8 +346,8 @@ class IndexPageValidator(MarkdownValidator): return super(IndexPageValidator, self)._validate_links(links_to_skip) def _run_tests(self): - tests = [self._validate_intro_section()] parent_tests = super(IndexPageValidator, self)._run_tests() + tests = [self._validate_intro_section()] return all(tests) and parent_tests @@ -397,9 +399,9 @@ class TopicPageValidator(MarkdownValidator): return False def _run_tests(self): + parent_tests = super(TopicPageValidator, self)._run_tests() tests = [self._validate_has_no_headings(), self._validate_learning_objective()] - parent_tests = super(TopicPageValidator, self)._run_tests() return all(tests) and parent_tests @@ -496,7 +498,7 @@ class LicensePageValidator(MarkdownValidator): def _run_tests(self): """Skip the base tests; just check md5 hash""" # TODO: This hash is specific to the license for english-language repo - expected_hash = '258aa6822fa77f7c49c37c3759017891' + expected_hash = 'cd5742b6596a1f2f35c602ad43fa24b2' m = hashlib.md5() try: m.update(self.markdown) @@ -658,7 +660,3 @@ def main(parsed_args_obj): if __name__ == "__main__": parsed_args = command_line() main(parsed_args) - - #### Sample of how validator is used directly - # validator = HomePageValidator('../index.md') - # print validator.validate() diff --git a/tools/test_check.py b/tools/test_check.py index 003e26e82ef8086335a4c1e14565ad1c0a58acd7..0451c8444d914aca6ab9fe2635575f49814dfd34 100644 --- a/tools/test_check.py +++ b/tools/test_check.py @@ -7,14 +7,11 @@ Some of these tests require looking for example files, which exist only on the gh-pages branch. Some tests may therefore fail on branch "core". """ - -import imp import logging import os import unittest -check = imp.load_source("check", # Import non-.py file - os.path.join(os.path.dirname(__file__), "check")) +import check # Make log messages visible to help audit test failures check.start_logging(level=logging.DEBUG) @@ -63,7 +60,6 @@ class TestIndexPage(BaseTemplateTest): layout: lesson title: Lesson Title -keywords: ["some", "key terms", "in a list"] Another section that isn't an HR """) @@ -74,7 +70,6 @@ Another section that isn't an HR """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()) @@ -83,16 +78,14 @@ keywords: ["some", "key terms", "in a list"] 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): + def test_fail_when_headers_not_yaml_dict(self): + """Fail when the headers can't be parsed to a dict of YAML data""" validator = self._create_validator("""--- -layout: lesson -title: Lesson Title -keywords: this is not a list +This will parse as a string, not a dictionary ---""") self.assertFalse(validator._validate_doc_headers()) @@ -158,7 +151,6 @@ Paragraph of introductory material. validator = self._create_validator("""--- layout: lesson title: Lesson Title -keywords: ["some", "key terms", "in a list"] --- Paragraph of introductory material. @@ -185,7 +177,6 @@ Paragraph of introductory material. validator = self._create_validator("""--- layout: lesson title: Lesson Title -keywords: ["some", "key terms", "in a list"] --- Paragraph of introductory material. @@ -200,7 +191,6 @@ Paragraph of introductory material. validator = self._create_validator("""--- layout: lesson title: Lesson Title -keywords: ["some", "key terms", "in a list"] --- Paragraph of introductory material. @@ -295,6 +285,16 @@ class TestTopicPage(BaseTemplateTest): SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "01-one.md") VALIDATOR = check.TopicPageValidator + def test_headers_fail_because_invalid_content(self): + """The value provided as YAML does not match the expected datatype""" + validator = self._create_validator("""--- +layout: lesson +title: Lesson Title +subtitle: A page +minutes: not a number +---""") + self.assertFalse(validator._validate_doc_headers()) + def test_sample_file_passes_validation(self): sample_validator = self.VALIDATOR(self.SAMPLE_FILE) res = sample_validator.validate()