Files
Leonid Shalupov 28300b5f37 IJI-2422: community/python: move resources under resources root
GitOrigin-RevId: 4c24cfa366aebda0fef265b18c04a731a4f43a69
2025-02-05 10:14:39 +00:00

596 lines
24 KiB
Python

from __future__ import print_function, unicode_literals
import errno
import os
import sys
import textwrap
import unittest
import generator3.core
import json
from generator3.constants import (
CACHE_DIR_NAME,
ENV_REQUIRED_GEN_VERSION_FILE,
ENV_TEST_MODE_FLAG,
ENV_VERSION,
STATE_FILE_NAME)
from generator3.core import file_modification_timestamp
from generator3.util_methods import ignored_os_errors, mkdir
from generator3_tests import GeneratorTestCase, python2_only, python3_only, test_data_dir
# Such version implies that skeletons are always regenerated
TEST_GENERATOR_VERSION = '1000.0'
ARGPARSE_ERROR_CODE = 2
_helpers_root = os.path.dirname(os.path.dirname(os.path.abspath(generator3.__file__)))
class GeneratorResult(object):
def __init__(self, process_result, skeletons_dir):
self.skeletons_dir = skeletons_dir
# self.__dict__.update(process_result._as_dict())
self.exit_code = process_result.exit_code
self.stdout = process_result.stdout
self.stderr = process_result.stderr
def control_messages(self, type_=None):
result = []
for line in self.stdout.splitlines(False): # type: str
if line.startswith('{'):
message = json.loads(line)
if type_ is None or message['type'] == type_:
result.append(message)
return result
@property
def state_json(self):
with ignored_os_errors(errno.ENOENT):
with open(os.path.join(self.skeletons_dir, STATE_FILE_NAME)) as f:
return json.load(f)
# noinspection PyUnreachableCode
return None
class FunctionalGeneratorTestCase(GeneratorTestCase):
SDK_SKELETONS_DIR = 'sdk_skeletons'
PYTHON_STUBS_DIR = 'python_stubs'
default_generator_extra_args = []
default_generator_extra_syspath = []
@property
def temp_python_stubs_root(self):
return os.path.join(self.temp_dir, self.PYTHON_STUBS_DIR)
@property
def temp_skeletons_dir(self):
return os.path.join(self.temp_python_stubs_root, self.SDK_SKELETONS_DIR)
@property
def temp_cache_dir(self):
return os.path.join(self.temp_python_stubs_root, CACHE_DIR_NAME)
def tearDownForFailedTest(self):
print("\nLaunched processes stdout:\n" + self.process_stdout.getvalue() + '-' * 80)
print("\nLaunched processes stderr:\n" + self.process_stderr.getvalue() + '-' * 80)
def run_generator(self, mod_qname=None,
mod_path=None,
extra_syspath=None,
gen_version=None,
required_gen_version_file_path=None,
extra_env=None,
extra_args=None,
output_dir=None,
input=None):
if output_dir is None:
output_dir = self.temp_skeletons_dir
else:
output_dir = os.path.join(self.temp_dir, output_dir)
if extra_syspath is None:
extra_syspath = self.default_generator_extra_syspath
if not extra_syspath:
extra_syspath = [self.test_data_dir]
extra_syspath = [p if os.path.isabs(p) else self.resolve_in_test_data(p)
for p in extra_syspath]
if mod_path and not os.path.isabs(mod_path):
mod_path = os.path.join(extra_syspath[0], mod_path)
env = {
ENV_TEST_MODE_FLAG: 'True',
ENV_VERSION: gen_version or TEST_GENERATOR_VERSION,
'PYTHONPATH': _helpers_root,
}
if required_gen_version_file_path:
env[ENV_REQUIRED_GEN_VERSION_FILE] = required_gen_version_file_path
if extra_env:
env.update(extra_env)
args = [
sys.executable,
'-m',
'generator3',
'-d', output_dir,
'-s', os.pathsep.join(extra_syspath),
]
if extra_args is None:
extra_args = self.default_generator_extra_args
if extra_args:
args.extend(extra_args)
elif mod_qname:
args.append(mod_qname)
if mod_path:
args.append(mod_path)
self.log.info('Launching generator3 as: ' + ' '.join(args))
result = self.run_process(args, input=input, env=env)
return GeneratorResult(result, skeletons_dir=output_dir)
def check_generator_output(self, mod_name=None, mod_path=None, custom_required_gen=False, success=True, **kwargs):
if custom_required_gen:
kwargs.setdefault('required_gen_version_file_path',
self.resolve_in_test_data('required_gen_version'))
with self.comparing_dirs(tmp_subdir=self.PYTHON_STUBS_DIR):
result = self.run_generator(mod_name, mod_path=mod_path, **kwargs)
self.assertEqual(success, result.exit_code == 0)
class SkeletonGenerationTest(FunctionalGeneratorTestCase):
def test_layout_for_builtin_module(self):
self.run_generator(mod_qname='_ast')
self.assertDirLayoutEquals(self.temp_python_stubs_root, """
cache/
{hash}/
_ast.py
sdk_skeletons/
_ast.py
""".format(hash=generator3.core.module_hash('_ast', None)))
def test_layout_for_toplevel_physical_module(self):
mod_path = self.resolve_in_test_data('mod.py')
self.run_generator(mod_qname='mod', mod_path=mod_path)
self.assertDirLayoutEquals(self.temp_python_stubs_root, """
cache/
{hash}/
mod.py
sdk_skeletons/
mod.py
""".format(hash=generator3.core.module_hash('mod', mod_path)))
def test_layout_for_physical_module_inside_package(self):
mod_path = self.resolve_in_test_data('pkg/subpkg/mod.py')
self.run_generator(mod_qname='pkg.subpkg.mod', mod_path=mod_path)
self.assertDirLayoutEquals(self.temp_python_stubs_root, """
cache/
{hash}/
pkg/
__init__.py
subpkg/
__init__.py
mod.py
sdk_skeletons/
pkg/
__init__.py
subpkg/
__init__.py
mod.py
""".format(hash=generator3.core.module_hash('mod', mod_path)))
# PY-36884
def test_pregenerated_skeletons_mode(self):
self.check_generator_output('mod', mod_path='mod.py', extra_env={
'IS_PREGENERATED_SKELETONS': '1'
})
# PY-36884
def test_pregenerated_skeletons_mode_for_builtin_module(self):
self.run_generator('sys', extra_env={
'IS_PREGENERATED_SKELETONS': '1'
})
sys_cached_skeleton_path = os.path.join(self.temp_cache_dir, generator3.core.module_hash('sys', None), 'sys.py')
self.assertTrue(os.path.exists(sys_cached_skeleton_path))
with open(sys_cached_skeleton_path, 'r') as f:
self.assertTrue('# from (pre-generated)\n' in f.read())
sys_skeleton_path = os.path.join(self.temp_skeletons_dir, 'sys.py')
self.assertTrue(os.path.exists(sys_skeleton_path))
with open(sys_skeleton_path, 'r') as f:
self.assertTrue('# from (built-in)\n' in f.read())
def test_skeleton_regenerated_for_changed_module(self):
self.check_generator_output('mod', mod_path='mod.py', extra_syspath=['versions/v2'])
def test_skeleton_regenerated_for_upgraded_generator_with_explicit_update_stamp(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_skeleton_not_regenerated_for_upgraded_generator_without_explicit_version_stamp(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_skeleton_not_regenerated_for_upgraded_generator_with_earlier_update_stamp(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_version_stamp_put_in_cache_directory_for_failed_module(self):
self.check_generator_output('failing', mod_path='failing.py', gen_version='0.1', success=False)
def test_skeleton_regenerated_for_failed_module_on_generator_upgrade(self):
self.check_generator_output('failing', mod_path='failing.py', gen_version='0.2')
def test_skeleton_not_regenerated_for_failed_module_on_same_generator_version(self):
self.check_generator_output('failing', mod_path='failing.py', gen_version='0.1', success=False)
def test_segmentation_fault_handling(self):
self.check_generator_output('sigsegv', mod_path='sigsegv.py', gen_version='0.1', success=False)
def test_cache_not_updated_when_sdk_skeleton_is_up_to_date(self):
# We can't safely updated cache from SDK skeletons (backwards) because of binaries declaring
# multiple modules. Skeletons for them are scattered across SDK skeletons directory, and we can't
# collect them reliably.
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_cache_skeleton_reused_when_sdk_skeleton_is_missing(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_cache_skeleton_reused_when_sdk_skeleton_is_outdated(self):
# SDK skeleton version is 0.1, cache skeleton version is 0.2, skeletons need to be updated starting from 0.2
# New skeleton would have version 0.3, but we shouldn't generate any.
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.3', custom_required_gen=True)
def test_cache_skeleton_generated_and_reused_when_sdk_skeleton_is_outdated(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.2', custom_required_gen=True)
def test_cache_skeleton_reused_when_sdk_skeleton_generation_failed(self):
# Generation failed for version 0.1, cache skeleton version is 0.2
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.3', custom_required_gen=True)
def test_cache_skeleton_not_regenerated_when_sdk_skeleton_generation_failed_for_same_version_and_same_binary(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.1', custom_required_gen=True,
success=False)
def test_cache_skeleton_regenerated_when_sdk_skeleton_generation_failed_for_modified_binary(self):
self.check_generator_output('mod', mod_path='mod.py', gen_version='0.1', custom_required_gen=True)
@python3_only
def test_inaccessible_class_attribute_py3(self):
self.check_generator_output('mod', mod_path='mod.py', success=False)
@python2_only
def test_inaccessible_class_attribute_py2(self):
self.check_generator_output('mod', mod_path='mod.py', success=False)
def test_binary_declares_multiple_modules(self):
self.check_generator_output('mod', mod_path='mod.py')
def test_binary_declares_extra_module_that_fails(self):
self.check_generator_output('mod', mod_path='mod.py', success=False)
def test_origin_stamp_in_skeleton_header_is_updated_on_copying(self):
self.check_generator_output('mod', mod_path='mod.py')
def test_origin_stamp_for_pregenerated_builtins_is_updated(self):
mod_hash = generator3.core.module_hash('_abc', None)
template = textwrap.dedent("""\
# encoding: utf-8
# module _ast
# from {origin}
# by generator 1000.0
""")
cache_entry_dir = os.path.join(self.temp_cache_dir, mod_hash)
mkdir(cache_entry_dir)
with open(os.path.join(cache_entry_dir, '_ast.py'), 'w') as f:
f.write(template.format(origin='(pre-generated)'))
self.run_generator('_ast', None)
with open(os.path.join(self.temp_skeletons_dir, '_ast.py')) as f:
self.assertEqual(template.format(origin='(built-in)'), f.read())
def test_single_pyexpat_skeletons_layout(self):
self.run_generator('pyexpat')
self.assertFalse(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat.py')))
self.assertTrue(os.path.isdir(os.path.join(self.temp_skeletons_dir, 'pyexpat')))
self.assertNonEmptyFile(os.path.join(self.temp_skeletons_dir, 'pyexpat', '__init__.py'))
self.assertTrue(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat', 'model.py')))
self.assertTrue(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat', 'errors.py')))
@python3_only
def test_introspecting_submodule_modifies_sys_modules(self):
self.check_generator_output('mod', 'mod.py')
# PY-37241
# Python 2 version of the skeleton differs significantly
# TODO investigate why
@python3_only
def test_non_string_dunder_module(self):
self.check_generator_output('mod', 'mod.py')
def test_results_in_single_module_mode(self):
result = self.run_generator('mod', 'mod.py')
self.assertIn({
'type': 'generation_result',
'module_name': 'mod',
'module_origin': 'mod.py',
'generation_status': 'GENERATED'
}, result.control_messages())
def test_trailing_slash_in_sdk_skeletons_path_does_not_affect_cache_location(self):
self.run_generator('mod', 'mod.py', output_dir=self.temp_skeletons_dir + os.path.sep)
self.assertDirLayoutEquals(self.temp_python_stubs_root, """
cache/
e3b0c44298/
mod.py
sdk_skeletons/
mod.py
""")
@python3_only
def test_keyword_only_arguments_in_signatures(self):
self.check_generator_output('mod', 'mod.py')
@python3_only
def test_keyword_only_arguments_in_return_type_constructor(self):
self.check_generator_output('mod', 'mod.py')
def test_stdlib_modules_used_by_pyparsing_not_considered_submodules(self):
result = self.run_generator('_ast')
submodule_logs = [m for m in result.control_messages('log')
if 'generator3._vendor.' in m['message']]
self.assertFalse(submodule_logs)
# PY-60592
def test_ignoring_extra_submodules_not_under_binary_own_qname(self):
self.check_generator_output('pkg.mod', 'pkg/mod.py')
# PY-60959
def test_module_attributes_with_illegal_names_are_skipped(self):
self.check_generator_output('mod', 'mod.py')
class MultiModuleGenerationTest(FunctionalGeneratorTestCase):
default_generator_extra_args = ['--name-pattern', 'mod?']
# This is a hack to keep the existing behavior where we keep discovering only binary files
# (which can't be distributed with tests in a platform-independent manner), but user their .py
# counterparts for actual importing and introspection
default_generator_extra_syspath = ['mocks', 'binaries']
def test_skeletons_for_builtins_are_stored_in_same_cache_directory(self):
self.run_generator(extra_args=['--builtins-only'], extra_syspath=[])
builtins_hash = generator3.core.module_hash('sys', None)
builtins_cache_dir = os.path.join(self.temp_cache_dir, builtins_hash)
self.assertTrue(os.path.isdir(builtins_cache_dir))
builtin_mod_skeletons = os.listdir(builtins_cache_dir)
self.assertIn('_ast.py', builtin_mod_skeletons)
self.assertIn('sys.py', builtin_mod_skeletons)
# TODO figure out why this is not true for some interpreters
@unittest.skipUnless('pyexpat' in sys.builtin_module_names, "pyexpat must be a built-in module")
def test_pyexpat_layout_in_builtins(self):
self.run_generator(extra_args=['--builtins-only'], extra_syspath=[])
self.assertFalse(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat.py')))
self.assertTrue(os.path.isdir(os.path.join(self.temp_skeletons_dir, 'pyexpat')))
self.assertNonEmptyFile(os.path.join(self.temp_skeletons_dir, 'pyexpat', '__init__.py'))
self.assertTrue(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat', 'model.py')))
self.assertTrue(os.path.exists(os.path.join(self.temp_skeletons_dir, 'pyexpat', 'errors.py')))
@test_data_dir('simple')
def test_progress_indication(self):
result = self.run_generator()
self.assertContainsInRelativeOrder([
{'type': 'progress', 'text': 'mod1', 'minor': True, 'fraction': 0.0},
{'type': 'progress', 'text': 'mod2', 'minor': True, 'fraction': 0.5},
{'type': 'progress', 'fraction': 1.0},
], result.control_messages())
@test_data_dir('simple')
def test_intermediate_results_reporting(self):
result = self.run_generator()
self.assertContainsInRelativeOrder([
{'type': 'generation_result',
'module_name': 'mod1',
'module_origin': 'mod1.so',
'generation_status': 'GENERATED'},
{'type': 'generation_result',
'module_name': 'mod2',
'module_origin': 'mod2.so',
'generation_status': 'GENERATED'}
], result.control_messages())
@test_data_dir('simple')
def test_general_results_and_layout(self):
self.check_generator_output()
@test_data_dir('simple')
def test_logging_configured_and_propagates_from_worker_subprocess(self):
result = self.run_generator()
subprocess_messages = [m for m in result.control_messages('log')
if m['message'].startswith('Updating cache for mod')]
self.assertEqual(2, len(subprocess_messages))
class StatePassingGenerationTest(FunctionalGeneratorTestCase):
default_generator_extra_args = ['--state-file', '-',
'--name-pattern', 'mod?']
default_generator_extra_syspath = ['mocks', 'binaries']
def test_existing_updated_due_to_required_gen_version(self):
state = {
'sdk_skeletons': {
# shouldn't be updated
'mod1': {
'gen_version': '0.1',
'status': 'GENERATED'
},
# should be updated because of "required_gen_version" file
'mod2': {
'gen_version': '0.1',
'status': 'GENERATED'
}
}
}
self.check_generator_output(input=json.dumps(state),
custom_required_gen=True,
gen_version='0.2')
def test_failed_updated_due_to_updated_generator_version(self):
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.2',
'status': 'FAILED',
},
'mod2': {
'gen_version': '0.1',
'status': 'FAILED'
}
}
}
self.check_generator_output(input=json.dumps(state),
custom_required_gen=True,
gen_version='0.2')
def test_existing_updated_due_to_modified_binary(self):
mod1_mtime = file_modification_timestamp(
self.resolve_in_test_data('binaries/mod1.so'))
mod2_mtime = file_modification_timestamp(
self.resolve_in_test_data('binaries/mod2.so'))
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.1',
'bin_mtime': mod1_mtime,
'status': 'GENERATED',
},
'mod2': {
'gen_version': '0.1',
# generated for a binary modified ten seconds before its current state
'bin_mtime': mod2_mtime - 10,
'status': 'GENERATED'
}
}
}
self.check_generator_output(input=json.dumps(state),
custom_required_gen=True,
gen_version='0.2')
def test_failed_updated_due_to_modified_binary(self):
mod1_mtime = file_modification_timestamp(self.resolve_in_test_data('binaries/mod1.so'))
mod2_mtime = file_modification_timestamp(self.resolve_in_test_data('binaries/mod2.so'))
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.1',
'bin_mtime': mod1_mtime,
'status': 'FAILED',
},
'mod2': {
'gen_version': '0.1',
# generated for a binary modified ten seconds before its current state
'bin_mtime': mod2_mtime - 10,
'status': 'FAILED'
}
}
}
self.check_generator_output(input=json.dumps(state),
custom_required_gen=True,
gen_version='0.1')
def test_failed_skeleton_skipped(self):
mod_mtime = file_modification_timestamp(self.resolve_in_test_data('binaries/mod.so'))
state = {
'sdk_skeletons': {
'mod': {
'gen_version': '0.1',
'status': 'FAILED',
'bin_mtime': mod_mtime
}
}
}
self.check_generator_output(
extra_args=['--state-file', '-',
'--name-pattern', 'mod'],
gen_version='0.1',
custom_required_gen=True,
input=json.dumps(state)
)
def test_new_modules_are_added_to_state_json(self):
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.1',
'status': 'GENERATED',
},
}
}
self.check_generator_output(input=json.dumps(state),
custom_required_gen=True,
gen_version='0.1')
def test_not_found_modules_are_removed_from_state_json(self):
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.1',
'status': 'GENERATED',
},
'mod2': {
'gen_version': '0.1',
'status': 'GENERATED',
}
}
}
self.check_generator_output(input=json.dumps(state), gen_version='0.1', custom_required_gen=True)
def test_only_leaving_state_file_no_read(self):
self.check_generator_output(extra_args=['--init-state-file',
'--name-pattern', 'mod?'])
def test_state_indication_for_builtin_module(self):
result = self.run_generator(extra_args=['--init-state-file',
'--name-pattern', '_ast'])
self.assertIsNotNone(result.state_json)
self.assertIn('_ast', result.state_json['sdk_skeletons'])
self.assertEqual('GENERATED', result.state_json['sdk_skeletons']['_ast']['status'])
def test_modification_time_left_in_state_json_for_new_binaries(self):
result = self.run_generator(extra_args=['--init-state-file',
'--name-pattern', 'mod'],
extra_env={ENV_TEST_MODE_FLAG: None})
self.assertIsNotNone(result.state_json)
self.assertIn('bin_mtime', result.state_json['sdk_skeletons']['mod'])
def test_state_json_for_cached_skeletons_retains_original_gen_version(self):
# For mod1 we have a newer version in the cache.
# For non-existing mod2 we have satisfying version in the cache.
state = {
'sdk_skeletons': {
'mod1': {
'gen_version': '0.1',
'status': 'GENERATED',
}
}
}
self.check_generator_output(gen_version='0.3',
custom_required_gen=True,
input=json.dumps(state))
def test_state_json_for_up_to_date_skeletons_retains_original_gen_version(self):
self.check_generator_output(extra_args=['--init-state-file',
'--name-pattern', 'mod'],
gen_version='0.2',
custom_required_gen=True)
def test_state_json_accepted_as_path_to_file(self):
self.check_generator_output(
extra_args=[
'--state-file', self.resolve_in_test_data('.state.json'),
'--name-pattern', 'mod*',
],
)