add tests, update readme, requirements, move into subdir

This commit is contained in:
2025-08-30 11:58:19 +01:00
parent f512069fc7
commit 5349cd2fc9
5 changed files with 193 additions and 12 deletions

57
fifth/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Fifth
A simple stack-based language called Fifth
## Usage
```shell
python fifth.py
```
### Commands
- `PUSH <n>` - push integer onto stack
- `POP` - remove top element
- `SWAP` - swap top two elements
- `DUP` - duplicate top element
- `+`, `-`, `*`, `/` - arithmetic operations
- `EXIT` - quit
### Example
```
stack is []
PUSH 3
stack is [3]
PUSH 11
stack is [3, 11]
+
stack is [14]
DUP
stack is [14, 14]
PUSH 2
stack is [14, 14, 2]
*
stack is [14, 28]
SWAP
stack is [28, 14]
/
stack is [2]
+
ERROR: two numbers required
POP
stack is []
EXIT
```
## Tests
```bash
pip install pytest
pytest -v test_fifth.py
```
## Requirements
- Python 3.10+
- pytest

96
fifth/fifth.py Normal file
View File

@@ -0,0 +1,96 @@
import operator
import readline
from typing import Callable
class FifthStack:
def __init__(self):
self.stack: list[int] = []
self.commands: dict[str, Callable] = {
"push": self.push,
"pop": self.pop,
"swap": self.swap,
"dup": self.dup,
}
self.binary_ops: dict[str, Callable[[int, int], int]] = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.floordiv,
}
def _require(self, number, message):
if len(self.stack) < number:
print(f"ERROR: {message}")
return False
return True
def _get_binary_op(self, command):
return self.binary_ops.get(command)
def _binary_op(self, operator):
if not self._require(2, "two numbers required"):
return
b, a = self.stack.pop(), self.stack.pop()
try:
self.stack.append(operator(a, b))
except ZeroDivisionError as e:
print(f"ERROR: division by zero")
self.stack.extend([a, b])
def push(self, value):
try:
self.stack.append(int(value))
except ValueError:
print("ERROR: integer required")
def pop(self):
if not self.stack:
print("ERROR: stack empty")
return
self.stack.pop()
def swap(self):
if not self._require(2, "two numbers required"):
return
self.stack[-1], self.stack[-2] = self.stack[-2], self.stack[-1]
def dup(self):
if self._require(1, "stack empty"):
self.stack.append(self.stack[-1])
def execute(self, command: str):
tokens = command.lower().strip().split()
if not tokens:
return
command, argument = tokens[0], tokens[1] if len(tokens) > 1 else None
function = self.commands.get(command)
if function:
if command == "push":
function(argument)
else:
function()
elif command in self.binary_ops:
self._binary_op(self._get_binary_op(command))
else:
print("ERROR: unknown command")
def main():
fifth = FifthStack()
while True:
print(f"stack is {fifth.stack}")
try:
if (command := input().strip().lower()) == "exit":
break
if command:
fifth.execute(command)
except (EOFError, KeyboardInterrupt):
break
if __name__ == "__main__":
main()

1
fifth/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pytest==8.4.1

166
fifth/test_fifth.py Normal file
View File

@@ -0,0 +1,166 @@
import pytest
from fifth import FifthStack
# Test the functions
def test_push_and_pop():
stack = FifthStack()
stack.push(10)
stack.push(20)
assert stack.stack == [10, 20]
stack.pop()
assert stack.stack == [10]
stack.pop()
assert stack.stack == []
def test_push_invalid_value(capsys):
stack = FifthStack()
stack.push("abc")
captured = capsys.readouterr()
assert "ERROR: integer required" in captured.out
assert stack.stack == []
def test_pop_empty_stack(capsys):
stack = FifthStack()
stack.pop()
captured = capsys.readouterr()
assert "ERROR: stack empty" in captured.out
def test_swap():
stack = FifthStack()
stack.push(1)
stack.push(2)
stack.swap()
assert stack.stack == [2, 1]
def test_swap_insufficient_elements(capsys):
stack = FifthStack()
stack.push(1)
stack.swap()
captured = capsys.readouterr()
assert "ERROR: two numbers required" in captured.out
assert stack.stack == [1]
def test_dup():
stack = FifthStack()
stack.push(5)
stack.dup()
assert stack.stack == [5, 5]
def test_dup_empty_stack(capsys):
stack = FifthStack()
stack.dup()
captured = capsys.readouterr()
assert "ERROR: stack empty" in captured.out
def test_addition():
stack = FifthStack()
stack.push(2)
stack.push(3)
stack.execute("+")
assert stack.stack == [5]
def test_subtraction():
stack = FifthStack()
stack.push(5)
stack.push(2)
stack.execute("-")
assert stack.stack == [3]
def test_multiplication():
stack = FifthStack()
stack.push(4)
stack.push(3)
stack.execute("*")
assert stack.stack == [12]
def test_division():
stack = FifthStack()
stack.push(8)
stack.push(2)
stack.execute("/")
assert stack.stack == [4]
def test_division_by_zero(capsys):
stack = FifthStack()
stack.push(8)
stack.push(0)
stack.execute("/")
captured = capsys.readouterr()
assert "ERROR: division by zero" in captured.out
assert stack.stack == [8, 0]
def test_binary_op_insufficient_elements(capsys):
stack = FifthStack()
stack.push(1)
stack.execute("+")
captured = capsys.readouterr()
assert "ERROR: two numbers required" in captured.out
assert stack.stack == [1]
# Test executions
def test_execute_unknown_command(capsys):
stack = FifthStack()
stack.execute("foobar")
captured = capsys.readouterr()
assert "ERROR: unknown command" in captured.out
def test_execute_push_command():
stack = FifthStack()
stack.execute("push 42")
assert stack.stack == [42]
def test_execute_pop_command():
stack = FifthStack()
stack.push(99)
stack.execute("pop")
assert stack.stack == []
def test_execute_swap_command():
stack = FifthStack()
stack.push(1)
stack.push(2)
stack.execute("swap")
assert stack.stack == [2, 1]
def test_execute_dup_command():
stack = FifthStack()
stack.push(7)
stack.execute("dup")
assert stack.stack == [7, 7]
# Test edge cases
def test_execute_empty_command():
stack = FifthStack()
stack.execute("") # Should do nothing
assert stack.stack == []
def test_execute_whitespace_command():
stack = FifthStack()
stack.execute(" ") # Should do nothing
assert stack.stack == []
def test_case_insensitivity():
stack = FifthStack()
stack.execute("PUSH 42")
assert stack.stack == [42]