Coverage for tests/test_invalid_ast.py: 56%

92 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-06-13 21:17 +0200

1import ast 

2import hashlib 

3import sys 

4import textwrap 

5import warnings 

6from pathlib import Path 

7 

8import pytest 

9from pysource_minimize._minimize import minimize_ast 

10 

11from pysource_codegen._codegen import generate_ast 

12from pysource_codegen._codegen import is_valid_ast 

13from pysource_codegen._codegen import unparse 

14from pysource_codegen._utils import ast_dump 

15 

16sample_dir = Path(__file__).parent / "invalid_ast_samples" 

17sample_dir.mkdir(exist_ok=True) 

18 

19 

20def does_compile(tree: ast.Module): 

21 for node in ast.walk(tree): 

22 if isinstance(node, ast.BoolOp) and len(node.values) < 2: 

23 return False 

24 if not isinstance(node, ast.JoinedStr) and any( 

25 isinstance(n, ast.FormattedValue) for n in ast.iter_child_nodes(node) 

26 ): 

27 return False 

28 try: 

29 with warnings.catch_warnings(): 

30 warnings.simplefilter("ignore", SyntaxWarning) 

31 source = unparse(tree) 

32 compile(source, "<file>", "exec") 

33 compile(ast.fix_missing_locations(tree), "<file>", "exec") 

34 except Exception as e: 

35 print(e) 

36 return False 

37 return True 

38 

39 

40@pytest.mark.parametrize( 

41 "file", [pytest.param(f, id=f.stem[:12]) for f in sample_dir.glob("*.py")] 

42) 

43def test_invalid_ast(file): 

44 code = file.read_text() 

45 print(code) 

46 globals = {} 

47 try: 

48 exec(code, globals) 

49 except (NameError, ImportError) as e: 

50 pytest.skip(f"wrong python version {e}") 

51 

52 tree = globals["tree"] 

53 

54 for node in ast.walk(tree): 

55 for field in node._fields: 

56 if not hasattr(node, field): 

57 pytest.skip( 

58 f"wrong python version {node.__class__.__name__} is missing .{field}" 

59 ) 

60 if sys.version_info < (3, 8) and isinstance(node, ast.Constant): 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true

61 pytest.skip(f"ast.Constant can not be unparsed on python3.7") 

62 

63 assert is_valid_ast(tree) == does_compile(tree) 

64 

65 

66def x_test_example(): 

67 seed = 2273381 

68 tree = generate_ast(seed) 

69 tree = generate_ast(seed, depth_limit=9) 

70 # print(ast.dump(tree, indent=2)) 

71 assert is_valid_ast(tree) 

72 

73 

74def generate_invalid_ast(seed): 

75 print("seed =", seed) 

76 

77 tree = generate_ast(seed, depth_limit=9) 

78 try: 

79 assert is_valid_ast(tree) 

80 except: 

81 print(f"error for is_valid_ast seed={seed}") 

82 raise 

83 

84 if not does_compile(tree): 

85 last_checked_tree = tree 

86 

87 def checker(tree): 

88 nonlocal last_checked_tree 

89 

90 bug_found = not does_compile(tree) and is_valid_ast(tree) 

91 if bug_found: 

92 last_checked_tree = tree 

93 

94 return bug_found 

95 

96 try: 

97 new_tree = minimize_ast(tree, checker) 

98 except: 

99 print(f"error happend while minimize_ast seed={seed}") 

100 print(ast_dump(last_checked_tree)) 

101 raise 

102 

103 print( 

104 "pysource-codegen thinks that the current ast produces valid python code, but this is not the case:" 

105 ) 

106 info = "from ast import *\n" 

107 info += f"tree = {ast_dump(new_tree)}\n" 

108 source = "" 

109 try: 

110 source = unparse(new_tree) 

111 compile(source, "<file>", "exec") 

112 compile(ast.fix_missing_locations(tree), "<file>", "exec") 

113 except Exception as e: 

114 comment = f"version: {sys.version.split()[0]}\nseed = {seed}\n\n" 

115 if source: 

116 comment += f"Source:\n{source}\n\n" 

117 comment += f"\nError:\n {e!r}" 

118 

119 info += "\n" + textwrap.indent(comment, "# ", lambda l: True) 

120 

121 print(info) 

122 name = sample_dir / f"{hashlib.sha256(info.encode('utf-8')).hexdigest()}.py" 

123 name.write_text(info) 

124 return True 

125 else: 

126 assert False 

127 return False