mirror of
https://github.com/arendst/Tasmota.git
synced 2026-06-11 12:55:33 -04:00
600 lines
22 KiB
Python
600 lines
22 KiB
Python
"""
|
|
Tests for berry_port CLI (__main__.py) and REPL mode.
|
|
|
|
Uses subprocess.run for CLI tests and mock stdin for REPL tests.
|
|
Validates Requirements: 20.4, 25.1, 25.4
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import tempfile
|
|
import pytest
|
|
|
|
BERRY_CMD = [sys.executable, "-m", "berry_port"]
|
|
|
|
|
|
def run_berry(*args, input_text=None, timeout=15):
|
|
"""Run berry_port CLI with given args, return CompletedProcess."""
|
|
return subprocess.run(
|
|
[*BERRY_CMD, *args],
|
|
capture_output=True,
|
|
text=True,
|
|
input=input_text,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
# ── Version and Help ────────────────────────────────────────────────────
|
|
|
|
class TestVersionAndHelp:
|
|
def test_version_flag(self):
|
|
r = run_berry("-v")
|
|
assert r.returncode == 0
|
|
assert "Berry" in r.stdout
|
|
assert "1.1.0" in r.stdout
|
|
|
|
def test_help_flag(self):
|
|
r = run_berry("-h")
|
|
assert r.returncode == 0
|
|
assert "Usage:" in r.stdout
|
|
assert "-i" in r.stdout
|
|
assert "-e" in r.stdout
|
|
assert "-c" in r.stdout
|
|
assert "-o" in r.stdout
|
|
assert "-g" in r.stdout
|
|
assert "-s" in r.stdout
|
|
assert "-m" in r.stdout
|
|
assert "-v" in r.stdout
|
|
assert "-h" in r.stdout
|
|
assert "-l" in r.stdout
|
|
|
|
|
|
# ── Execute string (-e) ────────────────────────────────────────────────
|
|
|
|
class TestExecuteString:
|
|
def test_e_print_hello(self):
|
|
r = run_berry("-e", "print('hello')")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "hello"
|
|
|
|
def test_e_arithmetic(self):
|
|
r = run_berry("-e", "print(1+2)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "3"
|
|
|
|
def test_e_syntax_error(self):
|
|
r = run_berry("-e", "print(")
|
|
assert r.returncode != 0
|
|
|
|
def test_e_multistatement(self):
|
|
r = run_berry("-e", "var x = 42; print(x)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "42"
|
|
|
|
def test_e_function(self):
|
|
r = run_berry("-e",
|
|
"def fib(n) if n<=1 return n end return fib(n-1)+fib(n-2) end print(fib(10))")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "55"
|
|
|
|
def test_e_print_module(self):
|
|
"""Printing a module should output '<module: NAME>' without crashing."""
|
|
r = run_berry("-e", "import debug print(debug)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "<module: debug>"
|
|
|
|
def test_e_print_multiple_modules(self):
|
|
"""Printing various modules should all work (const, native, etc.)."""
|
|
r = run_berry("-e", "import math print(math)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "<module: math>"
|
|
|
|
def test_e_attrdump_module(self):
|
|
"""debug.attrdump(debug) should list module attributes without crashing."""
|
|
r = run_berry("-e", "import debug debug.attrdump(debug)")
|
|
assert r.returncode == 0
|
|
assert "value type <module>" in r.stdout
|
|
assert "attrdump: <function>" in r.stdout
|
|
|
|
def test_e_solidify_dump(self):
|
|
"""solidify.dump(f) should output C code without crashing."""
|
|
r = run_berry("-e",
|
|
"import solidify def f() return 42 end solidify.dump(f)")
|
|
assert r.returncode == 0
|
|
assert "be_local_closure" in r.stdout
|
|
assert "LDINT" in r.stdout
|
|
assert "RET" in r.stdout
|
|
|
|
|
|
# ── Script file execution ───────────────────────────────────────────────
|
|
|
|
class TestScriptFile:
|
|
def test_run_script_file(self):
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("print('from file')\n")
|
|
f.flush()
|
|
try:
|
|
r = run_berry(f.name)
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "from file"
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
def test_script_file_not_found(self):
|
|
r = run_berry("/tmp/nonexistent_berry_script_12345.be")
|
|
assert r.returncode != 0
|
|
|
|
def test_script_with_args(self):
|
|
"""Verify _argv contains positional args after the script."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("print(_argv)\n")
|
|
f.flush()
|
|
try:
|
|
r = run_berry(f.name, "foo", "bar")
|
|
assert r.returncode == 0
|
|
assert "foo" in r.stdout
|
|
assert "bar" in r.stdout
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
|
|
# ── _argv global ────────────────────────────────────────────────────────
|
|
|
|
class TestArgv:
|
|
def test_argv_empty_with_e(self):
|
|
r = run_berry("-e", "print(_argv)")
|
|
assert r.returncode == 0
|
|
assert "[]" in r.stdout
|
|
|
|
def test_argv_with_positional_args(self):
|
|
r = run_berry("-e", "print(_argv)", "a", "b", "c")
|
|
# After -e, remaining args are positional — they go into _argv
|
|
# but also the first one is treated as a script file.
|
|
# The _argv should contain ['a', 'b', 'c']
|
|
assert "a" in r.stdout
|
|
assert "b" in r.stdout
|
|
assert "c" in r.stdout
|
|
|
|
|
|
# ── Compiler flags (-g, -s, -l) ────────────────────────────────────────
|
|
|
|
class TestCompilerFlags:
|
|
def test_strict_mode_undeclared_var(self):
|
|
"""In strict mode, reading an undeclared variable inside a function should error."""
|
|
r = run_berry("-s", "-e", "def f() return x end f()")
|
|
assert r.returncode != 0
|
|
|
|
def test_named_globals_basic(self):
|
|
"""Named globals mode: var + print should work."""
|
|
r = run_berry("-g", "-e", "var a = 42 print(a)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "42"
|
|
|
|
def test_named_globals_with_strict(self):
|
|
"""Named globals + strict mode together."""
|
|
r = run_berry("-g", "-s", "-e", "var a = 42 print(a)")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "42"
|
|
|
|
def test_named_globals_function(self):
|
|
"""Named globals mode with function definition and call."""
|
|
r = run_berry("-g", "-e",
|
|
"def add(a, b) return a + b end print(add(3, 4))")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "7"
|
|
|
|
def test_local_mode(self):
|
|
"""With -l, variables in script are parsed as local."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("x = 42\nprint(x)\n")
|
|
f.flush()
|
|
try:
|
|
r = run_berry("-l", f.name)
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "42"
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
|
|
# ── Bytecode compilation (-c, -o) ──────────────────────────────────────
|
|
|
|
class TestBytecodeCompilation:
|
|
def test_compile_to_default_output(self):
|
|
"""Test -c compiles to a.out by default (in CWD)."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("print('compiled')\n")
|
|
f.flush()
|
|
outfile = "a.out"
|
|
try:
|
|
# Clean up any previous output
|
|
if os.path.exists(outfile):
|
|
os.unlink(outfile)
|
|
r = run_berry("-c", f.name)
|
|
assert r.returncode == 0
|
|
assert os.path.exists(outfile)
|
|
# Check magic bytes
|
|
with open(outfile, "rb") as bf:
|
|
magic = bf.read(3)
|
|
assert magic == b'\xBE\xCD\xFE'
|
|
finally:
|
|
os.unlink(f.name)
|
|
if os.path.exists(outfile):
|
|
os.unlink(outfile)
|
|
|
|
def test_compile_with_output_name(self):
|
|
"""Test -c with -o writes to specified file."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("print('compiled')\n")
|
|
f.flush()
|
|
outfile = os.path.join(tempfile.gettempdir(), "test_output.bec")
|
|
try:
|
|
if os.path.exists(outfile):
|
|
os.unlink(outfile)
|
|
r = run_berry("-c", f.name, "-o", outfile)
|
|
assert r.returncode == 0
|
|
assert os.path.exists(outfile)
|
|
with open(outfile, "rb") as bf:
|
|
magic = bf.read(3)
|
|
assert magic == b'\xBE\xCD\xFE'
|
|
finally:
|
|
os.unlink(f.name)
|
|
if os.path.exists(outfile):
|
|
os.unlink(outfile)
|
|
|
|
|
|
# ── Module path (-m) ───────────────────────────────────────────────────
|
|
|
|
class TestModulePath:
|
|
def test_module_path_set(self):
|
|
"""Test -m sets custom module search path."""
|
|
mod_dir = tempfile.mkdtemp()
|
|
# Create a simple Berry module file
|
|
mod_file = os.path.join(mod_dir, "testmod.be")
|
|
with open(mod_file, "w") as f:
|
|
f.write("# simple module\n")
|
|
try:
|
|
# Just verify -m doesn't crash — actual module loading
|
|
# depends on the module system wiring
|
|
r = run_berry("-m", mod_dir, "-e", "print('ok')")
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "ok"
|
|
finally:
|
|
os.unlink(mod_file)
|
|
os.rmdir(mod_dir)
|
|
|
|
|
|
# ── Default module path (no -m) ────────────────────────────────────────
|
|
|
|
# Regression tests for the default module search path that the CLI entry
|
|
# point installs when -m is NOT provided. Mirrors berry_paths() in
|
|
# default/berry.c which adds BERRY_ROOT "/lib/berry/packages" on Unix or
|
|
# BERRY_ROOT "\\berry\\packages" on Windows.
|
|
|
|
_DEFAULT_PATH = (
|
|
"\\Windows\\system32\\berry\\packages"
|
|
if sys.platform == "win32"
|
|
else "/usr/local/lib/berry/packages"
|
|
)
|
|
|
|
C_BERRY = os.path.join(
|
|
os.path.dirname(__file__), "..", "..", "berry"
|
|
)
|
|
C_BERRY_AVAILABLE = os.path.isfile(C_BERRY) and os.access(C_BERRY, os.X_OK)
|
|
|
|
|
|
class TestDefaultModulePath:
|
|
"""Without -m, the CLI must install the default search path, matching
|
|
the C implementation's berry_paths() behavior."""
|
|
|
|
def test_default_path_is_non_empty(self):
|
|
"""sys.path() returns a non-empty list when -m is not provided."""
|
|
r = run_berry("-e", "import sys print(sys.path().size())")
|
|
assert r.returncode == 0
|
|
assert int(r.stdout.strip()) >= 1
|
|
|
|
def test_default_path_contains_expected_entry(self):
|
|
"""Default sys.path() contains the platform-specific BERRY_ROOT entry."""
|
|
r = run_berry("-e", "import sys print(sys.path())")
|
|
assert r.returncode == 0
|
|
assert _DEFAULT_PATH in r.stdout
|
|
|
|
def test_default_path_with_s_and_g_flags(self):
|
|
"""Default path is installed regardless of -s / -g compiler flags.
|
|
Regression: tests/module.be invoked as `-s -g` failed because the
|
|
Python port skipped berry_paths() entirely."""
|
|
r = run_berry("-s", "-g", "-e", "import sys print(sys.path().size())")
|
|
assert r.returncode == 0
|
|
assert int(r.stdout.strip()) >= 1
|
|
|
|
def test_custom_path_replaces_default(self):
|
|
"""With -m, only the custom path(s) are installed — the default
|
|
BERRY_ROOT entry is NOT added (matches C berry_paths() being in the
|
|
else branch)."""
|
|
with tempfile.TemporaryDirectory() as d:
|
|
r = run_berry("-m", d, "-e", "import sys print(sys.path())")
|
|
assert r.returncode == 0
|
|
assert d in r.stdout
|
|
assert _DEFAULT_PATH not in r.stdout
|
|
|
|
def test_custom_path_multiple_entries(self):
|
|
"""-m accepts PATH_SEPARATOR-separated entries, all added in order."""
|
|
sep = ";" if sys.platform == "win32" else ":"
|
|
with tempfile.TemporaryDirectory() as d1, tempfile.TemporaryDirectory() as d2:
|
|
r = run_berry(
|
|
"-m", f"{d1}{sep}{d2}",
|
|
"-e", "import sys for p : sys.path() print(p) end",
|
|
)
|
|
assert r.returncode == 0
|
|
lines = [ln.strip() for ln in r.stdout.strip().splitlines() if ln.strip()]
|
|
assert d1 in lines
|
|
assert d2 in lines
|
|
|
|
def test_default_path_entries_are_strings(self):
|
|
"""Every entry in the default sys.path() is a string."""
|
|
r = run_berry(
|
|
"-e",
|
|
"import sys for p : sys.path() assert(type(p) == 'string') end print('ok')",
|
|
)
|
|
assert r.returncode == 0
|
|
assert r.stdout.strip() == "ok"
|
|
|
|
def test_module_be_regression(self):
|
|
"""Regression: tests/module.be runs to completion under -s -g.
|
|
This was the original failure: line 78 `assert(p.size() >= 1)`
|
|
failed because berry_paths() was never called."""
|
|
module_test = os.path.join(
|
|
os.path.dirname(__file__), "..", "module.be"
|
|
)
|
|
if not os.path.isfile(module_test):
|
|
pytest.skip("tests/module.be not found")
|
|
r = run_berry("-s", "-g", module_test)
|
|
assert r.returncode == 0, (
|
|
f"tests/module.be failed:\n{r.stdout}\n{r.stderr}"
|
|
)
|
|
|
|
@pytest.mark.skipif(
|
|
not C_BERRY_AVAILABLE,
|
|
reason="C berry binary not available for cross-validation",
|
|
)
|
|
def test_default_path_matches_c_implementation(self):
|
|
"""Cross-validation: default sys.path() in Python port matches the
|
|
compiled C berry binary."""
|
|
code = "import sys for p : sys.path() print(p) end"
|
|
r_py = run_berry("-e", code)
|
|
r_c = subprocess.run(
|
|
[C_BERRY, "-e", code], capture_output=True, text=True, timeout=15
|
|
)
|
|
assert r_py.returncode == 0
|
|
assert r_c.returncode == 0
|
|
assert r_py.stdout == r_c.stdout
|
|
|
|
|
|
# ── berry_paths / berry_custom_paths unit tests ───────────────────────
|
|
|
|
class TestBerryPathsUnit:
|
|
"""Unit tests for the path-installing helpers in berry_port.__main__."""
|
|
|
|
@staticmethod
|
|
def _collect_path_entries(vm):
|
|
"""Call be_module_path(vm), read the list from the top of stack,
|
|
return Python-str entries, and pop."""
|
|
from berry_port.be_module import be_module_path
|
|
from berry_port.be_string import be_str2cstr
|
|
|
|
be_module_path(vm)
|
|
lst = vm.stack[vm.top - 1].v
|
|
entries = [be_str2cstr(lst.data[i].v) for i in range(lst.count)]
|
|
vm.top -= 1
|
|
return entries
|
|
|
|
def test_berry_paths_installs_default(self):
|
|
from berry_port.berry import be_vm_new, be_vm_delete
|
|
from berry_port.__main__ import berry_paths, _MODULE_PATHS
|
|
|
|
vm = be_vm_new()
|
|
try:
|
|
berry_paths(vm)
|
|
entries = self._collect_path_entries(vm)
|
|
for expected in _MODULE_PATHS:
|
|
assert expected in entries
|
|
finally:
|
|
be_vm_delete(vm)
|
|
|
|
def test_berry_custom_paths_splits_on_separator(self):
|
|
from berry_port.berry import be_vm_new, be_vm_delete
|
|
from berry_port.__main__ import berry_custom_paths, PATH_SEPARATOR
|
|
|
|
vm = be_vm_new()
|
|
try:
|
|
berry_custom_paths(vm, f"/a{PATH_SEPARATOR}/b{PATH_SEPARATOR}/c")
|
|
entries = self._collect_path_entries(vm)
|
|
assert "/a" in entries
|
|
assert "/b" in entries
|
|
assert "/c" in entries
|
|
finally:
|
|
be_vm_delete(vm)
|
|
|
|
def test_berry_custom_paths_skips_empty_segments(self):
|
|
"""Empty segments (from leading/trailing/double separators) are skipped,
|
|
matching C strtok() behavior."""
|
|
from berry_port.berry import be_vm_new, be_vm_delete
|
|
from berry_port.__main__ import berry_custom_paths, PATH_SEPARATOR
|
|
|
|
vm = be_vm_new()
|
|
try:
|
|
berry_custom_paths(
|
|
vm,
|
|
f"{PATH_SEPARATOR}/x{PATH_SEPARATOR}{PATH_SEPARATOR}/y{PATH_SEPARATOR}",
|
|
)
|
|
entries = self._collect_path_entries(vm)
|
|
assert "" not in entries
|
|
assert "/x" in entries
|
|
assert "/y" in entries
|
|
finally:
|
|
be_vm_delete(vm)
|
|
|
|
|
|
# ── REPL mode ──────────────────────────────────────────────────────────
|
|
|
|
class TestREPL:
|
|
def test_repl_expression_evaluation(self):
|
|
"""REPL evaluates expressions and prints non-nil results."""
|
|
r = run_berry(input_text="1 + 2\n")
|
|
assert r.returncode == 0
|
|
assert "3" in r.stdout
|
|
|
|
def test_repl_print(self):
|
|
"""REPL print statement."""
|
|
r = run_berry(input_text="print('repl hello')\n")
|
|
assert r.returncode == 0
|
|
assert "repl hello" in r.stdout
|
|
|
|
def test_repl_empty_lines(self):
|
|
"""REPL handles empty/blank lines without error."""
|
|
r = run_berry(input_text="\n\n\n")
|
|
assert r.returncode == 0
|
|
|
|
def test_repl_eof_exits_cleanly(self):
|
|
"""REPL exits cleanly on EOF (empty input)."""
|
|
r = run_berry(input_text="")
|
|
assert r.returncode == 0
|
|
|
|
def test_repl_syntax_error_continues(self):
|
|
"""REPL prints error on syntax error and continues."""
|
|
# Use '1 +' which is incomplete but NOT ending with 'EOS' in the
|
|
# try_return path, then use a clear error like '0/0' division
|
|
r = run_berry(input_text="1 @ 2\nprint('after error')\n")
|
|
assert r.returncode == 0
|
|
assert "after error" in r.stdout
|
|
|
|
def test_repl_multiline_function(self):
|
|
"""REPL handles multi-line function definition."""
|
|
code = "def f()\nreturn 42\nend\nprint(f())\n"
|
|
r = run_berry(input_text=code)
|
|
assert r.returncode == 0
|
|
assert "42" in r.stdout
|
|
|
|
def test_repl_var_assignment_and_use(self):
|
|
"""REPL can assign and use variables across lines."""
|
|
code = "var x = 10\nprint(x * 3)\n"
|
|
r = run_berry(input_text=code)
|
|
assert r.returncode == 0
|
|
assert "30" in r.stdout
|
|
|
|
|
|
# ── Interactive mode after script (-i) ─────────────────────────────────
|
|
|
|
class TestInteractiveAfterScript:
|
|
def test_i_flag_runs_script_then_repl(self):
|
|
"""With -i, script runs first, then REPL starts (EOF exits)."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".be", delete=False) as f:
|
|
f.write("print('script ran')\n")
|
|
f.flush()
|
|
try:
|
|
r = run_berry("-i", f.name, input_text="")
|
|
assert r.returncode == 0
|
|
assert "script ran" in r.stdout
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
|
|
# ── Option parsing edge cases ──────────────────────────────────────────
|
|
|
|
class TestOptionParsing:
|
|
def test_unknown_option(self):
|
|
"""Unknown option produces error."""
|
|
r = run_berry("-z")
|
|
assert r.returncode != 0
|
|
|
|
def test_combined_v_and_h(self):
|
|
"""Both -v and -h can be used together."""
|
|
r = run_berry("-v", "-h")
|
|
assert r.returncode == 0
|
|
assert "Berry" in r.stdout
|
|
assert "Usage:" in r.stdout
|
|
|
|
def test_option_parsing_unit_tests(self):
|
|
"""Unit test the option parser directly."""
|
|
from berry_port.__main__ import (
|
|
arg_opts, parse_arg, match_opt, is_letter,
|
|
arg_i, arg_c, arg_o, arg_l, arg_h, arg_v,
|
|
arg_e, arg_g, arg_s, arg_err, arg_m,
|
|
)
|
|
|
|
# -v
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
assert parse_arg(opt, ["berry", "-v"]) == arg_v
|
|
|
|
# -h
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
assert parse_arg(opt, ["berry", "-h"]) == arg_h
|
|
|
|
# -i -l
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
args = parse_arg(opt, ["berry", "-i", "-l", "script.be"])
|
|
assert args == (arg_i | arg_l)
|
|
assert opt.idx == 3
|
|
assert ["berry", "-i", "-l", "script.be"][opt.idx:] == ["script.be"]
|
|
|
|
# -e with argument
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
args = parse_arg(opt, ["berry", "-e", "print(1)"])
|
|
assert args == arg_e
|
|
assert opt.execute == "print(1)"
|
|
|
|
# -c with -o
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
args = parse_arg(opt, ["berry", "-c", "s.be", "-o", "out.bec"])
|
|
assert args == (arg_c | arg_o)
|
|
assert opt.src == "s.be"
|
|
assert opt.dst == "out.bec"
|
|
|
|
# -g -s
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
assert parse_arg(opt, ["berry", "-g", "-s"]) == (arg_g | arg_s)
|
|
|
|
# -m with path
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
args = parse_arg(opt, ["berry", "-m", "/path", "script.be"])
|
|
assert args == arg_m
|
|
assert opt.modulepath == "/path"
|
|
assert ["berry", "-m", "/path", "script.be"][opt.idx:] == ["script.be"]
|
|
|
|
# unknown option
|
|
opt = arg_opts()
|
|
opt.pattern = "m?vhile?gsc?o?"
|
|
assert parse_arg(opt, ["berry", "-z"]) & arg_err
|
|
|
|
def test_match_opt(self):
|
|
"""Unit test match_opt."""
|
|
from berry_port.__main__ import match_opt
|
|
# 'v' is in pattern
|
|
assert match_opt("m?vhile?gsc?o?", "v") is not None
|
|
# 'e' has '?' after it
|
|
res = match_opt("m?vhile?gsc?o?", "e")
|
|
assert res is not None
|
|
assert res[1] is True # has_arg
|
|
# 'i' has no '?' after it
|
|
res = match_opt("m?vhile?gsc?o?", "i")
|
|
assert res is not None
|
|
assert res[1] is False # no arg
|
|
# 'z' not in pattern
|
|
assert match_opt("m?vhile?gsc?o?", "z") is None
|
|
|
|
def test_is_letter(self):
|
|
"""Unit test is_letter."""
|
|
from berry_port.__main__ import is_letter
|
|
assert is_letter("a") is True
|
|
assert is_letter("Z") is True
|
|
assert is_letter("?") is False
|
|
assert is_letter("1") is False
|
|
assert is_letter("") is False
|