se2p / pynguin

The PYthoN General UnIt Test geNerator is a test-generation tool for Python
https://www.pynguin.eu
MIT License
1.22k stars 74 forks source link

Fix signature and private module errors that comes from C extension modules #60

Closed BergLucas closed 4 months ago

BergLucas commented 4 months ago

Hello,

I've tried to fix the bug that I've explained in issue #59.

My solution to the "no signature found" error is just to provide a default method signature that is very generic and that works on every function. This default method signature is only used when the inspect module can't find a method signature and it's just to prevent Pynguin from crashing. This creates a signature that is the same as for this function:

def foo(*args, **kwargs): ...

However, after correcting this error, I came across two others that were very closely related to it, so I've decided to correct them here too.

First, I got :

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/lucas/.conda/envs/2324-master-thesis/bin/pynguin:8 in <module>                             │
│                                                                                                  │
│   5 from pynguin.cli import main                                                                 │
│   6 if __name__ == '__main__':                                                                   │
│   7 │   sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])                         │
│ ❱ 8 │   sys.exit(main())                                                                         │
│   9                                                                                              │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/cli.py:193 in main                              │
│                                                                                                  │
│   190 │   set_configuration(parsed.config)                                                       │
│   191 │   if console is not None:                                                                │
│   192 │   │   with console.status("Running Pynguin..."):                                         │
│ ❱ 193 │   │   │   return run_pynguin().value                                                     │
│   194 │   else:                                                                                  │
│   195 │   │   return run_pynguin().value                                                         │
│   196                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:108 in run_pynguin                 │
│                                                                                                  │
│   105 │   """                                                                                    │
│   106 │   try:                                                                                   │
│   107 │   │   _LOGGER.info("Start Pynguin Test Generation…")                                     │
│ ❱ 108 │   │   return _run()                                                                      │
│   109 │   finally:                                                                               │
│   110 │   │   _LOGGER.info("Stop Pynguin Test Generation…")                                      │
│   111                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:507 in _run                        │
│                                                                                                  │
│   504                                                                                            │
│   505                                                                                            │
│   506 def _run() -> ReturnCode:                                                                  │
│ ❱ 507 │   if (setup_result := _setup_and_check()) is None:                                       │
│   508 │   │   return ReturnCode.SETUP_FAILED                                                     │
│   509 │   executor, test_cluster, constant_provider = setup_result                               │
│   510 │   # traces slices for test cases after execution                                         │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:258 in _setup_and_check            │
│                                                                                                  │
│   255 │                                                                                          │
│   256 │   # Analyzing the SUT should not cause any coverage.                                     │
│   257 │   tracer.disable()                                                                       │
│ ❱ 258 │   if (test_cluster := _setup_test_cluster()) is None:                                    │
│   259 │   │   return None                                                                        │
│   260 │   tracer.enable()                                                                        │
│   261                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:114 in _setup_test_cluster         │
│                                                                                                  │
│   111                                                                                            │
│   112                                                                                            │
│   113 def _setup_test_cluster() -> ModuleTestCluster | None:                                     │
│ ❱ 114 │   test_cluster = generate_test_cluster(                                                  │
│   115 │   │   config.configuration.module_name,                                                  │
│   116 │   │   config.configuration.type_inference.type_inference_strategy,                       │
│   117 │   │   query_type4py=config.configuration.type_inference.type4py,                         │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1450 in                      │
│ generate_test_cluster                                                                            │
│                                                                                                  │
│   1447 │   Returns:                                                                              │
│   1448 │   │   A new test cluster for the given module                                           │
│   1449 │   """                                                                                   │
│ ❱ 1450 │   return analyse_module(                                                                │
│   1451 │   │   parse_module(module_name, query_type4py=query_type4py),                           │
│   1452 │   │   type_inference_strategy,                                                          │
│   1453 │   │   query_type4py=query_type4py,                                                      │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1425 in analyse_module       │
│                                                                                                  │
│   1422 │   │   A test cluster for the module                                                     │
│   1423 │   """                                                                                   │
│   1424 │   test_cluster = ModuleTestCluster(linenos=parsed_module.linenos)                       │
│ ❱ 1425 │   __resolve_dependencies(                                                               │
│   1426 │   │   root_module=parsed_module,                                                        │
│   1427 │   │   type_inference_strategy=type_inference_strategy,                                  │
│   1428 │   │   test_cluster=test_cluster,                                                        │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1295 in                      │
│ __resolve_dependencies                                                                           │
│                                                                                                  │
│   1292 │   │   │   continue                                                                      │
│   1293 │   │                                                                                     │
│   1294 │   │   # Analyze all classes found in the current module                                 │
│ ❱ 1295 │   │   __analyse_included_classes(                                                       │
│   1296 │   │   │   module=current_module,                                                        │
│   1297 │   │   │   root_module_name=root_module.module_name,                                     │
│   1298 │   │   │   type_inference_strategy=type_inference_strategy,                              │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1355 in                      │
│ __analyse_included_classes                                                                       │
│                                                                                                  │
│   1352 │   │                                                                                     │
│   1353 │   │   type_info = test_cluster.type_system.to_type_info(current)                        │
│   1354 │   │                                                                                     │
│ ❱ 1355 │   │   results = parse_results[current.__module__]                                       │
│   1356 │   │                                                                                     │
│   1357 │   │   __analyse_class(                                                                  │
│   1358 │   │   │   type_info=type_info,                                                          │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1249 in __missing__          │
│                                                                                                  │
│   1246 │                                                                                         │
│   1247 │   def __missing__(self, key):                                                           │
│   1248 │   │   # Parse module on demand                                                          │
│ ❱ 1249 │   │   res = self[key] = parse_module(key, query_type4py=self._query_type4py)            │
│   1250 │   │   return res                                                                        │
│   1251                                                                                           │
│   1252                                                                                           │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:248 in parse_module          │
│                                                                                                  │
│    245 │   Returns:                                                                              │
│    246 │   │   A tuple of the imported module type and its optional AST                          │
│    247 │   """                                                                                   │
│ ❱  248 │   module = importlib.import_module(module_name)                                         │
│    249 │   type4py_data: Type4pyData | None = None                                               │
│    250 │   syntax_tree: astroid.Module | None = None                                             │
│    251 │   linenos: int = -1                                                                     │
│                                                                                                  │
│ /home/lucas/.conda/envs/2324-master-thesis/lib/python3.10/importlib/__init__.py:126 in           │
│ import_module                                                                                    │
│                                                                                                  │
│   123 │   │   │   if character != '.':                                                           │
│   124 │   │   │   │   break                                                                      │
│   125 │   │   │   level += 1                                                                     │
│ ❱ 126 │   return _bootstrap._gcd_import(name[level:], package, level)                            │
│   127                                                                                            │
│   128                                                                                            │
│   129 _RELOADING = {}                                                                            │
│ in _gcd_import:1050                                                                              │
│ in _find_and_load:1027                                                                           │
│ in _find_and_load_unlocked:1004                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ModuleNotFoundError: No module named 'pybind11_builtins'

I've searched online and I think that this module is only available from C code and not from Python code so I've added a condition that skips modules that come from C code if they are not found using the inspect module.

Then, I got the following error:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/lucas/.conda/envs/2324-master-thesis/bin/pynguin:8 in <module>                             │
│                                                                                                  │
│   5 from pynguin.cli import main                                                                 │
│   6 if __name__ == '__main__':                                                                   │
│   7 │   sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])                         │
│ ❱ 8 │   sys.exit(main())                                                                         │
│   9                                                                                              │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/cli.py:193 in main                              │
│                                                                                                  │
│   190 │   set_configuration(parsed.config)                                                       │
│   191 │   if console is not None:                                                                │
│   192 │   │   with console.status("Running Pynguin..."):                                         │
│ ❱ 193 │   │   │   return run_pynguin().value                                                     │
│   194 │   else:                                                                                  │
│   195 │   │   return run_pynguin().value                                                         │
│   196                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:108 in run_pynguin                 │
│                                                                                                  │
│   105 │   """                                                                                    │
│   106 │   try:                                                                                   │
│   107 │   │   _LOGGER.info("Start Pynguin Test Generation…")                                     │
│ ❱ 108 │   │   return _run()                                                                      │
│   109 │   finally:                                                                               │
│   110 │   │   _LOGGER.info("Stop Pynguin Test Generation…")                                      │
│   111                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:507 in _run                        │
│                                                                                                  │
│   504                                                                                            │
│   505                                                                                            │
│   506 def _run() -> ReturnCode:                                                                  │
│ ❱ 507 │   if (setup_result := _setup_and_check()) is None:                                       │
│   508 │   │   return ReturnCode.SETUP_FAILED                                                     │
│   509 │   executor, test_cluster, constant_provider = setup_result                               │
│   510 │   # traces slices for test cases after execution                                         │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:258 in _setup_and_check            │
│                                                                                                  │
│   255 │                                                                                          │
│   256 │   # Analyzing the SUT should not cause any coverage.                                     │
│   257 │   tracer.disable()                                                                       │
│ ❱ 258 │   if (test_cluster := _setup_test_cluster()) is None:                                    │
│   259 │   │   return None                                                                        │
│   260 │   tracer.enable()                                                                        │
│   261                                                                                            │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/generator.py:114 in _setup_test_cluster         │
│                                                                                                  │
│   111                                                                                            │
│   112                                                                                            │
│   113 def _setup_test_cluster() -> ModuleTestCluster | None:                                     │
│ ❱ 114 │   test_cluster = generate_test_cluster(                                                  │
│   115 │   │   config.configuration.module_name,                                                  │
│   116 │   │   config.configuration.type_inference.type_inference_strategy,                       │
│   117 │   │   query_type4py=config.configuration.type_inference.type4py,                         │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1495 in                      │
│ generate_test_cluster                                                                            │
│                                                                                                  │
│   1492 │   Returns:                                                                              │
│   1493 │   │   A new test cluster for the given module                                           │
│   1494 │   """                                                                                   │
│ ❱ 1495 │   return analyse_module(                                                                │
│   1496 │   │   parse_module(module_name, query_type4py=query_type4py),                           │
│   1497 │   │   type_inference_strategy,                                                          │
│   1498 │   │   query_type4py=query_type4py,                                                      │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1470 in analyse_module       │
│                                                                                                  │
│   1467 │   │   A test cluster for the module                                                     │
│   1468 │   """                                                                                   │
│   1469 │   test_cluster = ModuleTestCluster(linenos=parsed_module.linenos)                       │
│ ❱ 1470 │   __resolve_dependencies(                                                               │
│   1471 │   │   root_module=parsed_module,                                                        │
│   1472 │   │   type_inference_strategy=type_inference_strategy,                                  │
│   1473 │   │   test_cluster=test_cluster,                                                        │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1331 in                      │
│ __resolve_dependencies                                                                           │
│                                                                                                  │
│   1328 │   │   │   continue                                                                      │
│   1329 │   │                                                                                     │
│   1330 │   │   # Analyze all classes found in the current module                                 │
│ ❱ 1331 │   │   __analyse_included_classes(                                                       │
│   1332 │   │   │   module=current_module,                                                        │
│   1333 │   │   │   root_module_name=root_module.module_name,                                     │
│   1334 │   │   │   type_inference_strategy=type_inference_strategy,                              │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1393 in                      │
│ __analyse_included_classes                                                                       │
│                                                                                                  │
│   1390 │   │                                                                                     │
│   1391 │   │   # Skip some C-extension modules that are not publicly accessible.                 │
│   1392 │   │   try:                                                                              │
│ ❱ 1393 │   │   │   results = parse_results[current.__module__]                                   │
│   1394 │   │   except ModuleNotFoundError as error:                                              │
│   1395 │   │   │   if getattr(current, "__file__", None) is None or Path(                        │
│   1396 │   │   │   │   current.__file__                                                          │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:1285 in __missing__          │
│                                                                                                  │
│   1282 │                                                                                         │
│   1283 │   def __missing__(self, key):                                                           │
│   1284 │   │   # Parse module on demand                                                          │
│ ❱ 1285 │   │   res = self[key] = parse_module(key, query_type4py=self._query_type4py)            │
│   1286 │   │   return res                                                                        │
│   1287                                                                                           │
│   1288                                                                                           │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:284 in parse_module          │
│                                                                                                  │
│    281 │   Returns:                                                                              │
│    282 │   │   A tuple of the imported module type and its optional AST                          │
│    283 │   """                                                                                   │
│ ❱  284 │   module = import_module(module_name)                                                   │
│    285 │   type4py_data: Type4pyData | None = None                                               │
│    286 │   syntax_tree: astroid.Module | None = None                                             │
│    287 │   linenos: int = -1                                                                     │
│                                                                                                  │
│ /home/lucas/Documents/GitHub/pynguin/src/pynguin/analyses/module.py:244 in import_module         │
│                                                                                                  │
│    241 │   │   The imported module                                                               │
│    242 │   """                                                                                   │
│    243 │   try:                                                                                  │
│ ❱  244 │   │   return importlib.import_module(module_name)                                       │
│    245 │   except ModuleNotFoundError as error:                                                  │
│    246 │   │   try:                                                                              │
│    247 │   │   │   package_name, submodule_name = module_name.rsplit(".", 1)                     │
│                                                                                                  │
│ /home/lucas/.conda/envs/2324-master-thesis/lib/python3.10/importlib/__init__.py:117 in           │
│ import_module                                                                                    │
│                                                                                                  │
│   114 │                                                                                          │
│   115 │   """                                                                                    │
│   116 │   level = 0                                                                              │
│ ❱ 117 │   if name.startswith('.'):                                                               │
│   118 │   │   if not package:                                                                    │
│   119 │   │   │   msg = ("the 'package' argument is required to perform a relative "             │
│   120 │   │   │   │      "import for {!r}")                                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'property' object has no attribute 'startswith'

This error comes from the _ObjectProxyMethods class that overrides the __module__ attribute. I think that normally, the __module__ attribute is defined on the class and not on instances. I tried to create a custom property class that would keep the existing behavior but that would add the normal behavior on the _ObjectProxyMethods class but that custom property would only work on "normal" methods and not on dunder methods so this idea was abandoned. Instead, I've just added a condition that skips when a __module__ attribute is a property instead of a str.

What do you think of this solution?

Kind regards and have a nice day!

stephanlukasczyk commented 4 months ago

The _ObjectProxyMethods is quite a fragile thing, I agree. It's probably not optimal regarding usability and portability to different subjects. C extensions always cause trouble, also regarding the bytecode instrumentation. However, I appreciate your approach and will happily incorporate in the upcoming Pynguin release.