pyWeb Literate Programming 3.2 - Test Suite

Yet Another Literate Programming Tool

Contents

Introduction

There are two levels of testing in this document.

Other testing, like performance or security, is possible. But for this application, not very interesting.

This doument builds a complete test suite, test.py.

MacBookPro-SLott:test slott$ python3.3 ../pyweb.py pyweb_test.w
INFO:Application:Setting root log level to 'INFO'
INFO:Application:Setting command character to '@'
INFO:Application:Weaver RST
INFO:Application:load, tangle and weave 'pyweb_test.w'
INFO:LoadAction:Starting Load
INFO:WebReader:Including 'intro.w'
WARNING:WebReader:Unknown @-command in input: "@'"
INFO:WebReader:Including 'unit.w'
INFO:WebReader:Including 'func.w'
INFO:WebReader:Including 'combined.w'
INFO:TangleAction:Starting Tangle
INFO:TanglerMake:Tangling 'test_unit.py'
INFO:TanglerMake:No change to 'test_unit.py'
INFO:TanglerMake:Tangling 'test_loader.py'
INFO:TanglerMake:No change to 'test_loader.py'
INFO:TanglerMake:Tangling 'test.py'
INFO:TanglerMake:No change to 'test.py'
INFO:TanglerMake:Tangling 'page-layout.css'
INFO:TanglerMake:No change to 'page-layout.css'
INFO:TanglerMake:Tangling 'docutils.conf'
INFO:TanglerMake:No change to 'docutils.conf'
INFO:TanglerMake:Tangling 'test_tangler.py'
INFO:TanglerMake:No change to 'test_tangler.py'
INFO:TanglerMake:Tangling 'test_weaver.py'
INFO:TanglerMake:No change to 'test_weaver.py'
INFO:WeaveAction:Starting Weave
INFO:RST:Weaving 'pyweb_test.rst'
INFO:RST:Wrote 3173 lines to 'pyweb_test.rst'
INFO:WeaveAction:Finished Normally
INFO:Application:Load 1911 lines from 5 files in 0.05 sec., Tangle 138 lines in 0.03 sec., Weave 3173 lines in 0.02 sec.
MacBookPro-SLott:test slott$ PYTHONPATH=.. python3.3 test.py
ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found
ERROR:WebReader:Errors in included file test8_inc.tmp, output is incomplete.
.ERROR:WebReader:At ('test1.w', 8): expected ('@{',), found '@o'
ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)
ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)
.............................................................................
----------------------------------------------------------------------
Ran 78 tests in 0.025s

OK
MacBookPro-SLott:test slott$ rst2html.py pyweb_test.rst pyweb_test.html

Unit Testing

There are several broad areas of unit testing. There are the 34 classes in this application. However, it isn't really necessary to test everyone single one of these classes. We'll decompose these into several hierarchies.

This gives us the following outline for unit testing.

test_unit.py (1) =

Unit Test overheads: imports, etc. (43)Unit Test of Emitter class hierarchy (2)Unit Test of Chunk class hierarchy (10)Unit Test of Chunk References (22)Unit Test of Command class hierarchy (23)Unit Test of Web class (32)Unit Test of WebReader class (33)Unit Test of Action class hierarchy (37)Unit Test of Application class (42)Unit Test main (45)
test_unit.py (1).

Emitter Tests

The emitter class hierarchy produces output files; either woven output which uses templates to generate proper markup, or tangled output which precisely follows the document structure.

Unit Test of Emitter class hierarchy (2) =

Unit Test Mock Chunk class (4)Unit Test of Emitter Superclass (3)Unit Test of Weaver subclass of Emitter (5)Unit Test of LaTeX subclass of Emitter (6)Unit Test of HTML subclass of Emitter (7)Unit Test of Tangler subclass of Emitter (8)Unit Test of TanglerMake subclass of Emitter (9)
Unit Test of Emitter class hierarchy (2). Used by → test_unit.py (1).

The Emitter superclass is designed to be extended. The test creates a subclass to exercise a few key features. The default emitter is Tangler-like.

Unit Test of Emitter Superclass (3) =

class EmitterExtension(pyweb.Emitter):
    mock_emit = Mock()
    def emit(self, web: pyweb.Web) -> None:
        self.mock_emit(web)

class TestEmitter(unittest.TestCase):
    def setUp(self) -> None:
        self.output = Path("TestEmitter.out")
        self.emitter = EmitterExtension(self.output)
        self.web = Mock(name="mock web")
    def test_emitter_should_open_close_write(self) -> None:
        self.emitter.emit(self.web)
        self.emitter.mock_emit.called_once_with(self.web)
        self.assertEqual(self.emitter.output, self.output)
Unit Test of Emitter Superclass (3). Used by → Unit Test of Emitter class hierarchy (2).

A mock Chunk is a Chunk-like object that we can use to test Weavers.

Some tests will create multiple chunks. To keep their state separate, we define a function to return each mocked Chunk instance as a new Mock object. The overall MockChunk class, uses a side effect to invoke the the mock_chunk_instance() function.

The write_closure() is a function that calls the Tangler.write() method. This is not consistent with best unit testing practices. It is merely a hold-over from an older testing strategy. The mock call history to the tangle() method of each Chunk instance is a better test strategy.

TODO: Simplify the following definition. A great deal of these features are legacy definitions.

Unit Test Mock Chunk class (4) =

def mock_chunk_instance(name: str, seq: int, location: tuple[str, int]) -> Mock:
    chunk = Mock(
        wraps=pyweb.Chunk,
        full_name=name,
        seq=seq,
        location=location,
        commands=[],
        referencedBy=None,
        references=0,
        def_names=[],
        path=None,
        tangle=Mock(),
        type_is=Mock(side_effect=lambda x: x == "Chunk"),
        # reference_indent=Mock(),
        # reference_dedent=Mock(),
    )
    chunk.name = name
    return chunk

MockChunk = Mock(
    name="Chunk class",
    side_effect=mock_chunk_instance
)

def mock_web() -> pyweb.Web:
    def tangle_method(aTangler: pyweb.Tangler, target: TextIO) -> None:
        aTangler.codeBlock(target, "Mocked Tangle Output\n")

    mock_file = Mock(full_name="sample.out", seq=1)
    mock_file.name = "sample.out"
    mock_output = Mock(full_name="named chunk", seq=2, def_list=[3])
    mock_output.name = "named chunk"
    mock_uid_1 = Mock(userid="user_id_1", ref_list=[mock_output])
    mock_uid_2 = Mock(userid="user_id_2", ref_list=[mock_output])
    mock_ref = Mock(typeid=pyweb.TypeId(), full_name="named chunk", seq=42)
    mock_ref.typeid.__set_name__(pyweb.ReferenceCommand, "typeid")
    mock_ref.name = "named..."

    c_0 = Mock(
        name="mock Chunk",
        type_is=Mock(side_effect = lambda n: n == "Chunk"),
        commands=[
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.TextCommand, "typeid"),
                text="text with |char| untouched.",
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.TextCommand, "typeid"),
                text="\n",
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.FileXrefCommand, "typeid"),
                location=1,
                files=[mock_file],
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.TextCommand, "typeid"),
                text="\n",
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.MacroXrefCommand, "typeid"),
                location=2,
                macros=[mock_output],
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.TextCommand, "typeid"),
                text="\n",
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.UserIdXrefCommand, "typeid"),
                location=3,
                userids=[mock_uid_1, mock_uid_2]
            ),
        ],
        referencedBy=None,
    )
    c_1 = Mock(
        name="mock OutputChunk",
        type_is=Mock(side_effect = lambda n: n == "OutputChunk"),
        seq=42,
        full_name="sample.out",
        commands=[
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.CodeCommand, "typeid"),
                text="|char| `code` *em* _em_",
                tangle=Mock(side_effect=tangle_method),
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.CodeCommand, "typeid"),
                text="\n",
                tangle=Mock(),
            ),
            mock_ref,
        ],
        def_names=["some_name"],
        referencedBy=None,
    )
    c_2 = Mock(
        name="mock NamedChunk",
        type_is=Mock(side_effect = lambda n: n == "NamedChunk"),
        seq=42,
        full_name="named chunk",
        commands=[
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.CodeCommand, "typeid"),
                text="|char| `code` *em* _em_",
            ),
            Mock(
                typeid=pyweb.TypeId().__set_name__(pyweb.CodeCommand, "typeid"),
                text="\n",
                tangle=Mock(),
            ),
        ],
        def_names=["another_name"],
        referencedBy=c_1
    )
    web = Mock(
        name="mock web",
        web_path=Path("TestWeaver.w"),
        chunks=[c_0, c_1, c_2],
    )
    web.chunks[1].name="sample.out"
    web.chunks[2].name="named..."
    web.files = [web.chunks[1]]
    return web
Unit Test Mock Chunk class (4). Used by → Unit Test of Emitter class hierarchy (2).

The default Weaver is an Emitter that uses templates to produce RST markup.

Unit Test of Weaver subclass of Emitter (5) =

def test_rst_quote_rules():
    assert pyweb.rst_quote_rules("|char| `code` *em* _em_") == r"\|char\| \`code\` \*em\* \_em\_"

def test_html_quote_rules():
    assert pyweb.html_quote_rules("a & b < c > d") == r"a &amp; b &lt; c &gt; d"


class TestWeaver(unittest.TestCase):
    def setUp(self) -> None:
        self.filepath = Path.cwd()
        self.weaver = pyweb.Weaver(self.filepath)
        self.weaver.set_markup("rst")
        # self.weaver.reference_style = pyweb.SimpleReference()  # Remove this
        self.output_path = self.filepath / "TestWeaver.rst"
        self.web = mock_web()
        self.maxDiff = None

    def tearDown(self) -> None:
        try:
            self.output_path.unlink()
        except OSError:
            pass

    def test_weaver_functions_generic(self) -> None:
        self.weaver.emit(self.web)
        result = self.output_path.read_text()
        expected = ('text with |char| untouched.\n'
             ':sample.out:\n'
             '    → `sample.out (1)`_\n'
             ':named chunk:\n'
             '    → ` ()`_\n'
             '\n'
             '\n'
             ':user_id_1:\n'
             '    → `named chunk (2)`_\n'
             '\n'
             ':user_id_2:\n'
             '    → `named chunk (2)`_\n'
             '\n'
             '\n'
            '..  _`sample.out (42)`:\n'
            '..  rubric:: sample.out (42) =\n'
            '..  parsed-literal::\n'
            '    :class: code\n'
            '\n'
            '    \\|char\\| \\`code\\` \\*em\\* \\_em\\_\n'
            '    \n'
            '    → `named chunk (42)`_\n'
            '..\n'
            '\n'
            '..  container:: small\n'
            '\n'
            '    ∎ *sample.out (42)*.\n'
            '    \n'
            '\n'
            '\n'
            '..  _`named chunk (42)`:\n'
            '..  rubric:: named chunk (42) =\n'
            '..  parsed-literal::\n'
            '    :class: code\n'
            '\n'
            '    \\|char\\| \\`code\\` \\*em\\* \\_em\\_\n'
            '    \n'
            '\n'
            '..\n'
            '\n'
            '..  container:: small\n'
            '\n'
            '    ∎ *named chunk (42)*.\n'
            '    Used by     → `sample.out (42)`_.\n'
            '\n')
        self.assertEqual(expected, result)
Unit Test of Weaver subclass of Emitter (5). Used by → Unit Test of Emitter class hierarchy (2).

A significant fraction of the various subclasses of weaver are simply expansion of templates. There's no real point in testing the template expansion, since that's more easily tested by running a document through pyweb and looking at the results.

We'll examine a few features of the LaTeX templates.

Unit Test of LaTeX subclass of Emitter (6) =

class TestLaTeX(unittest.TestCase):
    def setUp(self) -> None:
        self.weaver = pyweb.Weaver()
        self.weaver.set_markup("tex")
        # self.weaver.reference_style = pyweb.SimpleReference()  # Remove this
        self.filepath = Path("testweaver")
        self.aFileChunk = MockChunk("File", 123, ("sample.w", 456))
        self.aFileChunk.referencedBy = [ ]
        self.aChunk = MockChunk("Chunk", 314, ("sample.w", 789))
        self.aChunk.type_is = Mock(side_effect=lambda n: n == "OutputChunk")
        self.aChunk.referencedBy = [self.aFileChunk,]
        self.aChunk.references = [(self.aFileChunk.name, self.aFileChunk.seq)]

    def tearDown(self) -> None:
        try:
            self.filepath.with_suffix(".tex").unlink()
        except OSError:
            pass

    def test_weaver_functions_latex(self) -> None:
        result = pyweb.latex_quote_rules("\\end{Verbatim}")
        self.assertEqual("\\end\\,{Verbatim}", result)
        web = Mock(chunks=[self.aChunk])
        result = list(self.weaver.generate_text(web))
        expected = [
            '\n'
            '\\label{pyweb-314}\n'
            '\\begin{flushleft}\n'
            '\\textit{Code example Chunk (314)}\n'
            '\\begin{Verbatim}[commandchars=\\\\\\{\\},codes={\\catcode`$$=3\\catcode`^=7},frame=single]',
            '\n'
            '\\end{Verbatim}\n'
            '\\end{flushleft}\n'
        ]
        self.assertEqual(expected, result)
Unit Test of LaTeX subclass of Emitter (6). Used by → Unit Test of Emitter class hierarchy (2).

We'll examine a few features of the HTML templates.

Unit Test of HTML subclass of Emitter (7) =

class TestHTML(unittest.TestCase):
    def setUp(self) -> None:
        self.maxDiff = None
        self.weaver = pyweb.Weaver( )
        self.weaver.set_markup("html")
        # self.weaver.reference_style = pyweb.SimpleReference()  # Remove this
        self.filepath = Path("testweaver")
        self.aFileChunk = MockChunk("File", 123, ("sample.w", 456))
        self.aFileChunk.referencedBy = []
        self.aChunk = MockChunk("Chunk", 314, ("sample.w", 789))
        self.aChunk.type_is = Mock(side_effect=lambda n: n == "OutputChunk")
        self.aChunk.referencedBy = [self.aFileChunk,]
        self.aChunk.references = [(self.aFileChunk.name, self.aFileChunk.seq)]

    def tearDown(self) -> None:
        try:
            self.filepath.with_suffix(".html").unlink()
        except OSError:
            pass

    def test_weaver_functions_html(self) -> None:
        result = pyweb.html_quote_rules("a < b && c > d")
        self.assertEqual("a &lt; b &amp;&amp; c &gt; d", result)
        web = Mock(chunks=[self.aChunk])
        result = list(self.weaver.generate_text(web))
        expected = [
            '\n'
            '<a name="pyweb_314"></a>\n'
            "<!--line number ('sample.w', 789)-->\n"
            '<p><em>Chunk (314)</em> =</p>\n'
            '<pre><code>',
            '\n'
            '</code></pre>\n'
            '<p>&#8718; <em>Chunk (314)</em>.\n'
            'Used by &rarr;<a href="#pyweb_"><em> ()</em></a>.\n'
            '</p> \n'
        ]
        self.assertEqual(expected, result)
Unit Test of HTML subclass of Emitter (7). Used by → Unit Test of Emitter class hierarchy (2).

A Tangler emits the various named source files in proper format for the desired compiler and language.

Unit Test of Tangler subclass of Emitter (8) =

class TestTangler(unittest.TestCase):
    def setUp(self) -> None:
        self.filepath = Path.cwd()
        self.tangler = pyweb.Tangler(self.filepath)

    def tearDown(self) -> None:
        try:
            target = self.filepath / "sample.out"
            target.unlink()
        except FileNotFoundError:
            pass

    def test_tangler_should_codeBlock(self) -> None:
        target = io.StringIO()
        self.tangler.codeBlock(target, "Some")
        self.tangler.codeBlock(target, " Code")
        self.tangler.codeBlock(target, "\n")
        output = target.getvalue()
        self.assertEqual("Some Code\n", output)

    def test_tangler_should_indent(self) -> None:
        target = io.StringIO()
        self.tangler.codeBlock(target, "Begin\n")
        self.tangler.addIndent(4)
        self.tangler.codeBlock(target, "More Code\n")
        self.tangler.clrIndent()
        self.tangler.codeBlock(target, "End\n")
        output = target.getvalue()
        self.assertEqual("Begin\n    More Code\nEnd\n", output)

    def test_tangler_should_noindent(self) -> None:
        target = io.StringIO()
        self.tangler.codeBlock(target, "Begin")
        self.tangler.codeBlock(target, "\n")
        self.tangler.setIndent(0)
        self.tangler.codeBlock(target, "More Code")
        self.tangler.codeBlock(target, "\n")
        self.tangler.clrIndent()
        self.tangler.codeBlock(target, "End")
        self.tangler.codeBlock(target, "\n")
        output = target.getvalue()
        self.assertEqual("Begin\nMore Code\nEnd\n", output)
Unit Test of Tangler subclass of Emitter (8). Used by → Unit Test of Emitter class hierarchy (2).

A TanglerMake uses a cheap hack to see if anything changed. It creates a temporary file and then does a complete (slow, expensive) file difference check. If the file is different, the old version is replaced with the new version. If the file content is the same, the old version is left intact with all of the operating system creation timestamps untouched.

Unit Test of TanglerMake subclass of Emitter (9) =

class TestTanglerMake(unittest.TestCase):
    def setUp(self) -> None:
        self.filepath = Path.cwd()
        self.tangler = pyweb.TanglerMake()
        self.web = mock_web()
        self.output = self.filepath / "sample.out"
        self.tangler.emit(self.web)
        self.time_original = self.output.stat().st_mtime
        self.original = self.output.stat()

    def tearDown(self) -> None:
        try:
            self.output.unlink()
        except OSError:
            pass

    def test_confirm_tanged_output(self) -> None:
        tangled = self.output.read_text()
        expected = (
            'Mocked Tangle Output\n'
        )
        self.assertEqual(expected, tangled)


    def test_same_should_leave(self) -> None:
        self.tangler.emit(self.web)
        self.assertTrue(os.path.samestat(self.original, self.output.stat()))
        #self.assertEqual(self.time_original, self.output.stat().st_mtime)

    def test_different_should_update(self) -> None:
        # Modify the web in some way to create a distinct value.
        def tangle_method(aTangler: pyweb.Tangler, target: TextIO) -> None:
            aTangler.codeBlock(target, "Updated Tangle Output\n")
        self.web.chunks[1].commands[0].tangle = Mock(side_effect=tangle_method)
        self.tangler.emit(self.web)
        print(self.output.read_text())
        self.assertFalse(os.path.samestat(self.original, self.output.stat()))
        #self.assertNotEqual(self.time_original, self.output.stat().st_mtime)
Unit Test of TanglerMake subclass of Emitter (9). Used by → Unit Test of Emitter class hierarchy (2).

Chunk Tests

The Chunk and Command class hierarchies model the input document -- the web of chunks that are used to produce the documentation and the source files.

Unit Test of Chunk class hierarchy (10) =

Unit Test of Chunk superclass (11)Unit Test of NamedChunk subclass (18)Unit Test of NamedChunk_Noindent subclass (19)Unit Test of OutputChunk subclass (20)Unit Test of NamedDocumentChunk subclass (21)
Unit Test of Chunk class hierarchy (10). Used by → test_unit.py (1).

In order to test the Chunk superclass, we need several mock objects. A Chunk contains one or more commands. A Chunk is a part of a Web. Also, a Chunk is processed by a Tangler or a Weaver. We'll need mock objects for all of these relationships in which a Chunk participates.

A MockCommand can be attached to a Chunk.

Unit Test of Chunk superclass (11) =

MockCommand = Mock(
    name="Command class",
    side_effect=lambda: Mock(
        name="Command instance",
        # text="",  # Only used for TextCommand.
        lineNumber=314,
        startswith=Mock(return_value=False)
    )
)
Unit Test of Chunk superclass (11). Used by → Unit Test of Chunk class hierarchy (10).

A MockWeb can contain a Chunk.

Unit Test of Chunk superclass (12) +=

def mock_web_instance() -> Mock:
    web = Mock(
        name="Web instance",
        chunks=[],
        # add=Mock(return_value=None),
        # addNamed=Mock(return_value=None),
        # addOutput=Mock(return_value=None),
        fullNameFor=Mock(side_effect=lambda name: name),
        fileXref=Mock(return_value={'file': [1,2,3]}),
        chunkXref=Mock(return_value={'chunk': [4,5,6]}),
        userNamesXref=Mock(return_value={'name': (7, [8,9,10])}),
        # getchunk=Mock(side_effect=lambda name: [MockChunk(name, 1, ("sample.w", 314))]),
        createUsedBy=Mock(),
        weaveChunk=Mock(side_effect=lambda name, weaver: weaver.write(name)),
        weave=Mock(return_value=None),
        tangle=Mock(return_value=None),
    )
    return web

MockWeb = Mock(
    name="Web class",
    side_effect=mock_web_instance,
    file_path="sample.input",
)
Unit Test of Chunk superclass (12). Used by → Unit Test of Chunk class hierarchy (10).

A MockWeaver or MockTangler appear to process a Chunk. We can interrogate the mock_calls to be sure the right things were done.

We need to permit __enter__() and __exit__(), which leads to a multi-step instance. The initial instance with __enter__() that returns the context manager instance.

Unit Test of Chunk superclass (13) +=

def mock_weaver_instance() -> MagicMock:
    context = MagicMock(
        name="Weaver instance context",
        __exit__=Mock()
    )

    weaver = MagicMock(
        name="Weaver instance",
        quote=Mock(return_value="quoted"),
        __enter__=Mock(return_value=context)
    )
    return weaver

MockWeaver = Mock(
    name="Weaver class",
    side_effect=mock_weaver_instance
)

def mock_tangler_instance() -> MagicMock:
    context = MagicMock(
        name="Tangler instance context",
        reference_names=Mock(add=Mock()),
        __exit__=Mock()
    )

    tangler = MagicMock(
        name="Tangler instance",
        __enter__=Mock(return_value=context),
    )
    return tangler

MockTangler = Mock(
    name="Tangler class",
    side_effect=mock_tangler_instance
)
Unit Test of Chunk superclass (13). Used by → Unit Test of Chunk class hierarchy (10).

A Chunk is built, interrogated and then emitted.

Unit Test of Chunk superclass (14) +=

class TestChunk(unittest.TestCase):
    def setUp(self) -> None:
        self.theChunk = pyweb.Chunk()


→ Unit Test of Chunk construction (15)Unit Test of Chunk interrogation (16)Unit Test of Chunk properties (17)
Unit Test of Chunk superclass (14). Used by → Unit Test of Chunk class hierarchy (10).

Can we build a Chunk?

Unit Test of Chunk construction (15) =

def test_append_command_should_work(self) -> None:
    cmd1 = MockCommand()
    self.theChunk.commands.append(cmd1)
    self.assertEqual(1, len(self.theChunk.commands))
    self.assertEqual([cmd1], self.theChunk.commands)

    cmd2 = MockCommand()
    self.theChunk.commands.append(cmd2)
    self.assertEqual(2, len(self.theChunk.commands))
    self.assertEqual([cmd1, cmd2], self.theChunk.commands)
Unit Test of Chunk construction (15). Used by → Unit Test of Chunk superclass (14).

Can we interrogate a Chunk?

Unit Test of Chunk interrogation (16) =

def test_lineNumber_should_work(self) -> None:
    cmd1 = MockCommand()
    self.theChunk.commands.append(cmd1)
    self.assertEqual(314, self.theChunk.commands[0].lineNumber)
Unit Test of Chunk interrogation (16). Used by → Unit Test of Chunk superclass (14).

Can we emit a Chunk with a weaver or tangler?

Unit Test of Chunk properties (17) =

def test_properties(self) -> None:
    self.theChunk.name = "some name"
    web = MockWeb()
    self.theChunk.web = Mock(return_value=web)
    self.theChunk.full_name
    web.resolve_name.assert_called_once_with(self.theChunk.name)
    self.assertIsNone(self.theChunk.path)
    self.assertTrue(self.theChunk.type_is('Chunk'))
    self.assertFalse(self.theChunk.type_is('OutputChunk'))
    self.assertIsNone(self.theChunk.referencedBy)
Unit Test of Chunk properties (17). Used by → Unit Test of Chunk superclass (14).

The NamedChunk is created by a @d command. Since it's named, it appears in the Web's index. Also, it is woven and tangled differently than anonymous chunks.

Unit Test of NamedChunk subclass (18) =

class TestNamedChunk(unittest.TestCase):
    def setUp(self) -> None:
        self.theChunk = pyweb.NamedChunk(name="Some Name...")
        cmd = MockCommand()
        self.theChunk.commands.append(cmd)
        self.theChunk.def_names = ["index", "terms"]

    def test_should_find_xref_words(self) -> None:
        self.assertEqual(2, len(self.theChunk.def_names))
        self.assertEqual({"index", "terms"}, set(self.theChunk.def_names))

    def test_properties(self) -> None:
        web = MockWeb()
        self.theChunk.web = Mock(return_value=web)
        self.theChunk.full_name
        web.resolve_name.assert_called_once_with(self.theChunk.name)
        self.assertIsNone(self.theChunk.path)
        self.assertTrue(self.theChunk.type_is("NamedChunk"))
        self.assertFalse(self.theChunk.type_is("OutputChunk"))
        self.assertFalse(self.theChunk.type_is("Chunk"))
        self.assertIsNone(self.theChunk.referencedBy)
Unit Test of NamedChunk subclass (18). Used by → Unit Test of Chunk class hierarchy (10).

Unit Test of NamedChunk_Noindent subclass (19) =

class TestNamedChunk_Noindent(unittest.TestCase):
    def setUp(self) -> None:
        self.theChunk = pyweb.NamedChunk("NoIndent Name...", options="-noindent")
        cmd = MockCommand()
        self.theChunk.commands.append(cmd)
        self.theChunk.def_names = ["index", "terms"]

    def test_should_find_xref_words(self) -> None:
        self.assertEqual(2, len(self.theChunk.def_names))
        self.assertEqual({"index", "terms"}, set(self.theChunk.def_names))

    def test_properties(self) -> None:
        web = MockWeb()
        self.theChunk.web = Mock(return_value=web)
        self.theChunk.full_name
        web.resolve_name.assert_called_once_with(self.theChunk.name)
        self.assertIsNone(self.theChunk.path)
        self.assertTrue(self.theChunk.type_is("NamedChunk"))
        self.assertFalse(self.theChunk.type_is("Chunk"))
        self.assertIsNone(self.theChunk.referencedBy)
Unit Test of NamedChunk_Noindent subclass (19). Used by → Unit Test of Chunk class hierarchy (10).

The OutputChunk is created by a @o command. Since it's named, it appears in the Web's index. Also, it is woven and tangled differently than anonymous chunks of text. This defines the files of tangled code.

Unit Test of OutputChunk subclass (20) =

class TestOutputChunk(unittest.TestCase):
    def setUp(self) -> None:
        self.theChunk = pyweb.OutputChunk("filename.out")
        self.theChunk.comment_start = "# "
        self.theChunk.comment_end = ""
        cmd = MockCommand()
        self.theChunk.commands.append(cmd)
        self.theChunk.def_names = ["index", "terms"]

    def test_should_find_xref_words(self) -> None:
        self.assertEqual(2, len(self.theChunk.def_names))
        self.assertEqual({"index", "terms"}, set(self.theChunk.def_names))

    def test_properties(self) -> None:
        web = MockWeb()
        self.theChunk.web = Mock(return_value=web)
        self.assertIsNone(self.theChunk.full_name)
        web.resolve_name.assert_not_called()
        self.assertEqual(self.theChunk.path, Path("filename.out"))
        self.assertTrue(self.theChunk.type_is("OutputChunk"))
        self.assertFalse(self.theChunk.type_is("Chunk"))
        self.assertIsNone(self.theChunk.referencedBy)
Unit Test of OutputChunk subclass (20). Used by → Unit Test of Chunk class hierarchy (10).

The NamedDocumentChunk is a way to define substitutable text, similar to tabled code, but it applies to document chunks. It's not clear how useful this really is.

Unit Test of NamedDocumentChunk subclass (21) =

class TestNamedDocumentChunk(unittest.TestCase):
    def setUp(self) -> None:
        self.theChunk = pyweb.NamedDocumentChunk("Document Chunk Name...")
        cmd = MockCommand()
        self.theChunk.commands.append(cmd)
        self.theChunk.def_names = ["index", "terms"]

    def test_should_find_xref_words(self) -> None:
        self.assertEqual(2, len(self.theChunk.def_names))
        self.assertEqual({"index", "terms"}, set(self.theChunk.def_names))

    def test_properties(self) -> None:
        web = MockWeb()
        self.theChunk.web = Mock(return_value=web)
        self.theChunk.full_name
        web.resolve_name.assert_called_once_with(self.theChunk.name)
        self.assertIsNone(self.theChunk.path)
        self.assertTrue(self.theChunk.type_is("NamedDocumentChunk"))
        self.assertFalse(self.theChunk.type_is("OutputChunk"))
        self.assertIsNone(self.theChunk.referencedBy)
Unit Test of NamedDocumentChunk subclass (21). Used by → Unit Test of Chunk class hierarchy (10).

Chunk References Tests

A Chunk's "referencedBy" attribute is set by the Web during the initialization processing.

The test fixture is this

@d main @{ @< parent @> @}

@d parent @{ @< sub @> @}

@d sub @{ something @}

The sub item is referenced by parent which is referenced by main.

The simple reference is sub referenced by parent.

The transitive references are sub referenced by parent which is referenced by main.

Unit Test of Chunk References (22) =

class TestReferences(unittest.TestCase):
    def setUp(self) -> None:
        self.web = MockWeb()
        self.main = pyweb.NamedChunk("Main", 1)
        self.main.referencedBy = None
        self.main.web = Mock(return_value=self.web)
        self.parent = pyweb.NamedChunk("Parent", 2)
        self.parent.referencedBy = self.main
        self.parent.web = Mock(return_value=self.web)
        self.chunk = pyweb.NamedChunk("Sub", 3)
        self.chunk.referencedBy = self.parent
        self.chunk.web = Mock(return_value=self.web)

    def test_simple(self) -> None:
        self.assertEqual(self.chunk.referencedBy, self.parent)

    def test_transitive_sub_sub(self) -> None:
        theList = self.chunk.transitive_referencedBy
        self.assertEqual(2, len(theList))
        self.assertEqual(self.parent, theList[0])
        self.assertEqual(self.main, theList[1])

    def test_transitive_sub(self) -> None:
        theList = self.parent.transitive_referencedBy
        self.assertEqual(1, len(theList))
        self.assertEqual(self.main, theList[0])

    def test_transitive_top(self) -> None:
        theList = self.main.transitive_referencedBy
        self.assertEqual(0, len(theList))
Unit Test of Chunk References (22). Used by → test_unit.py (1).

Command Tests

Unit Test of Command class hierarchy (23) =

Unit Test of Command superclass (24)Unit Test of TextCommand class to contain a document text block (25)Unit Test of CodeCommand class to contain a program source code block (26)Unit Test of XrefCommand superclass for all cross-reference commands (27)Unit Test of FileXrefCommand class for an output file cross-reference (28)Unit Test of MacroXrefCommand class for a named chunk cross-reference (29)Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30)Unit Test of ReferenceCommand class for chunk references (31)
Unit Test of Command class hierarchy (23). Used by → test_unit.py (1).

This Command superclass is essentially an inteface definition, it has no real testable features.

Unit Test of Command superclass (24) =

# No Tests
Unit Test of Command superclass (24). Used by → Unit Test of Command class hierarchy (23).

A TextCommand object must be built from source text, interrogated, and emitted. A TextCommand should not (generally) be created in a Chunk, it should only be part of a NamedChunk or OutputChunk.

Unit Test of TextCommand class to contain a document text block (25) =

class TestTextCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.cmd = pyweb.TextCommand("Some text & words in the document\n    ", ("sample.w", 314))
        self.cmd2 = pyweb.TextCommand("No Indent\n", ("sample.w", 271))

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.TextCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)

    def test_tangle_should_error(self) -> None:
        tnglr = MockTangler()
        with self.assertRaises(pyweb.Error) as exc_info:
            self.cmd.tangle(tnglr, sentinel.TARGET)
        assert exc_info.exception.args == (
            "attempt to tangle a text block ('sample.w', 314) 'Some text & words in the [...]'",
        )
Unit Test of TextCommand class to contain a document text block (25). Used by → Unit Test of Command class hierarchy (23).

A CodeCommand object is a TextCommand with different processing for being emitted. It represents a block of code in a NamedChunk or OutputChunk.

Unit Test of CodeCommand class to contain a program source code block (26) =

class TestCodeCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.cmd = pyweb.CodeCommand("Some code in the document\n    ", ("sample.w", 314))

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.CodeCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)

    def test_tangle_should_work(self) -> None:
        tnglr = MockTangler()
        self.cmd.tangle(tnglr, sentinel.TARGET)
        tnglr.codeBlock.assert_called_once_with(sentinel.TARGET, 'Some code in the document\n    ')
Unit Test of CodeCommand class to contain a program source code block (26). Used by → Unit Test of Command class hierarchy (23).

An XrefCommand class (if defined) would be abstract. We could formalize this, but it seems easier to have a collection of @dataclass definitions a Union[...] type hint.

Unit Test of XrefCommand superclass for all cross-reference commands (27) =

# No Tests
Unit Test of XrefCommand superclass for all cross-reference commands (27). Used by → Unit Test of Command class hierarchy (23).

The FileXrefCommand command is expanded by a weaver to a list of @o locations.

Unit Test of FileXrefCommand class for an output file cross-reference (28) =

class TestFileXRefCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.cmd = pyweb.FileXrefCommand(("sample.w", 314))
        self.web = Mock(files=sentinel.FILES)
        self.cmd.web = Mock(return_value=self.web)

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.FileXrefCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)
        self.assertEqual(sentinel.FILES, self.cmd.files)

    def test_tangle_should_fail(self) -> None:
        tnglr = MockTangler()
        try:
            self.cmd.tangle(tnglr, sentinel.TARGET)
            self.fail()
        except pyweb.Error:
            pass
Unit Test of FileXrefCommand class for an output file cross-reference (28). Used by → Unit Test of Command class hierarchy (23).

The MacroXrefCommand command is expanded by a weaver to a list of all @d locations.

Unit Test of MacroXrefCommand class for a named chunk cross-reference (29) =

class TestMacroXRefCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.cmd = pyweb.MacroXrefCommand(("sample.w", 314))
        self.web = Mock(macros=sentinel.MACROS)
        self.cmd.web = Mock(return_value=self.web)

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.MacroXrefCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)
        self.assertEqual(sentinel.MACROS, self.cmd.macros)

    def test_tangle_should_fail(self) -> None:
        tnglr = MockTangler()
        try:
            self.cmd.tangle(tnglr, sentinel.TARGET)
            self.fail()
        except pyweb.Error:
            pass
Unit Test of MacroXrefCommand class for a named chunk cross-reference (29). Used by → Unit Test of Command class hierarchy (23).

The UserIdXrefCommand command is expanded by a weaver to a list of all @| names.

Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30) =

class TestUserIdXrefCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.cmd = pyweb.UserIdXrefCommand(("sample.w", 314))
        self.web = Mock(userids=sentinel.USERIDS)
        self.cmd.web = Mock(return_value=self.web)

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.UserIdXrefCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)
        self.assertEqual(sentinel.USERIDS, self.cmd.userids)

    def test_tangle_should_fail(self) -> None:
        tnglr = MockTangler()
        try:
            self.cmd.tangle(tnglr, sentinel.TARGET)
            self.fail()
        except pyweb.Error:
            pass
Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30). Used by → Unit Test of Command class hierarchy (23).

Instances of the Reference command reflect @< name @> locations in code. These require a context when tangling. The context helps provide the required indentation. They can't be simply tangled, since the expand to code that may (transitively) have more references to more code.

The document here is a mock-up of the following

@d name @{ @<Some Name@> @}

@d Some Name @{ code @}

This is a single Chunk with a reference to another Chunk.

The Web class __post_init__ sets the references and referencedBy attributes of each Chunk.

Unit Test of ReferenceCommand class for chunk references (31) =

class TestReferenceCommand(unittest.TestCase):
    def setUp(self) -> None:
        self.chunk = MockChunk("name", 123, ("sample.w", 456))
        self.cmd = pyweb.ReferenceCommand("Some Name", ("sample.w", 314))
        self.chunk.commands = [self.cmd]
        self.referenced_chunk = Mock(seq=sentinel.SEQUENCE, references=1, referencedBy=self.chunk, commands=[Mock()])
        self.web = Mock(
            resolve_name=Mock(return_value=sentinel.FULL_NAME),
            resolve_chunk=Mock(return_value=[self.referenced_chunk])
        )
        self.cmd.web = Mock(return_value=self.web)

    def test_methods_should_work(self) -> None:
        self.assertTrue(self.cmd.typeid.ReferenceCommand)
        self.assertEqual(("sample.w", 314), self.cmd.location)
        self.assertEqual(sentinel.FULL_NAME, self.cmd.full_name)
        self.assertEqual(sentinel.SEQUENCE, self.cmd.seq)

    def test_tangle_should_work(self) -> None:
        tnglr = MockTangler()
        self.cmd.tangle(tnglr, sentinel.TARGET)
        self.web.resolve_chunk.assert_called_once_with("Some Name")
        tnglr.reference_names.add.assert_called_once_with('Some Name')
        self.assertEqual(1, self.referenced_chunk.references)
        self.referenced_chunk.commands[0].tangle.assert_called_once_with(tnglr, sentinel.TARGET)
Unit Test of ReferenceCommand class for chunk references (31). Used by → Unit Test of Command class hierarchy (23).

Web Tests

We create a Web instance with mocked Chunks and mocked Commands. The point is to test the Web features in isolation. This is tricky because some state is recorded in the Chunk instances.

Unit Test of Web class (32) =

class TestWebConstruction(unittest.TestCase):
    def setUp(self) -> None:
        self.c1 = MockChunk("c1", 1, ("sample.w", 11))
        self.c1.type_is = Mock(side_effect = lambda n: n == "Chunk")
        self.c1.referencedBy = None
        self.c1.name = None
        self.c2 = MockChunk("c2", 2, ("sample.w", 22))
        self.c2.type_is = Mock(side_effect = lambda n: n == "OutputChunk")
        self.c2.commands = [Mock()]
        self.c2.commands[0].name = "c3..."
        self.c2.commands[0].typeid = Mock(ReferenceCommand=True, TextCommand=False, CodeCommand=False)
        self.c2.referencedBy = None
        self.c3 = MockChunk("c3 has a long name", 3, ("sample.w", 33))
        self.c3.type_is = Mock(side_effect = lambda n: n == "NamedChunk")
        self.c3.referencedBy = None
        self.c3.def_names = ["userid"]
        self.web = pyweb.Web([self.c1, self.c2, self.c3])

    def test_name_resolution(self) -> None:
        self.assertEqual(self.web.resolve_name("c1"), "c1")
        self.assertEqual(self.web.resolve_chunk("c2"), [self.c2])
        self.assertEqual(self.web.resolve_name("c1..."), "c1")
        self.assertEqual(self.web.resolve_name("c3..."), "c3 has a long name")

    def test_chunks_should_iterate(self) -> None:
        self.assertEqual([self.c2], list(self.web.file_iter()))
        self.assertEqual([self.c3], list(self.web.macro_iter()))
        self.assertEqual([SimpleNamespace(def_name="userid", chunk=self.c3)], list(self.web.userid_iter()))
        self.assertEqual([self.c2], self.web.files)
        self.assertEqual(
            [
                SimpleNamespace(name="c2", full_name="c2", seq=1, def_list=[self.c2]),
                SimpleNamespace(name="c3 has a long name", full_name="c3 has a long name", seq=2, def_list=[self.c3])
            ],
            self.web.macros)
        self.assertEqual([SimpleNamespace(userid='userid', ref_list=[self.c3])], self.web.userids)
        self.assertEqual([self.c2], self.web.no_reference())
        self.assertEqual([], self.web.multi_reference())

    def test_valid_web_should_tangle(self) -> None:
        """This is the entire interface used by tangling.
        The details are pushed down to ```command.tangle()`` for each command in each chunk.
        """
        self.assertEqual([self.c2], self.web.files)

    def test_valid_web_should_weave(self) -> None:
        """This is the entire interface used by tangling.
        The details are pushed down to unique processing based on ``chunk.type_is``.
        """
        self.assertEqual([self.c1, self.c2, self.c3], self.web.chunks)
Unit Test of Web class (32). Used by → test_unit.py (1).

WebReader Tests

Generally, this is tested separately through the functional tests. Those tests each present source files to be processed by the WebReader.

The WebReader is poorly designed for unit testing. The various chunk and command classes are part of the WebReader, and new classes cannot be injected gracefully.

Exacerbating this are two special cases: the @@ and @(expr@) constructs are evaluated immediately, and don't create commands.

Unit Test of WebReader class (33) =

# Tested via functional tests
Unit Test of WebReader class (33). Used by → test_unit.py (1).

Some lower-level units: specifically the tokenizer and the option parser.

Unit Test of WebReader class (34) +=

class TestTokenizer(unittest.TestCase):
    def test_should_split_tokens(self) -> None:
        input = io.StringIO("@@ word @{ @[ @< @>\n@] @} @i @| @m @f @u @( @)\n")
        self.tokenizer = pyweb.Tokenizer(input)
        tokens = list(self.tokenizer)
        self.assertEqual(28, len(tokens))
        self.assertEqual( ['@@', ' word ', '@{', ' ', '@[', ' ', '@<', ' ',
        '@>', '\n', '@]', ' ', '@}', ' ', '@i', ' ', '@|', ' ', '@m', ' ',
        '@f', ' ', '@u', ' ', '@(', ' ', '@)', '\n'], tokens )
        self.assertEqual(2, self.tokenizer.lineNumber)
Unit Test of WebReader class (34). Used by → test_unit.py (1).

Unit Test of WebReader class (35) +=

class TestOptionParser_OutputChunk(unittest.TestCase):
    def setUp(self) -> None:
        rdr = pyweb.WebReader()
        self.option_parser = rdr.output_option_parser
    def test_with_options_should_parse(self) -> None:
        text1 = " -start /* -end */ something.css "
        options1 = self.option_parser.parse_args(shlex.split(text1))
        self.assertEqual(argparse.Namespace(start='/*', end='*/', argument=['something.css']), options1)
    def test_without_options_should_parse(self) -> None:
        text2 = " something.py "
        options2 = self.option_parser.parse_args(shlex.split(text2))
        self.assertEqual(argparse. Namespace(start=None, end='', argument=['something.py']), options2)

class TestOptionParser_NamedChunk(unittest.TestCase):
    def setUp(self) -> None:
        rdr = pyweb.WebReader()
        self.option_parser = rdr.definition_option_parser
    def test_with_options_should_parse(self) -> None:
        text1 = " -indent the name of test1 chunk... "
        options1 = self.option_parser.parse_args(shlex.split(text1))
        self.assertEqual(argparse.Namespace(argument=['the', 'name', 'of', 'test1', 'chunk...'], indent=True, noindent=False), options1)
    def test_without_options_should_parse(self) -> None:
        text2 = " the name of test2 chunk... "
        options2 = self.option_parser.parse_args(shlex.split(text2))
        self.assertEqual(argparse.Namespace(argument=['the', 'name', 'of', 'test2', 'chunk...'], indent=False, noindent=False), options2)
Unit Test of WebReader class (35). Used by → test_unit.py (1).

Testing the @@ case and one of the @(expr@) cases. Need to test all the available variables: os.path, os.getcwd, os.name, time, datetime, platform, theWebReader, theFile, thisApplication, version, theLocation.

Unit Test of WebReader class (36) +=

class TestWebReader_Immediate(unittest.TestCase):
    def setUp(self) -> None:
        self.reader = pyweb.WebReader()

    def test_should_build_escape_chunk(self):
        chunks = self.reader.load(Path(), io.StringIO("Escape: @@ Example"))
        self.assertEqual(1, len(chunks))
        self.assertEqual(1, len(chunks[0].commands))
        self.assertEqual("Escape: @ Example", chunks[0].commands[0].text)

    def test_expressions(self):
        chunks = self.reader.load(Path("sample.w"), io.StringIO("Filename: @(theFile@)"))
        self.assertEqual(1, len(chunks))
        self.assertEqual(1, len(chunks[0].commands))
        self.assertEqual("Filename: sample.w", chunks[0].commands[0].text)
Unit Test of WebReader class (36). Used by → test_unit.py (1).

Action Tests

Each class is tested separately. Sequence of some mocks, load, tangle, weave.

Unit Test of Action class hierarchy (37) =

Unit test of Action Sequence class (38)Unit test of LoadAction class (41)Unit test of TangleAction class (40)Unit test of WeaverAction class (39)
Unit Test of Action class hierarchy (37). Used by → test_unit.py (1).

TODO: Replace with Mock

Unit test of Action Sequence class (38) =

class TestActionSequence(unittest.TestCase):
    def setUp(self) -> None:
        self.web = MockWeb()
        self.a1 = MagicMock(name="Action1")
        self.a2 = MagicMock(name="Action2")
        self.action = pyweb.ActionSequence("TwoSteps", [self.a1, self.a2])
        self.action.web = self.web
        self.options = argparse.Namespace()
    def test_should_execute_both(self) -> None:
        self.action(self.options)
        self.assertEqual(self.a1.call_count, 1)
        self.assertEqual(self.a2.call_count, 1)
Unit test of Action Sequence class (38). Used by → Unit Test of Action class hierarchy (37).

Unit test of WeaverAction class (39) =

class TestWeaveAction(unittest.TestCase):
    def setUp(self) -> None:
        self.web = MockWeb()
        self.action = pyweb.WeaveAction()
        self.weaver = MockWeaver()
        self.options = argparse.Namespace(
            theWeaver=self.weaver,
            # reference_style=pyweb.SimpleReference(),  # Remove this
            output=Path.cwd(),
            web=self.web,
            weaver='rst',
        )
    def test_should_execute_weaving(self) -> None:
        self.action(self.options)
        self.assertEqual(self.weaver.emit.mock_calls, [call(self.web)])
Unit test of WeaverAction class (39). Used by → Unit Test of Action class hierarchy (37).

Unit test of TangleAction class (40) =

class TestTangleAction(unittest.TestCase):
    def setUp(self) -> None:
        self.web = MockWeb()
        self.action = pyweb.TangleAction()
        self.tangler = MockTangler()
        self.options = argparse.Namespace(
            theTangler = self.tangler,
            tangler_line_numbers = False,
            output=Path.cwd(),
            web = self.web
        )
    def test_should_execute_tangling(self) -> None:
        self.action(self.options)
        self.assertEqual(self.tangler.emit.mock_calls, [call(self.web)])
Unit test of TangleAction class (40). Used by → Unit Test of Action class hierarchy (37).

The mocked WebReader must provide an errors property to the LoadAction instance.

Unit test of LoadAction class (41) =

class TestLoadAction(unittest.TestCase):
    def setUp(self) -> None:
        self.web = MockWeb()
        self.action = pyweb.LoadAction()
        self.webReader = Mock(
            name="WebReader",
            errors=0,
            load=Mock(return_value=[])
        )
        self.source_path = Path("TestLoadAction.w")
        self.options = argparse.Namespace(
            webReader = self.webReader,
            source_path=self.source_path,
            command="@",
            permitList = [],
            output=Path.cwd(),
        )
        Path("TestLoadAction.w").write_text("")
    def tearDown(self) -> None:
        try:
            Path("TestLoadAction.w").unlink()
        except IOError:
            pass
    def test_should_execute_loading(self) -> None:
        self.action(self.options)
        print(self.webReader.load.mock_calls)
        self.assertEqual(self.webReader.load.mock_calls, [call(self.source_path)])
        self.webReader.web.assert_not_called()  # Deprecated
        self.webReader.source.assert_not_called()  # Deprecated
Unit test of LoadAction class (41). Used by → Unit Test of Action class hierarchy (37).

Application Tests

As with testing WebReader, this requires extensive mocking. It's easier to simply run the various use cases.

TODO: Test Application class

Unit Test of Application class (42) =

# TODO Test Application class
Unit Test of Application class (42). Used by → test_unit.py (1).

Overheads and Main Script

The boilerplate code for unit testing is the following.

Unit Test overheads: imports, etc. (43) =

"""Unit tests."""
import argparse
import io
import logging
import os
from pathlib import Path
import re
import shlex
import string
import sys
import textwrap
import time
from types import SimpleNamespace
from typing import Any, TextIO
import unittest
from unittest.mock import Mock, call, MagicMock, sentinel
import warnings

import pyweb
Unit Test overheads: imports, etc. (43). Used by → test_unit.py (1).

One more overhead is a function we can inject into selected subclasses of unittest.TestCase. This is monkeypatch feature that seems useful.

Unit Test overheads: imports, etc. (44) +=

def rstrip_lines(source: str) -> list[str]:
    return list(l.rstrip() for l in source.splitlines())
Unit Test overheads: imports, etc. (44). Used by → test_unit.py (1).

Unit Test main (45) =

if __name__ == "__main__":
    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    unittest.main()
Unit Test main (45). Used by → test_unit.py (1).

We run the default unittest.main() to execute the entire suite of tests.

Functional Testing

There are three broad areas of functional testing.

There are a total of 11 test cases.

Tests for Loading

We need to be able to load a web from one or more source files.

test_loader.py (46) =

Load Test overheads: imports, etc. (48)Load Test superclass to refactor common setup (47)Load Test error handling with a few common syntax errors (49)Load Test include processing with syntax errors (51)Load Test main program (54)
test_loader.py (46).

Parsing test cases have a common setup shown in this superclass.

By using some class-level variables text, file_path, we can simply provide a file-like input object to the WebReader instance.

Load Test superclass to refactor common setup (47) =

class ParseTestcase(unittest.TestCase):
    text: ClassVar[str]
    file_path: ClassVar[Path]

    def setUp(self) -> None:
        self.source = io.StringIO(self.text)
        self.rdr = pyweb.WebReader()
Load Test superclass to refactor common setup (47). Used by → test_loader.py (46).

There are a lot of specific parsing exceptions which can be thrown. We'll cover most of the cases with a quick check for a failure to find an expected next token.

Load Test overheads: imports, etc. (48) =

import logging.handlers
from pathlib import Path
from textwrap import dedent
from typing import ClassVar
Load Test overheads: imports, etc. (48). Used by → test_loader.py (46).

Load Test error handling with a few common syntax errors (49) =

Sample Document 1 with correct and incorrect syntax (50)

class Test_ParseErrors(ParseTestcase):
    text = test1_w
    file_path = Path("test1.w")
    def test_error_should_count_1(self) -> None:
        with self.assertLogs('WebReader', level='WARN') as log_capture:
            chunks = self.rdr.load(self.file_path, self.source)
        self.assertEqual(3, self.rdr.errors)
        self.assertEqual(log_capture.output,
            [
                "ERROR:WebReader:At ('test1.w', 8): expected {'@{'}, found '@o'",
                "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)",
                "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"
            ]
        )
Load Test error handling with a few common syntax errors (49). Used by → test_loader.py (46).

Sample Document 1 with correct and incorrect syntax (50) =

test1_w = """Some anonymous chunk
@o test1.tmp
@{@<part1@>
@<part2@>
@}@@
@d part1 @{This is part 1.@}
Okay, now for an error.
@o show how @o commands work
@{ @{ @] @]
"""
Sample Document 1 with correct and incorrect syntax (50). Used by → Load Test error handling with a few common syntax errors (49).

All of the parsing exceptions should be correctly identified with any included file. We'll cover most of the cases with a quick check for a failure to find an expected next token.

In order to test the include file processing, we have to actually create a temporary file. It's hard to mock the include processing, since it's a nested instance of the tokenizer.

Load Test include processing with syntax errors (51) =

Sample Document 8 and the file it includes (52)

class Test_IncludeParseErrors(ParseTestcase):
    text = test8_w
    file_path = Path("test8.w")
    def setUp(self) -> None:
        super().setUp()
        Path('test8_inc.tmp').write_text(test8_inc_w)
    def test_error_should_count_2(self) -> None:
        with self.assertLogs('WebReader', level='WARN') as log_capture:
            chunks = self.rdr.load(self.file_path, self.source)
        self.assertEqual(1, self.rdr.errors)
        self.assertEqual(log_capture.output,
            [
                "ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, {'@{', '@['} not found",
                "ERROR:WebReader:Errors in included file 'test8_inc.tmp', output is incomplete."
            ]
        )
    def tearDown(self) -> None:
        super().tearDown()
        Path('test8_inc.tmp').unlink()
Load Test include processing with syntax errors (51). Used by → test_loader.py (46).

The sample document must reference the correct name that will be given to the included document by setUp.

Sample Document 8 and the file it includes (52) =

test8_w = """Some anonymous chunk.
@d title @[the title of this document, defined with @@[ and @@]@]
A reference to @<title@>.
@i test8_inc.tmp
A final anonymous chunk from test8.w
"""

test8_inc_w="""A chunk from test8a.w
And now for an error - incorrect syntax in an included file!
@d yap
"""
Sample Document 8 and the file it includes (52). Used by → Load Test include processing with syntax errors (51).

<p>The overheads for a Python unittest.</p>

Load Test overheads: imports, etc. (53) +=

"""Loader and parsing tests."""
import io
import logging
import os
from pathlib import Path
import string
import sys
import types
import unittest

import pyweb
Load Test overheads: imports, etc. (53). Used by → test_loader.py (46).

A main program that configures logging and then runs the test.

Load Test main program (54) =

if __name__ == "__main__":
    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    unittest.main()
Load Test main program (54). Used by → test_loader.py (46).

Tests for Tangling

We need to be able to tangle a web.

test_tangler.py (55) =

Tangle Test overheads: imports, etc. (69)Tangle Test superclass to refactor common setup (56)Tangle Test semantic error 2 (57)Tangle Test semantic error 3 (59)Tangle Test semantic error 4 (61)Tangle Test semantic error 5 (63)Tangle Test semantic error 6 (65)Tangle Test include error 7 (67)Tangle Test main program (70)
test_tangler.py (55).

Tangling test cases have a common setup and teardown shown in this superclass. Since tangling must produce a file, it's helpful to remove the file that gets created. The essential test case is to load and attempt to tangle, checking the exceptions raised.

Tangle Test superclass to refactor common setup (56) =

class TangleTestcase(unittest.TestCase):
    text: ClassVar[str]
    error: ClassVar[str]
    file_path: ClassVar[Path]

    def setUp(self) -> None:
        self.source = io.StringIO(self.text)
        self.rdr = pyweb.WebReader()
        self.tangler = pyweb.Tangler()

    def tangle_and_check_exception(self, exception_text: str) -> None:
        with self.assertRaises(pyweb.Error) as exc_mgr:
            chunks = self.rdr.load(self.file_path, self.source)
            self.web = pyweb.Web(chunks)
            self.tangler.emit(self.web)
            self.fail("Should not tangle")
        exc = exc_mgr.exception
        self.assertEqual(exception_text, exc.args[0])

    def tearDown(self) -> None:
        try:
            self.file_path.with_suffix(".tmp").unlink()
        except FileNotFoundError:
            pass  # If the test fails, nothing to remove...
Tangle Test superclass to refactor common setup (56). Used by → test_tangler.py (55).

Tangle Test semantic error 2 (57) =

Sample Document 2 (58)

class Test_SemanticError_2(TangleTestcase):
    text = test2_w
    file_path = Path("test2.w")
    def test_should_raise_undefined(self) -> None:
        self.tangle_and_check_exception("Attempt to tangle an undefined Chunk, 'part2'")
Tangle Test semantic error 2 (57). Used by → test_tangler.py (55).

Sample Document 2 (58) =

test2_w = """Some anonymous chunk
@o test2.tmp
@{@<part1@>
@<part2@>
@}@@
@d part1 @{This is part 1.@}
Okay, now for some errors: no part2!
"""
Sample Document 2 (58). Used by → Tangle Test semantic error 2 (57).

Tangle Test semantic error 3 (59) =

Sample Document 3 (60)

class Test_SemanticError_3(TangleTestcase):
    text = test3_w
    file_path = Path("test3.w")
    def test_should_raise_bad_xref(self) -> None:
        self.tangle_and_check_exception("Illegal tangling of a cross reference command.")
Tangle Test semantic error 3 (59). Used by → test_tangler.py (55).

Sample Document 3 (60) =

test3_w = """Some anonymous chunk
@o test3.tmp
@{@<part1@>
@<part2@>
@}@@
@d part1 @{This is part 1.@}
@d part2 @{This is part 2, with an illegal: @f.@}
Okay, now for some errors: attempt to tangle a cross-reference!
"""
Sample Document 3 (60). Used by → Tangle Test semantic error 3 (59).

Tangle Test semantic error 4 (61) =

Sample Document 4 (62)

class Test_SemanticError_4(TangleTestcase):
    """An optional feature of a Web."""
    text = test4_w
    file_path = Path("test4.w")
    def test_should_raise_noFullName(self) -> None:
        self.tangle_and_check_exception("No full name for 'part1...'")
Tangle Test semantic error 4 (61). Used by → test_tangler.py (55).

Sample Document 4 (62) =

test4_w = """Some anonymous chunk
@o test4.tmp
@{@<part1...@>
@<part2@>
@}@@
@d part1... @{This is part 1.@}
@d part2 @{This is part 2.@}
Okay, now for some errors: attempt to weave but no full name for part1....
"""
Sample Document 4 (62). Used by → Tangle Test semantic error 4 (61).

Tangle Test semantic error 5 (63) =

Sample Document 5 (64)

class Test_SemanticError_5(TangleTestcase):
    text = test5_w
    file_path = Path("test5.w")
    def test_should_raise_ambiguous(self) -> None:
        self.tangle_and_check_exception("Ambiguous abbreviation 'part1...', matches ['part1a', 'part1b']")
Tangle Test semantic error 5 (63). Used by → test_tangler.py (55).

Sample Document 5 (64) =

test5_w = """
Some anonymous chunk
@o test5.tmp
@{@<part1...@>
@<part2@>
@}@@
@d part1a @{This is part 1 a.@}
@d part1b @{This is part 1 b.@}
@d part2 @{This is part 2.@}
Okay, now for some errors: part1... is ambiguous
"""
Sample Document 5 (64). Used by → Tangle Test semantic error 5 (63).

Tangle Test semantic error 6 (65) =

Sample Document 6 (66)

class Test_SemanticError_6(TangleTestcase):
    text = test6_w
    file_path = Path("test6.w")
    def test_should_warn(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.web = pyweb.Web(chunks)
        self.tangler.emit(self.web)
        print(self.web.no_reference())
        self.assertEqual(1, len(self.web.no_reference()))
        self.assertEqual(1, len(self.web.multi_reference()))
        self.assertEqual({'part1a', 'part1...'}, self.tangler.reference_names)
Tangle Test semantic error 6 (65). Used by → test_tangler.py (55).

Sample Document 6 (66) =

test6_w = """Some anonymous chunk
@o test6.tmp
@{@<part1...@>
@<part1a@>
@}@@
@d part1a @{This is part 1 a.@}
@d part2 @{This is part 2.@}
Okay, now for some warnings:
- part1 has multiple references.
- part2 is unreferenced.
"""
Sample Document 6 (66). Used by → Tangle Test semantic error 6 (65).

Tangle Test include error 7 (67) =

Sample Document 7 and it's included file (68)

class Test_IncludeError_7(TangleTestcase):
    text = test7_w
    file_path = Path("test7.w")
    def setUp(self) -> None:
        Path('test7_inc.tmp').write_text(test7_inc_w)
        super().setUp()
    def test_should_include(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.web = pyweb.Web(chunks)
        self.tangler.emit(self.web)
        self.assertEqual(5, len(self.web.chunks))
        self.assertEqual(test7_inc_w, self.web.chunks[3].commands[0].text)
    def tearDown(self) -> None:
        Path('test7_inc.tmp').unlink()
        super().tearDown()
Tangle Test include error 7 (67). Used by → test_tangler.py (55).

Sample Document 7 and it's included file (68) =

test7_w = """
Some anonymous chunk.
@d title @[the title of this document, defined with @@[ and @@]@]
A reference to @<title@>.
@i test7_inc.tmp
A final anonymous chunk from test7.w
"""

test7_inc_w = """The test7a.tmp chunk for test7.w"""
Sample Document 7 and it's included file (68). Used by → Tangle Test include error 7 (67).

Tangle Test overheads: imports, etc. (69) =

"""Tangler tests exercise various semantic features."""
import io
import logging
import os
from pathlib import Path
from typing import ClassVar
import unittest

import pyweb
Tangle Test overheads: imports, etc. (69). Used by → test_tangler.py (55).

Tangle Test main program (70) =

if __name__ == "__main__":
    import sys
    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    unittest.main()
Tangle Test main program (70). Used by → test_tangler.py (55).

Tests for Weaving

We need to be able to weave a document from one or more source files.

test_weaver.py (71) =

Weave Test overheads: imports, etc. (79)Weave Test superclass to refactor common setup (72)Weave Test references and definitions (73)Weave Test evaluation of expressions (77)Weave Test main program (80)
test_weaver.py (71).

Weaving test cases have a common setup shown in this superclass.

Weave Test superclass to refactor common setup (72) =

class WeaveTestcase(unittest.TestCase):
    text: ClassVar[str]
    error: ClassVar[str]
    file_path: ClassVar[Path]

    def setUp(self) -> None:
        self.source = io.StringIO(self.text)
        self.rdr = pyweb.WebReader()
        self.maxDiff = None

    def tearDown(self) -> None:
        try:
            self.file_path.with_suffix(".html").unlink()
        except FileNotFoundError:
            pass
        try:
            self.file_path.with_suffix(".debug").unlink()
        except FileNotFoundError:
            pass
Weave Test superclass to refactor common setup (72). Used by → test_weaver.py (71).

Weave Test references and definitions (73) =

Sample Document 0 (74)Expected Output 0 (75)

class Test_RefDefWeave(WeaveTestcase):
    text = test0_w
    file_path = Path("test0.w")
    def test_load_should_createChunks(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.assertEqual(3, len(chunks))

    def test_weave_should_create_html(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.web = pyweb.Web(chunks)
        self.web.web_path = self.file_path
        doc = pyweb.Weaver( )
        doc.set_markup("html")
        doc.emit(self.web)
        actual = self.file_path.with_suffix(".html").read_text()
        self.maxDiff = None
        self.assertEqual(test0_expected_html, actual)

    def test_weave_should_create_debug(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.web = pyweb.Web(chunks)
        self.web.web_path = self.file_path
        doc = pyweb.Weaver( )
        doc.set_markup("debug")
        doc.emit(self.web)
        actual = self.file_path.with_suffix(".debug").read_text()
        self.maxDiff = None
        self.assertEqual(test0_expected_debug, actual)
Weave Test references and definitions (73). Used by → test_weaver.py (71).

Sample Document 0 (74) =

test0_w = """<html>
<head>
    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
</head>
<body>
@<some code@>

@d some code
@{
def fastExp(n, p):
    r = 1
    while p > 0:
        if p%2 == 1: return n*fastExp(n,p-1)
    return n*n*fastExp(n,p/2)

for i in range(24):
    fastExp(2,i)
@}
</body>
</html>
"""
Sample Document 0 (74). Used by → Weave Test references and definitions (73).

Expected Output 0 (75) =

test0_expected_html = """<html>
<head>
    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
</head>
<body>
&rarr;<a href="#pyweb_1"><em>some code (1)</em></a>


<a name="pyweb_1"></a>
<!--line number ('test0.w', 10)-->
<p><em>some code (1)</em> =</p>
<pre><code>
def fastExp(n, p):
    r = 1
    while p &gt; 0:
        if p%2 == 1: return n*fastExp(n,p-1)
    return n*n*fastExp(n,p/2)

for i in range(24):
    fastExp(2,i)

</code></pre>
<p>&#8718; <em>some code (1)</em>.

</p>

</body>
</html>
"""
Expected Output 0 (75). Used by → Weave Test references and definitions (73).

Expected Output 0 (76) +=

test0_expected_debug = (
    'text: TextCommand(text=\'<html>\\n<head>\\n    <link rel="StyleSheet" href="pyweb.css" type="text/css" />\\n</head>\\n<body>\\n\', location=(\'test0.w\', 1))\n'
    "ref: ReferenceCommand(name='some code', location=('test0.w', 6))"
    "text: TextCommand(text='\\n\\n', location=('test0.w', 7))\n"
    "begin_code: NamedChunk(name='some code', seq=1, commands=[CodeCommand(text='\\ndef fastExp(n, p):\\n    r = 1\\n    while p > 0:\\n        if p%2 == 1: return n*fastExp(n,p-1)\\n    return n*n*fastExp(n,p/2)\\n\\nfor i in range(24):\\n    fastExp(2,i)\\n', location=('test0.w', 10))], options=[], def_names=[], initial=True, comment_start=None, comment_end=None, references=0, referencedBy=None, logger=<Logger Chunk (INFO)>)\n"
    "code: CodeCommand(text='\\ndef fastExp(n, p):\\n    r = 1\\n    while p > 0:\\n        if p%2 == 1: return n*fastExp(n,p-1)\\n    return n*n*fastExp(n,p/2)\\n\\nfor i in range(24):\\n    fastExp(2,i)\\n', location=('test0.w', 10))\n"
    "end_code: NamedChunk(name='some code', seq=1, commands=[CodeCommand(text='\\ndef fastExp(n, p):\\n    r = 1\\n    while p > 0:\\n        if p%2 == 1: return n*fastExp(n,p-1)\\n    return n*n*fastExp(n,p/2)\\n\\nfor i in range(24):\\n    fastExp(2,i)\\n', location=('test0.w', 10))], options=[], def_names=[], initial=True, comment_start=None, comment_end=None, references=0, referencedBy=None, logger=<Logger Chunk (INFO)>)\n"
    "text: TextCommand(text='\\n</body>\\n</html>\\n', location=('test0.w', 19))"
    )
Expected Output 0 (76). Used by → Weave Test references and definitions (73).

Note that this really requires a mocked time module in order to properly provide a consistent output from time.asctime().

Weave Test evaluation of expressions (77) =

Sample Document 9 (78)

from unittest.mock import Mock

class TestEvaluations(WeaveTestcase):
    text = test9_w
    file_path = Path("test9.w")
    def setUp(self):
        super().setUp()
        self.mock_time = Mock(asctime=Mock(return_value="mocked time"))
    def test_should_evaluate(self) -> None:
        chunks = self.rdr.load(self.file_path, self.source)
        self.web = pyweb.Web(chunks)
        self.web.web_path = self.file_path
        doc = pyweb.Weaver( )
        doc.set_markup("html")
        doc.emit(self.web)
        actual = self.file_path.with_suffix(".html").read_text().splitlines()
        #print(actual)
        self.assertEqual("An anonymous chunk.", actual[0])
        self.assertTrue("Time = mocked time", actual[1])
        self.assertEqual("File = ('test9.w', 3)", actual[2])
        self.assertEqual('Version = 3.2', actual[3])
        self.assertEqual(f'CWD = {os.getcwd()}', actual[4])
Weave Test evaluation of expressions (77). Used by → test_weaver.py (71).

Sample Document 9 (78) =

test9_w= """An anonymous chunk.
Time = @(time.asctime()@)
File = @(theLocation@)
Version = @(__version__@)
CWD = @(os.path.realpath('.')@)
"""
Sample Document 9 (78). Used by → Weave Test evaluation of expressions (77).

Weave Test overheads: imports, etc. (79) =

"""Weaver tests exercise various weaving features."""
import io
import logging
import os
from pathlib import Path
import string
import sys
from textwrap import dedent
from typing import ClassVar
import unittest

import pyweb
Weave Test overheads: imports, etc. (79). Used by → test_weaver.py (71).

Weave Test main program (80) =

if __name__ == "__main__":
    logging.basicConfig(stream=sys.stderr, level=logging.WARN)
    unittest.main()
Weave Test main program (80). Used by → test_weaver.py (71).

Additional Scripts Testing

We provide these two additional scripts; effectively command-line short-cuts:

These need their own test cases.

This gives us the following outline for the script testing.

test_scripts.py (81) =

Script Test overheads: imports, etc. (86)Sample web file to test with (82)Superclass for test cases (83)Test of weave.py (84)Test of tangle.py (85)Scripts Test main (87)
test_scripts.py (81).

Sample Web File

This is a web .w file to create a document and tangle a small file.

Sample web file to test with (82) =

sample = textwrap.dedent("""
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Sample HTML web file</title>
      </head>
      <body>
        <h1>Sample HTML web file</h1>
        <p>We're avoiding using Python specifically.
        This hints at other languages being tangled by this tool.</p>

    @o sample_tangle.code
    @{
    @<preamble@>
    @<body@>
    @}

    @d preamble
    @{
    #include <stdio.h>
    @}

    @d body
    @{
    int main() {
        println("Hello, World!")
    }
    @}

      </body>
    </html>
    """)
Sample web file to test with (82). Used by → test_scripts.py (81).

Superclass for test cases

This superclass definition creates a consistent test fixture for both test cases. The sample test_sample.w file is created and removed after the test.

Superclass for test cases (83) =

class SampleWeb(unittest.TestCase):
    def setUp(self) -> None:
        self.sample_path = Path("test_sample.w")
        self.sample_path.write_text(sample)
        self.maxDiff = None

    def tearDown(self) -> None:
        self.sample_path.unlink()

    def assertEqual_Ignore_Blank_Lines(self, first: str, second: str, msg: str=None) -> None:
        """Skips blank lines and trailing whitespace that (generally) aren't problems when weaving."""
        def non_blank(line: str) -> bool:
            return len(line) > 0
        first_nb = '\n'.join(filter(non_blank, (line.rstrip() for line in first.splitlines())))
        second_nb = '\n'.join(filter(non_blank, (line.rstrip() for line in second.splitlines())))
        self.assertEqual(first_nb, second_nb, msg)
Superclass for test cases (83). Used by → test_scripts.py (81).

Weave Script Test

We check the weave output to be sure it's what we expected. This could be altered to check a few features of the weave file rather than compare the entire file.

Test of weave.py (84) =

expected_weave = ('<!doctype html>\n'
    '<html lang="en">\n'
    '  <head>\n'
    '    <meta charset="utf-8">\n'
    '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
    '    <title>Sample HTML web file</title>\n'
    '  </head>\n'
    '  <body>\n'
    '    <h1>Sample HTML web file</h1>\n'
    "    <p>We're avoiding using Python specifically.\n"
    '    This hints at other languages being tangled by this tool.</p>\n'
    '<div class="card">\n'
    '  <div class="card-header">\n'
    '    <a type="button" class="btn btn-primary" name="pyweb_1"></a>\n'
    "    <!--line number ('test_sample.w', 16)-->\n"
    '    <p class="small"><em>sample_tangle.code (1)</em> =</p>\n'
    '   </div>\n'
    '  <div class="card-body">\n'
    '    <pre><code>\n'
    '&rarr;<a href="#pyweb_2"><em>preamble (2)</em></a>\n'
    '&rarr;<a href="#pyweb_3"><em>body (3)</em></a>\n'
    '    </code></pre>\n'
    '  </div>\n'
    '<div class="card-footer">\n'
    '  <p>&#8718; <em>sample_tangle.code (1)</em>.\n'
    '  </p>\n'
    '</div>\n'
    '</div>\n'
    '<div class="card">\n'
    '  <div class="card-header">\n'
    '    <a type="button" class="btn btn-primary" name="pyweb_2"></a>\n'
    "    <!--line number ('test_sample.w', 22)-->\n"
    '    <p class="small"><em>preamble (2)</em> =</p>\n'
    '   </div>\n'
    '  <div class="card-body">\n'
    '    <pre><code>\n'
    '#include &lt;stdio.h&gt;\n'
    '    </code></pre>\n'
    '  </div>\n'
    '<div class="card-footer">\n'
    '  <p>&#8718; <em>preamble (2)</em>.\n'
    '  </p>\n'
    '</div>\n'
    '</div>\n'
    '<div class="card">\n'
    '  <div class="card-header">\n'
    '    <a type="button" class="btn btn-primary" name="pyweb_3"></a>\n'
    "    <!--line number ('test_sample.w', 27)-->\n"
    '    <p class="small"><em>body (3)</em> =</p>\n'
    '   </div>\n'
    '  <div class="card-body">\n'
    '    <pre><code>\n'
    'int main() {\n'
    '    println(&quot;Hello, World!&quot;)\n'
    '}\n'
    '    </code></pre>\n'
    '  </div>\n'
    '<div class="card-footer">\n'
    '  <p>&#8718; <em>body (3)</em>.\n'
    '  </p>\n'
    '</div>\n'
    '</div>\n'
    '  </body>\n'
    '</html>')

class TestWeave(SampleWeb):
    def setUp(self) -> None:
        super().setUp()
        self.output = self.sample_path.with_suffix(".html")
        self.maxDiff = None

    def test(self) -> None:
        weave.main(self.sample_path)
        result = self.output.read_text()
        self.assertEqual_Ignore_Blank_Lines(expected_weave, result)

    def tearDown(self) -> None:
        super().tearDown()
        self.output.unlink()
Test of weave.py (84). Used by → test_scripts.py (81).

Tangle Script Test

We check the tangle output to be sure it's what we expected.

Test of tangle.py (85) =

expected_tangle = textwrap.dedent("""

    #include <stdio.h>


    int main() {
        println("Hello, World!")
    }

    """)

class TestTangle(SampleWeb):
    def setUp(self) -> None:
        super().setUp()
        self.output = Path("sample_tangle.code")

    def test(self) -> None:
        tangle.main(self.sample_path)
        result = self.output.read_text()
        self.assertEqual(expected_tangle, result)

    def tearDown(self) -> None:
        super().tearDown()
        self.output.unlink()
Test of tangle.py (85). Used by → test_scripts.py (81).

Overheads and Main Script

This is typical of the other test modules. We provide a unittest runner here in case we want to run these tests in isolation.

Script Test overheads: imports, etc. (86) =

"""Script tests."""
import logging
from pathlib import Path
import sys
import textwrap
import unittest

import tangle
import weave
Script Test overheads: imports, etc. (86). Used by → test_scripts.py (81).

Scripts Test main (87) =

if __name__ == "__main__":
    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    unittest.main()
Scripts Test main (87). Used by → test_scripts.py (81).

We run the default unittest.main() to execute the entire suite of tests.

No Longer supported: @i runner.w, using pytest seems better.

Additional Files

To get the RST to look good, there are two additional files. These are clones of what's in the src directory.

docutils.conf defines two CSS files to use.
The default CSS file may need to be customized.

docutils.conf (88) =

# docutils.conf

[html4css1 writer]
stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css,
    page-layout.css
syntax-highlight: long
docutils.conf (88).

page-layout.css This tweaks one CSS to be sure that the resulting HTML pages are easier to read. These are minor tweaks to the default CSS.

page-layout.css (89) =

/* Page layout tweaks */
div.document { width: 7in; }
.small { font-size: smaller; }
.code
{
    color: #101080;
    display: block;
    border-color: black;
    border-width: thin;
    border-style: solid;
    background-color: #E0FFFF;
    /*#99FFFF*/
    padding: 0 0 0 1%;
    margin: 0 6% 0 6%;
    text-align: left;
    font-size: smaller;
}
page-layout.css (89).

Indices

Files

test_unit.py:test_unit.py (1):test_loader.py: → test_loader.py (46):test_tangler.py: → test_tangler.py (55):test_weaver.py: → test_weaver.py (71):test_scripts.py: → test_scripts.py (81):docutils.conf: → docutils.conf (88):page-layout.css: → page-layout.css (89)

Macros

Expected Output 0:
 Expected Output 0 (75), → Expected Output 0 (76)
Load Test error handling with a few common syntax errors:
 Load Test error handling with a few common syntax errors (49)
Load Test include processing with syntax errors:
 Load Test include processing with syntax errors (51)
Load Test main program:
 Load Test main program (54)
Load Test overheads:
 imports, etc.: → Load Test overheads: imports, etc. (48), → Load Test overheads: imports, etc. (53)
Load Test superclass to refactor common setup:
 Load Test superclass to refactor common setup (47)
Sample Document 0:
 Sample Document 0 (74)
Sample Document 1 with correct and incorrect syntax:
 Sample Document 1 with correct and incorrect syntax (50)
Sample Document 2:
 Sample Document 2 (58)
Sample Document 3:
 Sample Document 3 (60)
Sample Document 4:
 Sample Document 4 (62)
Sample Document 5:
 Sample Document 5 (64)
Sample Document 6:
 Sample Document 6 (66)
Sample Document 7 and it's included file:
 Sample Document 7 and it's included file (68)
Sample Document 8 and the file it includes:
 Sample Document 8 and the file it includes (52)
Sample Document 9:
 Sample Document 9 (78)
Sample web file to test with:
 Sample web file to test with (82)
Script Test overheads:
 imports, etc.: → Script Test overheads: imports, etc. (86)
Scripts Test main:
 Scripts Test main (87)
Superclass for test cases:
 Superclass for test cases (83)
Tangle Test include error 7:
 Tangle Test include error 7 (67)
Tangle Test main program:
 Tangle Test main program (70)
Tangle Test overheads:
 imports, etc.: → Tangle Test overheads: imports, etc. (69)
Tangle Test semantic error 2:
 Tangle Test semantic error 2 (57)
Tangle Test semantic error 3:
 Tangle Test semantic error 3 (59)
Tangle Test semantic error 4:
 Tangle Test semantic error 4 (61)
Tangle Test semantic error 5:
 Tangle Test semantic error 5 (63)
Tangle Test semantic error 6:
 Tangle Test semantic error 6 (65)
Tangle Test superclass to refactor common setup:
 Tangle Test superclass to refactor common setup (56)
Test of tangle.py:
 Test of tangle.py (85)
Test of weave.py:
 Test of weave.py (84)
Unit Test Mock Chunk class:
 Unit Test Mock Chunk class (4)
Unit Test main:Unit Test main (45)
Unit Test of Action class hierarchy:
 Unit Test of Action class hierarchy (37)
Unit Test of Application class:
 Unit Test of Application class (42)
Unit Test of Chunk References:
 Unit Test of Chunk References (22)
Unit Test of Chunk class hierarchy:
 Unit Test of Chunk class hierarchy (10)
Unit Test of Chunk construction:
 Unit Test of Chunk construction (15)
Unit Test of Chunk interrogation:
 Unit Test of Chunk interrogation (16)
Unit Test of Chunk properties:
 Unit Test of Chunk properties (17)
Unit Test of Chunk superclass:
 Unit Test of Chunk superclass (11), → Unit Test of Chunk superclass (12), → Unit Test of Chunk superclass (13), → Unit Test of Chunk superclass (14)
Unit Test of CodeCommand class to contain a program source code block:
 Unit Test of CodeCommand class to contain a program source code block (26)
Unit Test of Command class hierarchy:
 Unit Test of Command class hierarchy (23)
Unit Test of Command superclass:
 Unit Test of Command superclass (24)
Unit Test of Emitter Superclass:
 Unit Test of Emitter Superclass (3)
Unit Test of Emitter class hierarchy:
 Unit Test of Emitter class hierarchy (2)
Unit Test of FileXrefCommand class for an output file cross-reference:
 Unit Test of FileXrefCommand class for an output file cross-reference (28)
Unit Test of HTML subclass of Emitter:
 Unit Test of HTML subclass of Emitter (7)
Unit Test of LaTeX subclass of Emitter:
 Unit Test of LaTeX subclass of Emitter (6)
Unit Test of MacroXrefCommand class for a named chunk cross-reference:
 Unit Test of MacroXrefCommand class for a named chunk cross-reference (29)
Unit Test of NamedChunk subclass:
 Unit Test of NamedChunk subclass (18)
Unit Test of NamedChunk_Noindent subclass:
 Unit Test of NamedChunk_Noindent subclass (19)
Unit Test of NamedDocumentChunk subclass:
 Unit Test of NamedDocumentChunk subclass (21)
Unit Test of OutputChunk subclass:
 Unit Test of OutputChunk subclass (20)
Unit Test of ReferenceCommand class for chunk references:
 Unit Test of ReferenceCommand class for chunk references (31)
Unit Test of Tangler subclass of Emitter:
 Unit Test of Tangler subclass of Emitter (8)
Unit Test of TanglerMake subclass of Emitter:
 Unit Test of TanglerMake subclass of Emitter (9)
Unit Test of TextCommand class to contain a document text block:
 Unit Test of TextCommand class to contain a document text block (25)
Unit Test of UserIdXrefCommand class for a user identifier cross-reference:
 Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30)
Unit Test of Weaver subclass of Emitter:
 Unit Test of Weaver subclass of Emitter (5)
Unit Test of Web class:
 Unit Test of Web class (32)
Unit Test of WebReader class:
 Unit Test of WebReader class (33), → Unit Test of WebReader class (34), → Unit Test of WebReader class (35), → Unit Test of WebReader class (36)
Unit Test of XrefCommand superclass for all cross-reference commands:
 Unit Test of XrefCommand superclass for all cross-reference commands (27)
Unit Test overheads:
 imports, etc.: → Unit Test overheads: imports, etc. (43), → Unit Test overheads: imports, etc. (44)
Unit test of Action Sequence class:
 Unit test of Action Sequence class (38)
Unit test of LoadAction class:
 Unit test of LoadAction class (41)
Unit test of TangleAction class:
 Unit test of TangleAction class (40)
Unit test of WeaverAction class:
 Unit test of WeaverAction class (39)
Weave Test evaluation of expressions:
 Weave Test evaluation of expressions (77)
Weave Test main program:
 Weave Test main program (80)
Weave Test overheads:
 imports, etc.: → Weave Test overheads: imports, etc. (79)
Weave Test references and definitions:
 Weave Test references and definitions (73)
Weave Test superclass to refactor common setup:
 Weave Test superclass to refactor common setup (72)

Created by src/pyweb.py at Tue Jul 19 14:29:45 2022.

Source tests/pyweb_test.w modified Sat Jul 2 09:39:56 2022.

pyweb.__version__ '3.2'.

Working directory '/Users/slott/Documents/Projects/py-web-tool'.