Skip to content

Commit 794e733

Browse files
committed
test_block_string: add fuzzing test for 'print_block_string'
Replicates graphql/graphql-js@541a449
1 parent ae93e75 commit 794e733

File tree

6 files changed

+151
-34
lines changed

6 files changed

+151
-34
lines changed

tests/language/test_block_string.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
from typing import Optional
2+
3+
from graphql.error import GraphQLSyntaxError
4+
from graphql.language import Source, Lexer, TokenKind
15
from graphql.language.block_string import (
26
dedent_block_string_value,
37
print_block_string,
48
get_block_string_indentation,
59
)
610

11+
from ..utils import dedent, gen_fuzz_strings
12+
713

814
def join_lines(*args):
915
return "\n".join(args)
@@ -132,3 +138,43 @@ def correctly_prints_string_with_a_first_line_indentation():
132138
assert print_block_string(s) == join_lines(
133139
'"""', " first ", " line ", "indentation", " string", '"""'
134140
)
141+
142+
def correctly_print_random_strings():
143+
def lex_value(s: str) -> Optional[str]:
144+
lexer = Lexer(Source(s))
145+
value = lexer.advance().value
146+
assert lexer.advance().kind == TokenKind.EOF, "Expected EOF"
147+
return value
148+
149+
# Testing with length >5 is taking exponentially more time. However it is
150+
# highly recommended to test with increased limit if you make any change.
151+
for fuzz_str in gen_fuzz_strings(allowed_chars='\n\t "a\\', max_length=5):
152+
test_str = f'"""{fuzz_str}"""'
153+
154+
try:
155+
test_value = lex_value(test_str)
156+
except (AssertionError, GraphQLSyntaxError):
157+
continue # skip invalid values
158+
assert isinstance(test_value, str)
159+
160+
printed_value = lex_value(print_block_string(test_value))
161+
162+
assert test_value == printed_value, dedent(
163+
f"""
164+
Expected lex_value(print_block_string({test_value!r})
165+
to equal {test_value!r}
166+
but got {printed_value!r}
167+
"""
168+
)
169+
170+
printed_multiline_string = lex_value(
171+
print_block_string(test_value, " ", True)
172+
)
173+
174+
assert test_value == printed_multiline_string, dedent(
175+
f"""
176+
Expected lex_value(print_block_string({test_value!r}, ' ', True)
177+
to equal {test_value!r}
178+
but got {printed_multiline_string!r}
179+
"""
180+
)

tests/utilities/test_strip_ignored_characters.py

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from graphql.utilities import strip_ignored_characters
99

1010
from ..fixtures import kitchen_sink_query, kitchen_sink_sdl # noqa: F401
11-
from ..utils import dedent
11+
from ..utils import dedent, gen_fuzz_strings
1212

1313
ignored_tokens = [
1414
# UnicodeBOM
@@ -374,39 +374,26 @@ def expect_stripped_string(block_str: str):
374374
expect_stripped_string('"""\n a\n b"""').to_equal('"""a\nb"""')
375375
expect_stripped_string('"""\na\n b\nc"""').to_equal('"""a\n b\nc"""')
376376

377+
def strips_ignored_characters_inside_random_block_strings():
377378
# Testing with length >5 is taking exponentially more time. However it is
378379
# highly recommended to test with increased limit if you make any change.
379-
max_combination_length = 5
380-
possible_chars = ["\n", " ", '"', "a", "\\"]
381-
num_possible_chars = len(possible_chars)
382-
num_combinations = 1
383-
for length in range(1, max_combination_length):
384-
num_combinations *= num_possible_chars
385-
for combination in range(num_combinations):
386-
test_str = '"""'
387-
388-
left_over = combination
389-
for i in range(length):
390-
reminder = left_over % num_possible_chars
391-
test_str += possible_chars[reminder]
392-
left_over = (left_over - reminder) // num_possible_chars
393-
394-
test_str += '"""'
395-
396-
try:
397-
test_value = lex_value(test_str)
398-
except (AssertionError, GraphQLSyntaxError):
399-
continue # skip invalid values
400-
401-
stripped_value = lex_value(strip_ignored_characters(test_str))
402-
403-
assert test_value == stripped_value, dedent(
404-
f"""
405-
Expected lexValue(stripIgnoredCharacters({test_str!r})
406-
to equal {test_value!r}
407-
but got {stripped_value!r}
408-
"""
409-
)
380+
for fuzz_str in gen_fuzz_strings(allowed_chars='\n\t "a\\', max_length=5):
381+
test_str = f'"""{fuzz_str}"""'
382+
383+
try:
384+
test_value = lex_value(test_str)
385+
except (AssertionError, GraphQLSyntaxError):
386+
continue # skip invalid values
387+
388+
stripped_value = lex_value(strip_ignored_characters(test_str))
389+
390+
assert test_value == stripped_value, dedent(
391+
f"""
392+
Expected lexValue(stripIgnoredCharacters({test_str!r})
393+
to equal {test_value!r}
394+
but got {stripped_value!r}
395+
"""
396+
)
410397

411398
# noinspection PyShadowingNames
412399
def strips_kitchen_sink_query_but_maintains_the_exact_same_ast(

tests/utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test utilities"""
22

33
from .dedent import dedent
4+
from .gen_fuzz_strings import gen_fuzz_strings
45

5-
__all__ = ["dedent"]
6+
__all__ = ["dedent", "gen_fuzz_strings"]

tests/utils/gen_fuzz_strings.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Generator
2+
3+
__all__ = ["gen_fuzz_strings"]
4+
5+
6+
def gen_fuzz_strings(allowed_chars: str, max_length: int) -> Generator[str, None, None]:
7+
"""Generator that produces all possible combinations of allowed characters."""
8+
num_allowed_chars = len(allowed_chars)
9+
10+
num_combinations = 0
11+
for length in range(1, max_length + 1):
12+
num_combinations += num_allowed_chars ** length
13+
14+
yield "" # special case for empty string
15+
for combination in range(num_combinations):
16+
permutation = ""
17+
18+
left_over = combination
19+
while left_over >= 0:
20+
reminder = left_over % num_allowed_chars
21+
permutation = allowed_chars[reminder] + permutation
22+
left_over = (left_over - reminder) // num_allowed_chars - 1
23+
24+
yield permutation

tests/utils/test_dedent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..utils import dedent
1+
from . import dedent
22

33

44
def describe_dedent():

tests/utils/test_gen_fuzz_strings.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from . import gen_fuzz_strings
2+
3+
4+
def describe_gen_fuzz_strings():
5+
def always_provide_empty_string():
6+
assert list(gen_fuzz_strings(allowed_chars="", max_length=0)) == [""]
7+
assert list(gen_fuzz_strings(allowed_chars="", max_length=1)) == [""]
8+
assert list(gen_fuzz_strings(allowed_chars="a", max_length=0)) == [""]
9+
10+
def generate_strings_with_single_character():
11+
assert list(gen_fuzz_strings(allowed_chars="a", max_length=1)) == ["", "a"]
12+
assert list(gen_fuzz_strings(allowed_chars="abc", max_length=1)) == [
13+
"",
14+
"a",
15+
"b",
16+
"c",
17+
]
18+
19+
def generate_strings_with_multiple_character():
20+
assert list(gen_fuzz_strings(allowed_chars="a", max_length=2)) == [
21+
"",
22+
"a",
23+
"aa",
24+
]
25+
26+
assert list(gen_fuzz_strings(allowed_chars="abc", max_length=2)) == [
27+
"",
28+
"a",
29+
"b",
30+
"c",
31+
"aa",
32+
"ab",
33+
"ac",
34+
"ba",
35+
"bb",
36+
"bc",
37+
"ca",
38+
"cb",
39+
"cc",
40+
]
41+
42+
def generate_strings_longer_than_possible_number_of_characters():
43+
assert list(gen_fuzz_strings(allowed_chars="ab", max_length=3)) == [
44+
"",
45+
"a",
46+
"b",
47+
"aa",
48+
"ab",
49+
"ba",
50+
"bb",
51+
"aaa",
52+
"aab",
53+
"aba",
54+
"abb",
55+
"baa",
56+
"bab",
57+
"bba",
58+
"bbb",
59+
]

0 commit comments

Comments
 (0)