Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

literal tests #29

Open
Jelleas opened this issue Dec 19, 2023 · 2 comments
Open

literal tests #29

Jelleas opened this issue Dec 19, 2023 · 2 comments

Comments

@Jelleas
Copy link
Owner

Jelleas commented Dec 19, 2023

@literal()
def testGuess():
     """number of guesses decreases after incorrect guess"""
     hangman = Hangman("hello", 5)
     assert hangman.number_of_guesses() == 5
     assert not hangman.guess("a")
     assert hangman.number_of_guesses() == 4
     assert hangman.guess("e")
     assert hangman.number_of_guesses() == 4

produces the following output on AssertionError:

This check failed. Run the following code in the terminal to see why:
$ python3
>>> from hangman import *
>>> hangman = Hangman("hello", 5)
>>> assert hangman.number_of_guesses() == 5
>>> assert not hangman.guess("a")
>>> assert hangman.number_of_guesses() == 4
>>> assert hangman.guess("e")
>>> assert hangman.number_of_guesses() == 4
@Jelleas
Copy link
Owner Author

Jelleas commented Dec 19, 2023

@Jelleas
Copy link
Owner Author

Jelleas commented Jul 17, 2024

(declarative.repl()
        .run("from war import Card, Deck, DrawPile")
        .stdout("")
        .run("deck = Deck()")
        .run("assert deck.deal() == Card('A', '2')")
)()

Some initial work:

class repl:
    def __init__(self, fileName: Optional[str]=None):
        self._initialState: ReplState = ReplState(fileName=fileName)
        self._stack: List[Callable[["ReplState"], None]] = []
        self._description: Optional[str] = None

        init = self.do(lambda replState: ...)
        self._stack = init._stack
        self.__name__ = init.__name__
        self.__doc__ = init.__doc__

    def run(self, statement: str) -> Self:
        def run(state: ReplState) -> None:
            import checkpy.lib
            with checkpy.lib.captureStdout() as stdout:
                exec(statement, state.env)

            state.addStatement(statement, stdout.read())

        return self.do(run)

    def stdout(self, expected: str) -> Self:
        """Assert that the last statement printed expected."""
        def testStdout(state: ReplState):
            nonlocal expected
            expected = str(expected)
            actual = state._stdoutOutputs[-1] # TODO
            if expected != actual:
                msg = (
                    "Expected the following output:\n" +
                    expected +
                    "\nBut found:\n" +
                    actual +
                    "\n"
                )
                raise AssertionError(
                    msg +
                    state.replLog()
                )

        return self.do(testStdout)

    def do(self, function: Callable[["ReplState"], None]) -> Self:
        """
        Put function on the internal stack and call it after all previous calls have resolved.
        .do serves as an entry point for extensibility. Allowing you, the test writer, to insert
        specific and custom asserts, hints, and the like. For example:

        ```
        def checkDataFileIsUnchanged(state: "FunctionState"):
            with open("data.txt") as f:
                assert f.read() == "42\\n", "make sure not to change the file data.txt"
        
        testDataUnchanged = test()(function("process_data").call("data.txt").do(checkDataFileIsUnchanged))
        ```
        """
        self = deepcopy(self)
        self._stack.append(function)
        
        self.__name__ = f"declarative_repl_test_{uuid4()}"
        self.__doc__ = self._description if self._description is not None else self._initialState.description
        
        return self

    def __call__(self, test: Optional[checkpy.tests.Test]=None) -> "FunctionState":
        """Run the test."""
        if test is None:
            test = checkpy.tester.getActiveTest()

        initialDescription = ""
        if test is not None\
            and test.description != test.PLACEHOLDER_DESCRIPTION\
            and test.description != self._initialState.description:
            initialDescription = test.description

        stack = list(self._stack)
        state = deepcopy(self._initialState)

        for step in stack:
            step(state)

        if initialDescription:
            state.setDescriptionFormatter(lambda descr, state: descr)
            state.description = initialDescription
        elif state.wasCalled:
            state.description = f"{state.getFunctionCallRepr()} works as expected"
        else:
            state.description = f"{state.name} is correctly defined"

        return state
    
class ReplState:
    def __init__(self, fileName: Optional[str]=None):
        self._description: str = f"TODO"
        self._fileName = fileName
        self._isDescriptionMutable: bool = True
        self._statements: List[str] = []
        self._stdoutOutputs: List[str] = []
        self.env: dict = {}

    @staticmethod
    def _descriptionFormatter(descr: str, state: "ReplState") -> str:
        return f"testing" + (f" >> {descr}" if descr else "")

    @property
    def description(self) -> str:
        """The description of the test, what is ultimately shown on the screen."""
        return self._descriptionFormatter(self._description, self)

    @description.setter
    def description(self, newDescription: str):
        if not self.isDescriptionMutable:
            return
        self._description = newDescription

        test = checkpy.tester.getActiveTest()
        if test is None:
            raise checkpy.entities.exception.CheckpyError(
                message=f"Cannot change description while there is no test running."
            )
        test.description = self.description

    @property
    def fileName(self) -> Optional[str]:
        """
        The name of the Python file to run and import.
        If this is not set (`None`), the default file (`checkpy.file.name`) is used.
        """
        return self._fileName

    @fileName.setter
    def fileName(self, newFileName: Optional[str]):
        self._fileName = newFileName

    def addStatement(self, statement: str, stdout: str):
        self._statements.append(statement)
        self._stdoutOutputs.append(stdout)

    @property
    def isDescriptionMutable(self):
        """Can the description be changed (mutated)?"""
        return self._isDescriptionMutable

    @isDescriptionMutable.setter
    def isDescriptionMutable(self, newIsDescriptionMutable: bool):
        self._isDescriptionMutable = newIsDescriptionMutable

    def setDescriptionFormatter(self, formatter: Callable[[str, "ReplState"], str]):
        """
        The test's description is formatted by a function accepting the new description and the state.
        This method allows you to overwrite that function, for instance:

        `state.setDescriptionFormatter(lambda descr, state: f"Testing your function {state.name}: {descr}")`
        """
        self._descriptionFormatter = formatter # type:ignore [method-assign, assignment]
        
        test = checkpy.tester.getActiveTest()
        if test is None:
            raise checkpy.entities.exception.CheckpyError(
                message=f"Cannot change descriptionFormatter while there is no test running."
            )
        test.description = self.description

    def replLog(self):
        """
        Helper function that formats each line as if it were fed to Python's repl.
        """
        def fixLine(line: str) -> str:
            line = line.rstrip("\n")

            if line.startswith(" "):
                return "... " + line
            if not line.startswith(">>> "):
                return ">>> " + line
            return line

        # break-up multi-line statements
        actualLines = []
        for line in self._statements:
            actualLines.extend([l for l in line.split("\n") if l])

        # prepend >>> and ... (what you'd see in the REPL)
        # replace any "assert " statements with "True" on the next line
        fixedLines = [fixLine(l) for l in actualLines]

        pre = (
            'This check failed. Run the following code in the terminal to find out why:\n'
            '$ python3\n'
        )

        return pre + "\n".join(fixedLines)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant