Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/libcst/codemod/_testing.py: 33%

43 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:43 +0000

1# Copyright (c) Meta Platforms, Inc. and affiliates. 

2# 

3# This source code is licensed under the MIT license found in the 

4# LICENSE file in the root directory of this source tree. 

5# 

6from textwrap import dedent 

7from typing import Optional, Sequence, Type 

8 

9from libcst import parse_module, PartialParserConfig 

10from libcst.codemod._codemod import Codemod 

11from libcst.codemod._context import CodemodContext 

12from libcst.codemod._runner import SkipFile 

13from libcst.testing.utils import UnitTest 

14 

15 

16# pyre-fixme[13]: This should be an ABC but there are metaclass conflicts due to 

17# the way we implement the data_provider decorator, so pyre complains about the 

18# uninitialized TRANSFORM below. 

19class _CodemodTest: 

20 """ 

21 Mixin that can be added to a unit test framework in order to provide 

22 convenience features. This is provided as an internal-only feature so 

23 that CodemodTest can be used with other frameworks. This is necessary 

24 since we set a metaclass on our UnitTest implementation. 

25 """ 

26 

27 TRANSFORM: Type[Codemod] = ... 

28 

29 @staticmethod 

30 def make_fixture_data(data: str) -> str: 

31 """ 

32 Given a code string originting from a multi-line triple-quoted string, 

33 normalize the code using ``dedent`` and ensuring a trailing newline 

34 is present. 

35 """ 

36 

37 lines = dedent(data).split("\n") 

38 

39 def filter_line(line: str) -> str: 

40 if len(line.strip()) == 0: 

41 return "" 

42 return line 

43 

44 # Get rid of lines that are space only 

45 lines = [filter_line(line) for line in lines] 

46 

47 # Get rid of leading and trailing newlines (because of """ style strings) 

48 while lines and lines[0] == "": 

49 lines = lines[1:] 

50 while lines and lines[-1] == "": 

51 lines = lines[:-1] 

52 

53 code = "\n".join(lines) 

54 if not code.endswith("\n"): 

55 return code + "\n" 

56 else: 

57 return code 

58 

59 def assertCodeEqual(self, expected: str, actual: str) -> None: 

60 """ 

61 Given an expected and actual code string, makes sure they equal. This 

62 ensures that both the expected and actual are sanitized, so its safe to 

63 use this on strings that may have come from a triple-quoted multi-line 

64 string. 

65 """ 

66 

67 # pyre-ignore This mixin needs to be used with a UnitTest subclass. 

68 self.assertEqual( 

69 CodemodTest.make_fixture_data(expected), 

70 CodemodTest.make_fixture_data(actual), 

71 ) 

72 

73 def assertCodemod( 

74 self, 

75 before: str, 

76 after: str, 

77 *args: object, 

78 context_override: Optional[CodemodContext] = None, 

79 python_version: Optional[str] = None, 

80 expected_warnings: Optional[Sequence[str]] = None, 

81 expected_skip: bool = False, 

82 **kwargs: object, 

83 ) -> None: 

84 """ 

85 Given a before and after code string, and any args/kwargs that should 

86 be passed to the codemod constructor specified in 

87 :attr:`~CodemodTest.TRANSFORM`, validate that the codemod executes as 

88 expected. Verify that the codemod completes successfully, unless the 

89 ``expected_skip`` option is set to ``True``, in which case verify that 

90 the codemod skips. Optionally, a :class:`CodemodContext` can be provided. 

91 If none is specified, a default, empty context is created for you. 

92 Additionally, the python version for the code parser can be overridden 

93 to a valid python version string such as `"3.6"`. If none is specified, 

94 the version of the interpreter running your tests will be used. Also, a 

95 list of warning strings can be specified and :meth:`~CodemodTest.assertCodemod` 

96 will verify that the codemod generates those warnings in the order 

97 specified. If it is left out, warnings are not checked. 

98 """ 

99 

100 context = context_override if context_override is not None else CodemodContext() 

101 # pyre-fixme[45]: Cannot instantiate abstract class `Codemod`. 

102 transform_instance = self.TRANSFORM(context, *args, **kwargs) 

103 input_tree = parse_module( 

104 CodemodTest.make_fixture_data(before), 

105 config=( 

106 PartialParserConfig(python_version=python_version) 

107 if python_version is not None 

108 else PartialParserConfig() 

109 ), 

110 ) 

111 try: 

112 output_tree = transform_instance.transform_module(input_tree) 

113 except SkipFile: 

114 if not expected_skip: 

115 raise 

116 output_tree = input_tree 

117 else: 

118 if expected_skip: 

119 # pyre-ignore This mixin needs to be used with a UnitTest subclass. 

120 self.fail("Expected SkipFile but was not raised") 

121 # pyre-ignore This mixin needs to be used with a UnitTest subclass. 

122 self.assertEqual( 

123 CodemodTest.make_fixture_data(after), 

124 CodemodTest.make_fixture_data(output_tree.code), 

125 ) 

126 if expected_warnings is not None: 

127 # pyre-ignore This mixin needs to be used with a UnitTest subclass. 

128 self.assertSequenceEqual(expected_warnings, context.warnings) 

129 

130 

131class CodemodTest(_CodemodTest, UnitTest): 

132 """ 

133 Base test class for a :class:`Codemod` test. Provides facilities for 

134 auto-instantiating and executing a codemod, given the args/kwargs that 

135 should be passed to it. Set the :attr:`~CodemodTest.TRANSFORM` class 

136 attribute to the :class:`Codemod` class you wish to test and call 

137 :meth:`~CodemodTest.assertCodemod` inside your test method to verify it 

138 transforms various source code chunks correctly. 

139 

140 Note that this is a subclass of ``UnitTest`` so any :class:`CodemodTest` 

141 can be executed using your favorite test runner such as the ``unittest`` 

142 module. 

143 """