Skip to content

Commit 26f185b

Browse files
author
Mateusz Bronisław Wasilewski
committed
Merge branch 'hypothesis_tests' into 'main'
Hypothesis tests See merge request mwasilew/2023-ZPRP!18
2 parents e8df6df + 72491e0 commit 26f185b

File tree

8 files changed

+367
-28
lines changed

8 files changed

+367
-28
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ venv/
33
__pycache__/
44
.vscode/*
55
.pytest_cache/
6-
.tox/
6+
.tox/
7+
.hypothesis/

Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ venv/bin/activate: requirements.txt
1515
. ./venv/bin/activate
1616
$(PIP) install -r requirements.txt
1717

18-
setup: venv/bin/activate
18+
setup_venv: venv/bin/activate
19+
20+
setup:
21+
$(PIP) install -r requirements.txt
1922

2023
clean:
2124
rm -rf __pycache__
2225
rm -rf venv
2326
rm -rf .tox
27+
rm -rf .hypothesis
2428

2529
.PHONY: tests clean pre_commit

image_formatter/error_handler/errors.py

+17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from image_formatter.lexer.token import TokenType
2+
from typing import List
23

34

45
class UnexpectedTagException(Exception):
@@ -15,3 +16,19 @@ def __eq__(self, other):
1516
return False
1617

1718
return (self.expected == other.expected) and (self.actual == other.actual)
19+
20+
21+
class InvalidConfigCharacterError(Exception):
22+
def __init__(self, invalid_char: str, valid_chars: List[str]):
23+
super().__init__()
24+
self.invalid_char = invalid_char
25+
self.valid_chars = valid_chars
26+
27+
def __str__(self):
28+
return f"{self.__class__}: invalid character found: {self.invalid_char} when list of valid chars is: {self.valid_chars}"
29+
30+
def __eq__(self, other):
31+
if other.__class__ != self.__class__:
32+
return False
33+
34+
return (set(self.valid_chars == other.valid_chars)) and set((self.invalid_char == other.invalid_char))

image_formatter/image_properties_tag_replacer/image_properties_tag_replacer.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ def __init__(self, lex: Lexer, image_tags_properties: dict, error_handler: Error
2727
self.image_tags_properties = image_tags_properties
2828
self.error_handler = error_handler
2929

30-
@staticmethod
31-
def name() -> str:
32-
return __class__.__name__
30+
@classmethod
31+
def name(cls) -> str:
32+
return cls.__name__
3333

3434
def next_token(self):
3535
self.curr_token = self.lexer.get_token()

image_formatter/lexer/lexer.py

+103-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from image_formatter.lexer.token import Token, TokenType, IntegerToken
22
from image_formatter.lexer.position import Position
3+
from image_formatter.error_handler.errors import InvalidConfigCharacterError
34
import io
45
import sys
56
from mkdocs.plugins import get_plugin_logger
67
from copy import deepcopy
8+
from typing import Tuple, List
79

810
log = get_plugin_logger(__name__)
911

@@ -21,10 +23,10 @@ def __init__(
2123
fp: io.TextIOWrapper,
2224
*,
2325
max_int: int = sys.maxsize,
24-
special_signs: tuple = ("-", "_"),
26+
special_signs: Tuple[str] = ("-", "_"),
2527
tag: str = "@",
26-
newline_characters: tuple = ("\n", "\r"),
27-
additional_path_signs: tuple = ("/", "."),
28+
newline_characters: Tuple[str] = ("\n", "\r"),
29+
additional_path_signs: Tuple[str] = ("/", "."),
2830
):
2931
"""
3032
Args:
@@ -40,6 +42,7 @@ def __init__(
4042
Attributes:
4143
running: defines if lexer should still go through the characters or EOF was encountered
4244
"""
45+
Lexer.verify_config(special_signs, tag, newline_characters, additional_path_signs)
4346
self.fp = fp
4447
self.running = True
4548
self.current_char = ""
@@ -50,9 +53,104 @@ def __init__(
5053
self.newline_characters = newline_characters # @TODO add hypothesis tests
5154
self.additional_path_signs = additional_path_signs
5255

56+
@classmethod
57+
def name(cls) -> str:
58+
return cls.__name__
59+
60+
@classmethod
61+
def verify_config(
62+
cls,
63+
special_signs: Tuple[str],
64+
tag: str,
65+
newline_characters: Tuple[str],
66+
additional_path_signs: Tuple[str],
67+
) -> bool:
68+
"""
69+
Verifies if provided to Lexer configuration is valid. Upon failure on any of the verification steps, the function returns immediately with fail reason.
70+
71+
Returns:
72+
True when configuration is valid
73+
Raises:
74+
InvalidConfigCharacterError: when invalid character is found
75+
Exception: when there is different reason of validation fail
76+
"""
77+
# configurations must be mutually exclusive
78+
flat_list = [*set(special_signs), tag, *set(newline_characters), *set(additional_path_signs)]
79+
if len(flat_list) != len(set(flat_list)):
80+
raise Exception("Characters cannot repeat across configuration options")
81+
82+
Lexer.verify_special_signs(special_signs)
83+
Lexer.verify_tag(tag)
84+
Lexer.verify_newline_characters(newline_characters)
85+
Lexer.verify_additional_path_signs(additional_path_signs)
86+
5387
@staticmethod
54-
def name() -> str:
55-
return __class__.__name__
88+
def find_invalid_char(valid_chars: List[str], check_chars: Tuple[str]) -> str:
89+
invalid_char = next(filter(lambda x: x not in valid_chars, check_chars), None)
90+
return invalid_char
91+
92+
@classmethod
93+
def verify_special_signs(cls, signs: Tuple[str]) -> bool:
94+
"""
95+
Verifies if all characters in the list are valid special signs characters
96+
97+
Returns:
98+
True when configuration is valid
99+
Raises:
100+
InvalidConfigCharacterError: when invalid character is found
101+
"""
102+
invalid_chars = [" ", "(", ")"]
103+
if any([sign in invalid_chars for sign in signs]):
104+
raise InvalidConfigCharacterError("<space>", [])
105+
return True
106+
107+
@classmethod
108+
def verify_tag(cls, tag: str) -> bool:
109+
"""
110+
Verifies if tag is valid
111+
112+
Returns:
113+
True when tag is valid
114+
Raises:
115+
InvalidConfigCharacterError: when invalid character is found
116+
"""
117+
valid_tags = "@#$%&~>?+=:"
118+
invalid_char = Lexer.find_invalid_char(valid_tags, (tag))
119+
if invalid_char:
120+
raise InvalidConfigCharacterError(invalid_char, valid_tags)
121+
return True
122+
123+
@classmethod
124+
def verify_newline_characters(cls, chars: Tuple[str]) -> bool:
125+
"""
126+
Verifies if all characters in the list are valid new line characters
127+
128+
Returns:
129+
True when configuration is valid
130+
Raises:
131+
InvalidConfigCharacterError: when invalid character is found
132+
"""
133+
valid_newline_chars = ["\n", "\r", "\r\n", "\x0b", "\v", "\f"]
134+
invalid_char = Lexer.find_invalid_char(valid_newline_chars, chars)
135+
if invalid_char:
136+
raise InvalidConfigCharacterError(invalid_char, valid_newline_chars)
137+
return True
138+
139+
@classmethod
140+
def verify_additional_path_signs(cls, signs: Tuple[str]) -> bool:
141+
"""
142+
Verifies if all characters in the list are valid additional path signs
143+
144+
Returns:
145+
True when configuration is valid
146+
Raises:
147+
InvalidConfigCharacterError: when invalid character is found
148+
"""
149+
valid_additional_path_signs = "-_.~:/?#[]@!$&'()*+,;=%"
150+
invalid_char = Lexer.find_invalid_char(valid_additional_path_signs, signs)
151+
if invalid_char:
152+
raise InvalidConfigCharacterError(invalid_char, valid_additional_path_signs)
153+
return True
56154

57155
def is_character(self) -> bool:
58156
"""

requirements.txt

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
attrs==23.1.0
2+
Babel==2.13.1
13
black==23.10.1
24
cachetools==5.3.2
5+
certifi==2023.11.17
36
chardet==5.2.0
7+
charset-normalizer==3.3.2
48
click==8.1.7
59
colorama==0.4.6
6-
flake8==6.1.0
7-
ghp-import==2.1.0
810
cssutils==2.9.0
911
distlib==0.3.7
1012
exceptiongroup==1.1.3
1113
filelock==3.13.1
14+
flake8==6.1.0
15+
ghp-import==2.1.0
16+
griffe==0.38.0
17+
hypothesis==6.89.0
18+
idna==3.4
1219
importlib-metadata==6.8.0
1320
iniconfig==2.0.0
1421
Jinja2==3.1.2
@@ -17,22 +24,37 @@ MarkupSafe==2.1.3
1724
mccabe==0.7.0
1825
mergedeep==1.3.4
1926
mkdocs==1.5.3
27+
mkdocs-autorefs==0.5.0
2028
mkdocs-material==9.4.9
29+
mkdocs-material-extensions==1.3
2130
mkdocstrings==0.24.0
2231
mkdocstrings-python==1.7.4
2332
mock==5.1.0
2433
mypy-extensions==1.0.0
2534
packaging==23.2
35+
paginate==0.5.6
2636
pathspec==0.11.2
2737
platformdirs==3.11.0
2838
pluggy==1.3.0
2939
pycodestyle==2.11.1
3040
pyflakes==3.1.0
41+
Pygments==2.16.1
42+
pymdown-extensions==10.4
43+
pyproject-api==1.6.1
3144
pytest==7.4.3
32-
setuptools~=65.5.1
3345
python-dateutil==2.8.2
46+
pytz==2023.3.post1
3447
PyYAML==6.0.1
35-
pyyaml_env_tag==0.1
48+
pyyaml-env-tag==0.1
49+
regex==2023.10.3
50+
requests==2.31.0
51+
setuptools==65.5.1
3652
six==1.16.0
53+
sortedcontainers==2.4.0
54+
tomli==2.0.1
55+
tox==4.11.3
56+
typing-extensions==4.8.0
57+
urllib3==2.1.0
58+
virtualenv==20.24.6
3759
watchdog==3.0.0
38-
pyproject-api==1.6.1
60+
zipp==3.17.0

tests/lexer/test_hypothesis_lexer.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from image_formatter.lexer.lexer import Lexer
2+
from image_formatter.lexer.token import TokenType
3+
from image_formatter.lexer.position import Position
4+
from tests.test_helpers import get_all_tokens
5+
import io
6+
from hypothesis import strategies as st
7+
from hypothesis import given
8+
9+
"""
10+
Lexer has some configurations that depend on user input (tag, special_signs, newline_characters and
11+
additional_path_signs). Most of them have strict list of available symbols. Special_signs doesn't
12+
have that. The following tests are to check if such freedom doesn't break lexer's logic.
13+
They are similar to unit tests in test_unit_lexer.py, but we decided to keep both, because one
14+
might want to execute unit tests without hypothesis tests as they are a bit more time consuming.
15+
"""
16+
17+
18+
def special_sign():
19+
chars_to_exclude = ["\n", "\r", "@", "/", ".", " ", "(", ")", "&"]
20+
return st.text(min_size=1, max_size=1).filter(lambda s: all(char not in s for char in chars_to_exclude))
21+
22+
23+
def special_sign_tuples():
24+
return st.tuples(special_sign(), special_sign(), special_sign())
25+
26+
27+
@given(special_sign_tuples())
28+
def test_given_text_when_tags_not_separated_by_spaces_then_tokens_returned(
29+
special_signs,
30+
):
31+
text = f"@tag1(url1.png)@one{special_signs[0]}more{special_signs[1]}tag&and{special_signs[2]}word"
32+
fp = io.StringIO(text)
33+
lexer = Lexer(fp, special_signs=special_signs)
34+
tokens = get_all_tokens(lexer)
35+
assert [token.type for token in tokens] == [
36+
TokenType.T_IMAGE_SIZE_TAG,
37+
TokenType.T_IMAGE_URL,
38+
TokenType.T_IMAGE_SIZE_TAG,
39+
TokenType.T_CHAR,
40+
TokenType.T_LITERAL,
41+
]
42+
assert [token.position for token in tokens] == [
43+
Position(1, 1),
44+
Position(1, 6),
45+
Position(1, 16),
46+
Position(1, 29),
47+
Position(1, 30),
48+
]
49+
50+
51+
@given(special_sign_tuples())
52+
def test_given_complex_text_with_special_chars_then_sequence_of_tokens_is_returned(
53+
special_signs,
54+
):
55+
text = f"word1& word2 && @tag1{special_signs[0]}tag \n\n @tag2(start{special_signs[1]}of/url.png)"
56+
expected_types = [
57+
TokenType.T_LITERAL,
58+
TokenType.T_CHAR,
59+
TokenType.T_WHITE_CHAR,
60+
TokenType.T_LITERAL,
61+
TokenType.T_WHITE_CHAR,
62+
TokenType.T_CHAR,
63+
TokenType.T_CHAR,
64+
TokenType.T_WHITE_CHAR,
65+
TokenType.T_IMAGE_SIZE_TAG,
66+
TokenType.T_WHITE_CHAR,
67+
TokenType.T_WHITE_CHAR,
68+
TokenType.T_WHITE_CHAR,
69+
TokenType.T_WHITE_CHAR,
70+
TokenType.T_IMAGE_SIZE_TAG,
71+
TokenType.T_IMAGE_URL,
72+
]
73+
expected_positions = [
74+
Position(1, 1),
75+
Position(1, 6),
76+
Position(1, 7),
77+
Position(1, 8),
78+
Position(1, 13),
79+
Position(1, 14),
80+
Position(1, 15),
81+
Position(1, 16),
82+
Position(1, 17),
83+
Position(1, 26),
84+
Position(1, 27),
85+
Position(2, 1),
86+
Position(3, 1),
87+
Position(3, 2),
88+
Position(3, 7),
89+
]
90+
fp = io.StringIO(text)
91+
lexer = Lexer(fp, special_signs=special_signs)
92+
tokens = get_all_tokens(lexer)
93+
assert len(tokens) == len(expected_types)
94+
assert len(tokens) == len(expected_positions)
95+
assert [token.type for token in tokens] == expected_types
96+
assert [token.position for token in tokens] == expected_positions

0 commit comments

Comments
 (0)