def test_prologue_evaluate_inner_break_loop(mocker): """ Check that an infinite include loop is detected """ pro = Prologue() m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a context with a bunch of mock files ctx = Context(pro) for _x in range(randint(10, 30)): ctx.stack_push(RegistryFile()) ctx.stack[-1].path = Path(random_str(5, 10) + "." + random_str(5, 10)) # Try evaluating files that are already on the stack for _x in range(100): r_file = choice(ctx.stack) m_reg.resolve.side_effect = [r_file] with pytest.raises(PrologueError) as excinfo: next(pro.evaluate_inner(r_file.filename, ctx)) assert ( f"Detected infinite recursion when including file '{r_file.filename}'" f" - file stack: {', '.join([x.filename for x in ctx.stack])}" ) == str(excinfo.value) m_reg.resolve.assert_has_calls([call(r_file.filename)]) m_reg.reset_mock() # Check a new file is pushed to the stack new_file = RegistryFile() new_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [new_file] m_con.return_value = [random_str(5, 10), random_str(5, 10)] next(pro.evaluate_inner(new_file.filename, ctx)) assert ctx.stack[-1] == new_file
def test_prologue_evaluate_inner_line_span(mocker): """ Test use of line spanning using '\' to escape new line """ pro = Prologue() ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a fake file r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [r_file] # Setup fake file contents intro = [random_str(10, 50, spaces=True) for _x in range(randint(5, 10))] span = [(random_str(10, 50, spaces=True) + "\\") for _x in range(randint(5, 10))] span += [random_str(10, 50, spaces=True)] outro = [random_str(10, 50, spaces=True) for _x in range(randint(5, 10))] m_con.return_value = intro + span + outro # Pull all lines out of the evaluate loop result = [x for x in pro.evaluate_inner(r_file.filename, ctx)] # Checks assert result == intro + ["".join([x.replace("\\", "") for x in span])] + outro m_reg.resolve.assert_has_calls([call(r_file.filename)]) assert ctx.stack == []
def test_prologue_evaluate_inner_block_floating(mocker): """ Test that floating block directives are flagged """ # Choose a delimiter delim = choice(("#", "@", "$", "%", "!")) # Create preprocessor, context, etc pro = Prologue(delimiter=delim) ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a block directive class BlockDirx(BlockDirective): def __init__(self, parent, src_file=None, src_line=0, callback=None): super().__init__( parent, yields=True, src_file=src_file, src_line=src_line, callback=callback, ) opening = [random_str(5, 10) for _x in range(randint(1, 5))] closing = [random_str(5, 10, avoid=opening) for _x in range(1, 5)] transit = [ random_str(5, 10, avoid=opening + closing) for _x in range(1, 5) ] pro.register_directive( DirectiveWrap(BlockDirx, opening, transition=transit, closing=closing)) # Create a fake file r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [r_file] # Setup fake file contents contents = [] used_open = [] for idx in range(randint(50, 100)): if choice((True, False)): used_open.append(choice(opening)) contents.append( random_str(50, 100, spaces=True) + f" {delim}{used_open[-1]} {random_str(50, 100, spaces=True)}") else: contents.append(random_str(50, 100, spaces=True)) m_con.return_value = [ Line(x, r_file, i + 1) for i, x in enumerate(contents) ] # Catch the floating block error with pytest.raises(PrologueError) as excinfo: [x for x in pro.evaluate_inner(r_file.filename, ctx)] assert (f"The directive '{used_open[0].lower()}' can only be used with an " f"anchored delimiter as it is a block directive") == str( excinfo.value)
def test_reg_file(tmp_path): """ Test that RegistryFile can locate a real file """ real_path = tmp_path / "my_file.txt" with open(real_path, "w") as fh: fh.write("dummy content") r_file = RegistryFile(real_path) assert r_file.filename == "my_file.txt"
def test_reg_file_string(tmp_path): """ Test that RegistryFile accepts a path as a string """ real_path = tmp_path / "my_file.txt" with open(real_path, "w") as fh: fh.write("dummy content") r_file = RegistryFile(str(real_path.as_posix())) assert r_file.filename == "my_file.txt"
def test_reg_file_bad_type(tmp_path): """ Test that RegistryFile raises error about bad file type """ bad_path = tmp_path / "my_folder" bad_path.mkdir() with pytest.raises(PrologueError) as excinfo: RegistryFile(bad_path) assert f"Path provided is not a file {bad_path}" in str(excinfo.value)
def test_context_stack_trace_consistency(mocker): """ Make random pushes and pops from stack and check consistency """ mocker.patch.object(RegistryFile, "__init__", lambda x, z: None) ctx = Context(None) state = [] trace = [] for _x in range(100): # Perform random stack pushes for _y in range(randint(1, 10)): state.append(RegistryFile(random_str(5, 10))) trace.append(state[-1]) ctx.stack_push(state[-1]) assert ctx.stack == state assert ctx.stack_top() == state[-1] # Perform random stack pops for _y in range(randint(1, 10)): if ctx.stack_top() == None: assert len(state) == 0 break assert ctx.stack_top() == state[-1] assert ctx.stack_pop() == state[-1] state.pop() # Check the final stack state assert ctx.stack == state assert ctx.trace == trace
def test_context_bad_pop(mocker): """ Try popping from an empty stack """ mocker.patch.object(RegistryFile, "__init__", lambda x, z: None) ctx = Context(None) for _x in range(100): for _y in range(randint(1, 10)): ctx.stack_push(RegistryFile(random_str(5, 10))) while ctx.stack_top() != None: ctx.stack_pop() with pytest.raises(PrologueError) as excinfo: ctx.stack_pop() assert "Trying to pop file from empty stack" == str(excinfo.value)
def test_context_inherit_stack_and_trace(mocker): """ Test that the stack and trace are always held by the root """ root = Context(None) child_a = Context(None, parent=root) child_b = Context(None, parent=child_a) # Push a stack entry to each layer # NOTE: First we patch RegistryFile's __init__ routine to avoid path check mocker.patch.object(RegistryFile, "__init__", lambda x, z: None) root_node = RegistryFile(random_str(30, 50)) child_a_node = RegistryFile(random_str(30, 50)) child_b_node = RegistryFile(random_str(30, 50)) root.stack_push(root_node) child_a.stack_push(child_a_node) child_b.stack_push(child_b_node) # Check the stack (at all levels) assert root.trace == [root_node, child_a_node, child_b_node] assert child_a.trace == [root_node, child_a_node, child_b_node] assert child_b.trace == [root_node, child_a_node, child_b_node] # Check that the trace is currently the same set assert root.trace == [root_node, child_a_node, child_b_node] assert child_a.trace == [root_node, child_a_node, child_b_node] assert child_b.trace == [root_node, child_a_node, child_b_node] # Try popping from stack at different levels assert choice((root, child_a, child_b)).stack_pop() == child_b_node assert choice((root, child_a, child_b)).stack_top() == child_a_node assert choice((root, child_a, child_b)).stack_pop() == child_a_node assert choice((root, child_a, child_b)).stack_top() == root_node assert choice((root, child_a, child_b)).stack_pop() == root_node assert choice((root, child_a, child_b)).stack_top() == None # Check stack is empty assert root.stack == [] assert child_a.stack == [] assert child_b.stack == [] # Check that the trace is unchanged assert root.trace == [root_node, child_a_node, child_b_node] assert child_a.trace == [root_node, child_a_node, child_b_node] assert child_b.trace == [root_node, child_a_node, child_b_node]
def test_prologue_evaluate_inner_plain(mocker): """ Check that a plain sequence of lines is reproduced within alteration """ pro = Prologue() ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a fake file r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [r_file] # Setup fake file contents contents = [ random_str(10, 50, spaces=True) for _x in range(randint(50, 100)) ] m_con.return_value = contents # Pull all lines out of the evaluate loop result = [x for x in pro.evaluate_inner(r_file.filename, ctx)] # Checks assert result == contents m_reg.resolve.assert_has_calls([call(r_file.filename)]) assert ctx.stack == []
def test_reg_file_contents(tmp_path): """ Check that RegistryFile correctly reads back contents of a file """ real_path = tmp_path / "my_file.txt" lines = [ "dummy line A", "dummy line B ", "dummy line C ", ] with open(real_path, "w") as fh: fh.write("\n".join(lines)) r_file = RegistryFile(str(real_path.as_posix())) assert r_file.filename == "my_file.txt" for idx, line in enumerate(r_file.contents): assert isinstance(line, Line) assert line.number == (idx + 1) assert line.file == r_file assert str(line) == lines[idx].rstrip()
def test_prologue_evaluate_inner_block_trailing(mocker): """ Check that unclosed blocks at the end of the file are detected """ # Choose a delimiter delim = choice(("#", "@", "$", "%", "!")) # Create a pair of block directives dirx_inst = [] class BlockDirx(BlockDirective): def __init__(self, parent, src_file, src_line, callback=None): super().__init__( parent, yields=True, src_file=src_file, src_line=src_line, callback=callback, ) dirx_inst.append(self) opening = [random_str(5, 10) for _x in range(randint(1, 5))] closing = [random_str(5, 10, avoid=opening) for _x in range(randint(1, 5))] transit = [ random_str(5, 10, avoid=opening + closing) for _x in range(randint(1, 5)) ] BlockDirx.OPENING = opening # Create a fake file mocker.patch.object(RegistryFile, "__init__", lambda x: None) r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) # Create preprocessor, context, etc for _x in range(100): pro = Prologue(delimiter=delim) ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) m_reg.resolve.side_effect = [r_file] m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) pro.register_directive( DirectiveWrap(BlockDirx, opening, transition=transit, closing=closing)) # Setup fake file contents contents = [] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] open_idx = len(contents) contents += [ f"{delim}{choice(opening)} {random_str(50, 100, spaces=True)}" ] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] for _y in range(randint(0, 3)): contents += [ f"{delim}{choice(transit)} {random_str(50, 100, spaces=True)}" ] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] m_con.return_value = [ Line(x, r_file, i + 1) for i, x in enumerate(contents) ] # Expected an unclosed directive with pytest.raises(PrologueError) as excinfo: [x for x in pro.evaluate_inner(r_file.filename, ctx)] assert str(excinfo.value).startswith( f"Unmatched BlockDirx block directive in {r_file.path}:{open_idx+1}:" )
def test_prologue_evaluate_inner_block_confused(mocker): """ Check that one block can't be closed by another's tags """ # Choose a delimiter delim = choice(("#", "@", "$", "%", "!")) # Create a pair of block directives class BlockDirA(BlockDirective): def __init__(self, parent, src_file=None, src_line=0, callback=None): super().__init__( parent, yields=True, src_file=src_file, src_line=src_line, callback=callback, ) class BlockDirB(BlockDirective): def __init__(self, parent, src_file=None, src_line=0, callback=None): super().__init__( parent, yields=True, src_file=src_file, src_line=src_line, callback=callback, ) all_tags = [] opening_a = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] all_tags += opening_a closing_a = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] all_tags += closing_a transit_a = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] all_tags += transit_a opening_b = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] all_tags += opening_b closing_b = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] all_tags += closing_b transit_b = [ random_str(5, 10, avoid=all_tags) for _x in range(randint(1, 5)) ] # Create a fake file mocker.patch.object(RegistryFile, "__init__", lambda x: None) r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) # Create preprocessor, context, etc for _x in range(100): pro = Prologue(delimiter=delim) ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) m_reg.resolve.side_effect = [r_file] m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) pro.register_directive( DirectiveWrap(BlockDirA, opening_a, transition=transit_a, closing=closing_a)) pro.register_directive( DirectiveWrap(BlockDirB, opening_b, transition=transit_b, closing=closing_b)) # Setup fake file contents bad_tag = choice(transit_b + closing_b) contents = [] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] contents += [ f"{delim}{choice(opening_a)} {random_str(50, 100, spaces=True)}" ] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] contents += [f"{delim}{bad_tag} {random_str(50, 100, spaces=True)}"] contents += [ random_str(50, 100, spaces=True) for _x in range(randint(5, 10)) ] m_con.return_value = [ Line(x, r_file, i + 1) for i, x in enumerate(contents) ] # Expect an unexpected transition tag with pytest.raises(PrologueError) as excinfo: [x for x in pro.evaluate_inner(r_file.filename, ctx)] if bad_tag in transit_b: assert ( f"Transition tag '{bad_tag.lower()}' was not expected") == str( excinfo.value) else: assert ( f"Closing tag '{bad_tag.lower()}' was not expected") == str( excinfo.value)
def test_prologue_evaluate_inner_block(mocker, should_yield): """ Check that a block directive is detected """ # Choose a delimiter delim = choice(("#", "@", "$", "%", "!")) # Create preprocessor, context, etc pro = Prologue(delimiter=delim) ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a line directive dirx_inst = [] class BlockDirx(BlockDirective): def __init__(self, parent, src_file=None, src_line=0, callback=None): super().__init__( parent, yields=should_yield, src_file=src_file, src_line=src_line, callback=callback, ) dirx_inst.append(self) mocker.patch.object(BlockDirx, "open", autospec=True) mocker.patch.object(BlockDirx, "transition", autospec=True) mocker.patch.object(BlockDirx, "close", autospec=True) mocker.patch.object(BlockDirx, "evaluate", autospec=True) def do_open(self, tag, arguments): self._BlockDirective__opened = True def do_close(self, tag, arguments): self._BlockDirective__closed = True BlockDirx.open.side_effect = do_open BlockDirx.close.side_effect = do_close dirx_text = [] for _x in range(randint(5, 10)): dirx_text.append(random_str(20, 30, spaces=True)) def block_eval(self, context): for line in dirx_text: yield Line(line, None, randint(1, 10000)) BlockDirx.evaluate.side_effect = block_eval opening = [random_str(5, 10) for _x in range(randint(1, 5))] closing = [random_str(5, 10, avoid=opening) for _x in range(1, 5)] transit = [ random_str(5, 10, avoid=opening + closing) for _x in range(1, 5) ] pro.register_directive( DirectiveWrap(BlockDirx, opening, transition=transit, closing=closing)) # Create a fake file r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [r_file] # Setup fake file contents contents = [] output = [] open_calls = [] tran_calls = [] close_calls = [] for idx in range(randint(50, 100)): use_dirx = choice((True, False)) open_arg = random_str(50, 100, spaces=True) tran_args = [ random_str(50, 100, spaces=True) for _x in range(randint(0, 3)) ] close_arg = random_str(50, 100, spaces=True) open_tag = choice(opening) close_tag = choice(closing) tran_tag = choice(transit) if use_dirx: contents.append(f"{delim}{open_tag} {open_arg}") else: contents.append(random_str(50, 100, spaces=True)) # If this is a directive, generate transitions and closing if use_dirx: # Opening block contents for _x in range(randint(5, 10)): contents.append(random_str(20, 30, spaces=True)) # Transitions for arg in tran_args: contents.append(f"{delim}{tran_tag} {arg}") for _x in range(5, 10): contents.append(random_str(20, 30, spaces=True)) contents.append(f"{delim}{close_tag} {close_arg}") # Setup expected output if use_dirx and should_yield: output += dirx_text elif not use_dirx: output.append(contents[-1]) # Accumulate calls if use_dirx: open_calls.append(call(ANY, open_tag.lower(), open_arg)) for arg in tran_args: tran_calls.append(call(ANY, tran_tag.lower(), arg)) close_calls.append(call(ANY, close_tag.lower(), close_arg)) m_con.return_value = [ Line(x, r_file, i + 1) for i, x in enumerate(contents) ] # Create a dummy callback def dummy_cb(): pass # Pull all lines out of the evaluate loop result = [ x for x in pro.evaluate_inner(r_file.filename, ctx, callback=dummy_cb) ] # Checks assert len(result) == len(output) assert ctx.stack == [] m_reg.resolve.assert_has_calls([call(r_file.filename)]) for got_out, exp_out in zip(result, output): assert str(got_out) == exp_out.rstrip(" ") BlockDirx.open.assert_has_calls(open_calls) BlockDirx.transition.assert_has_calls(tran_calls) BlockDirx.close.assert_has_calls(close_calls) for dirx in dirx_inst: assert dirx.callback == dummy_cb
def test_prologue_evaluate_inner_line(mocker, should_yield): """ Check that a line directive is detected """ # Choose a delimiter delim = choice(("#", "@", "$", "%", "!")) # Create preprocessor, context, etc pro = Prologue(delimiter=delim) ctx = Context(pro) m_reg = mocker.patch.object(pro, "registry", autospec=True) mocker.patch.object(RegistryFile, "__init__", lambda x: None) m_con = mocker.patch.object(RegistryFile, "contents", new_callable=PropertyMock) # Create a line directive dirx_inst = [] class LineDirx(LineDirective): def __init__(self, parent, src_file=None, src_line=0, callback=None): super().__init__( parent, yields=should_yield, src_file=src_file, src_line=src_line, callback=callback, ) dirx_inst.append(self) mocker.patch.object(LineDirx, "invoke", autospec=True) mocker.patch.object(LineDirx, "evaluate", autospec=True) dirx_text = "LINE DIRX " + random_str(20, 30, spaces=True) + " END LINE" def line_eval(self, context): yield Line(dirx_text, None, randint(1, 10000)) LineDirx.evaluate.side_effect = line_eval opening = [random_str(5, 10) for _x in range(randint(1, 5))] pro.register_directive(DirectiveWrap(LineDirx, opening)) # Create a fake file r_file = RegistryFile() r_file.path = Path(random_str(5, 10) + "." + random_str(5, 10)) m_reg.resolve.side_effect = [r_file] # Setup fake file contents contents = [] output = [] dirx_calls = [] for idx in range(randint(50, 100)): use_dirx = choice((True, False)) anchor = choice((True, False)) argument = random_str(50, 100, spaces=True) use_tag = choice(opening) line_txt = "" if use_dirx: if not anchor: line_txt += random_str(50, 100, spaces=True) + " " line_txt += f"{delim}{use_tag} {argument}" else: line_txt += random_str(50, 100, spaces=True) # Accumulate the data to push into evaluate contents.append(line_txt) # Accumulate expected outputs if not (use_dirx and anchor): output.append(line_txt.split(delim)[0]) if should_yield: if use_dirx and anchor: output.append(dirx_text) if use_dirx and not anchor: output.append(dirx_text) # Accumulate calls if use_dirx: dirx_calls.append(call(ANY, use_tag.lower(), argument)) m_con.return_value = [ Line(x, r_file, i + 1) for i, x in enumerate(contents) ] # Create a dummy callback def dummy_cb(): pass # Pull all lines out of the evaluate loop result = [ x for x in pro.evaluate_inner(r_file.filename, ctx, callback=dummy_cb) ] # Checks assert len(result) == len(output) assert ctx.stack == [] m_reg.resolve.assert_has_calls([call(r_file.filename)]) for got_out, exp_out in zip(result, output): assert str(got_out) == exp_out.rstrip(" ") LineDirx.invoke.assert_has_calls(dirx_calls) for dirx in dirx_inst: assert dirx.callback == dummy_cb
def test_reg_file_bad_path(tmp_path): """ Test that RegistryFile raises error about missing file """ bad_file = tmp_path / "my_file.txt" with pytest.raises(PrologueError) as excinfo: RegistryFile(bad_file) assert f"File does not exist at path {bad_file}" in str(excinfo.value)