from dataclasses import Field, dataclass, fields, is_dataclass from typing import Any, get_args import pytest from frostfs_testlib.load.load_config import EndpointSelectionStrategy, LoadParams, LoadScenario, LoadType, Preset, ReadFrom from frostfs_testlib.load.runners import DefaultRunner from frostfs_testlib.resources.load_params import BACKGROUND_LOAD_DEFAULT_VU_INIT_TIME from frostfs_testlib.storage.cluster import ClusterNode from frostfs_testlib.storage.controllers.background_load_controller import BackgroundLoadController from frostfs_testlib.storage.dataclasses.frostfs_services import StorageNode from frostfs_testlib.storage.dataclasses.node_base import NodeBase @dataclass class MetaTestField: field: Field field_type: type instance: Any class TestLoadConfig: @pytest.fixture def set_empty(self, request: pytest.FixtureRequest): # Workaround for verify if "param" in request.__dict__ and request.param: return request.param return False @pytest.fixture def load_type(self, request: pytest.FixtureRequest): # Workaround for verify if "param" in request.__dict__ and request.param: return request.param return None @pytest.fixture def load_params(self, load_type: LoadType, set_empty: bool, request: pytest.FixtureRequest): load_scenario = request.param return self._get_filled_load_params(load_type, load_scenario, set_empty) def test_load_params_only_load_type_required(self): load_params = LoadParams(load_type=LoadType.S3) expected = "s3" assert repr(load_params) == expected assert f"{load_params}" == expected def test_load_params_init_time(self): load_params = LoadParams(load_type=LoadType.S3) vus = 100 load_params.vu_init_time = BACKGROUND_LOAD_DEFAULT_VU_INIT_TIME # Used in time calculations load_params.readers = vus load_params.writers = vus load_params.preallocated_readers = vus load_params.preallocated_writers = vus # Not used in time calculations load_params.deleters = vus load_params.preallocated_deleters = vus expected = vus * 4 * BACKGROUND_LOAD_DEFAULT_VU_INIT_TIME actual = load_params.get_init_time() assert actual == expected, "Incorrect time for get_init_time()" def test_load_params_initially_have_all_values_none(self): load_params = LoadParams(load_type=LoadType.S3) self._check_all_values_none(load_params, ["load_type", "scenario"]) def test_preset_initially_have_all_values_none(self): preset = Preset() self._check_all_values_none(preset) @pytest.mark.parametrize("load_params", [LoadScenario.S3_CAR], indirect=True) def test_string_representation_s3_car(self, load_params: LoadParams): load_params.object_size = 524288 expected = "s3_car 512 MiB, write_rate=10, read_rate=9, delete_rate=11, preallocated_writers=20, preallocated_readers=20, preallocated_deleters=21" assert f"{load_params}" == expected assert repr(load_params) == expected @pytest.mark.parametrize("load_params", [LoadScenario.gRPC], indirect=True) def test_string_representation_grpc(self, load_params: LoadParams): load_params.object_size = 512 expected = "grpc 512 KiB, writers=7, readers=7, deleters=8" assert f"{load_params}" == expected assert repr(load_params) == expected @pytest.mark.parametrize("load_params", [LoadScenario.gRPC], indirect=True) def test_load_controller_string_representation(self, load_params: LoadParams): load_params.endpoint_selection_strategy = EndpointSelectionStrategy.ALL load_params.object_size = 512 background_load_controller = BackgroundLoadController("tmp", load_params, None, None, DefaultRunner(None)) expected = "grpc 512 KiB, writers=7, readers=7, deleters=8" assert f"{background_load_controller}" == expected assert repr(background_load_controller) == expected def test_load_set_id_changes_fields(self): load_params = LoadParams(load_type=LoadType.S3) load_params.preset = Preset() load_params.read_from = ReadFrom["REGISTRY"] load_params.working_dir = "/tmp" load_params.set_id("test_id") assert load_params.registry_file == "/tmp/test_id_registry.bolt" assert load_params.preset.pregen_json == "/tmp/test_id_prepare.json" assert load_params.load_id == "test_id" # No other values should be changed self._check_all_values_none( load_params, [ "load_type", "working_dir", "load_id", "registry_file", "preset", "scenario", "read_from", ], ) self._check_all_values_none(load_params.preset, ["pregen_json", "scenario"]) @pytest.mark.parametrize("load_params", [LoadScenario.gRPC], indirect=True) def test_argument_parsing_for_grpc_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '11'", "--acl 'acl'", "--preload_obj '13'", "--out 'pregen_json'", "--workers '7'", "--containers '16'", "--policy 'container_placement_policy' --policy 'container_placement_policy_2'", "--ignore-errors", "--sleep '19'", "--local", ] expected_env_vars = { "DURATION": 9, "WRITE_OBJ_SIZE": 11, "REGISTRY_FILE": "registry_file", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "WRITERS": 7, "READERS": 7, "DELETERS": 8, "READ_AGE": 8, "STREAMING": 9, "K6_OUT": "output", "PREGEN_JSON": "pregen_json", "PREPARE_LOCALLY": True, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.gRPC_CAR], indirect=True) def test_argument_parsing_for_grpc_car_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '11'", "--preload_obj '13'", "--out 'pregen_json'", "--workers '7'", "--containers '16'", "--policy 'container_placement_policy' --policy 'container_placement_policy_2'", "--ignore-errors", "--sleep '19'", "--local", "--acl 'acl'", ] expected_env_vars = { "DURATION": 9, "WRITE_OBJ_SIZE": 11, "K6_OUT": "output", "REGISTRY_FILE": "registry_file", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "MAX_WRITERS": 11, "MAX_READERS": 11, "MAX_DELETERS": 12, "PRE_ALLOC_DELETERS": 21, "PRE_ALLOC_READERS": 20, "PRE_ALLOC_WRITERS": 20, "PREGEN_JSON": "pregen_json", "TIME_UNIT": "time_unit", "WRITE_RATE": 10, "READ_RATE": 9, "READ_AGE": 8, "DELETE_RATE": 11, "STREAMING": 9, "PREPARE_LOCALLY": True, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.S3], indirect=True) def test_argument_parsing_for_s3_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '11'", "--preload_obj '13'", "--no-verify-ssl", "--out 'pregen_json'", "--workers '7'", "--buckets '13'", "--location 's3_location' --location 's3_location_2'", "--ignore-errors", "--sleep '19'", "--acl 'acl'", ] expected_env_vars = { "DURATION": 9, "WRITE_OBJ_SIZE": 11, "REGISTRY_FILE": "registry_file", "K6_OUT": "output", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "WRITERS": 7, "READERS": 7, "DELETERS": 8, "READ_AGE": 8, "STREAMING": 9, "NO_VERIFY_SSL": True, "PREGEN_JSON": "pregen_json", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.S3_CAR], indirect=True) def test_argument_parsing_for_s3_car_scenario_with_stringed_time(self, load_params: LoadParams): load_params.load_time = "2d3h5min" expected_preset_args = [ "--size '11'", "--preload_obj '13'", "--no-verify-ssl", "--out 'pregen_json'", "--workers '7'", "--buckets '13'", "--location 's3_location' --location 's3_location_2'", "--ignore-errors", "--sleep '19'", "--acl 'acl'", ] expected_env_vars = { "DURATION": 183900, "WRITE_OBJ_SIZE": 11, "REGISTRY_FILE": "registry_file", "K6_OUT": "output", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "NO_VERIFY_SSL": True, "MAX_WRITERS": 11, "MAX_READERS": 11, "MAX_DELETERS": 12, "PRE_ALLOC_DELETERS": 21, "PRE_ALLOC_READERS": 20, "PRE_ALLOC_WRITERS": 20, "PREGEN_JSON": "pregen_json", "TIME_UNIT": "time_unit", "WRITE_RATE": 10, "READ_RATE": 9, "READ_AGE": 8, "STREAMING": 9, "DELETE_RATE": 11, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.S3_CAR], indirect=True) def test_argument_parsing_for_s3_car_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '11'", "--preload_obj '13'", "--no-verify-ssl", "--out 'pregen_json'", "--workers '7'", "--buckets '13'", "--location 's3_location' --location 's3_location_2'", "--ignore-errors", "--sleep '19'", "--acl 'acl'", ] expected_env_vars = { "DURATION": 9, "WRITE_OBJ_SIZE": 11, "REGISTRY_FILE": "registry_file", "K6_OUT": "output", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "NO_VERIFY_SSL": True, "MAX_WRITERS": 11, "MAX_READERS": 11, "MAX_DELETERS": 12, "PRE_ALLOC_DELETERS": 21, "PRE_ALLOC_READERS": 20, "PRE_ALLOC_WRITERS": 20, "PREGEN_JSON": "pregen_json", "TIME_UNIT": "time_unit", "WRITE_RATE": 10, "READ_RATE": 9, "READ_AGE": 8, "STREAMING": 9, "DELETE_RATE": 11, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.HTTP], indirect=True) def test_argument_parsing_for_http_scenario(self, load_params: LoadParams): load_params.preset.local = False expected_preset_args = [ "--no-verify-ssl", "--size '11'", "--preload_obj '13'", "--out 'pregen_json'", "--workers '7'", "--containers '16'", "--policy 'container_placement_policy' --policy 'container_placement_policy_2'", "--ignore-errors", "--sleep '19'", "--acl 'acl'", ] expected_env_vars = { "DURATION": 9, "WRITE_OBJ_SIZE": 11, "K6_OUT": "output", "NO_VERIFY_SSL": True, "REGISTRY_FILE": "registry_file", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "WRITERS": 7, "READERS": 7, "DELETERS": 8, "READ_AGE": 8, "STREAMING": 9, "PREGEN_JSON": "pregen_json", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params", [LoadScenario.LOCAL], indirect=True) def test_argument_parsing_for_local_scenario(self, load_params: LoadParams): load_params.preset.local = False expected_preset_args = [ "--size '11'", "--preload_obj '13'", "--out 'pregen_json'", "--workers '7'", "--containers '16'", "--policy 'container_placement_policy' --policy 'container_placement_policy_2'", "--ignore-errors", "--sleep '19'", "--acl 'acl'", ] expected_env_vars = { "CONFIG_DIR": "config_dir", "CONFIG_FILE": "config_file", "DURATION": 9, "WRITE_OBJ_SIZE": 11, "K6_OUT": "output", "REGISTRY_FILE": "registry_file", "K6_MIN_ITERATION_DURATION": "min_iteration_duration", "K6_SETUP_TIMEOUT": "setup_timeout", "WRITERS": 7, "READERS": 7, "DELETERS": 8, "READ_AGE": 8, "STREAMING": 9, "MAX_TOTAL_SIZE_GB": 17, "PREGEN_JSON": "pregen_json", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize( "input, value, params", [ (["A C ", " B"], ["A C", "B"], [f"--policy 'A C' --policy 'B'"]), (" A ", ["A"], ["--policy 'A'"]), (" A , B ", ["A , B"], ["--policy 'A , B'"]), ([" A", "B "], ["A", "B"], ["--policy 'A' --policy 'B'"]), (None, None, []), ], ) def test_grpc_list_parsing_formatter(self, input, value, params): load_params = LoadParams(LoadType.gRPC) load_params.preset = Preset() load_params.preset.container_placement_policy = input assert load_params.preset.container_placement_policy == value self._check_preset_params(load_params, params) @pytest.mark.parametrize( "input, value, params", [ (["A C ", " B"], ["A C", "B"], [f"--location 'A C' --location 'B'"]), (" A ", ["A"], ["--location 'A'"]), (" A , B ", ["A , B"], ["--location 'A , B'"]), ([" A", "B "], ["A", "B"], ["--location 'A' --location 'B'"]), (None, None, []), ], ) def test_s3_list_parsing_formatter(self, input, value, params): load_params = LoadParams(LoadType.S3) load_params.preset = Preset() load_params.preset.s3_location = input assert load_params.preset.s3_location == value self._check_preset_params(load_params, params) @pytest.mark.parametrize("load_params, load_type", [(LoadScenario.VERIFY, LoadType.S3)], indirect=True) def test_argument_parsing_for_s3_verify_scenario(self, load_params: LoadParams): expected_env_vars = { "CLIENTS": 14, "REGISTRY_FILE": "registry_file", "K6_SETUP_TIMEOUT": "setup_timeout", "NO_VERIFY_SSL": True, "TIME_LIMIT": 11, } self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, load_type", [(LoadScenario.VERIFY, LoadType.gRPC)], indirect=True) def test_argument_parsing_for_grpc_verify_scenario(self, load_params: LoadParams): expected_env_vars = { "CLIENTS": 14, "REGISTRY_FILE": "registry_file", "K6_SETUP_TIMEOUT": "setup_timeout", "NO_VERIFY_SSL": True, "TIME_LIMIT": 11, } self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.gRPC, True)], indirect=True) def test_empty_argument_parsing_for_grpc_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--containers '0'", "--policy ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "DURATION": 0, "WRITE_OBJ_SIZE": 0, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "WRITERS": 0, "READERS": 0, "DELETERS": 0, "READ_AGE": 0, "STREAMING": 0, "PREGEN_JSON": "", "PREPARE_LOCALLY": False, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.gRPC_CAR, True)], indirect=True) def test_empty_argument_parsing_for_grpc_car_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--containers '0'", "--policy ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "DURATION": 0, "WRITE_OBJ_SIZE": 0, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "MAX_WRITERS": 0, "MAX_READERS": 0, "MAX_DELETERS": 0, "PRE_ALLOC_DELETERS": 0, "PRE_ALLOC_READERS": 0, "PRE_ALLOC_WRITERS": 0, "PREGEN_JSON": "", "TIME_UNIT": "", "WRITE_RATE": 0, "READ_RATE": 0, "DELETE_RATE": 0, "READ_AGE": 0, "STREAMING": 0, "PREPARE_LOCALLY": False, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.S3, True)], indirect=True) def test_empty_argument_parsing_for_s3_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--buckets '0'", "--location ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "DURATION": 0, "WRITE_OBJ_SIZE": 0, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "WRITERS": 0, "READERS": 0, "DELETERS": 0, "READ_AGE": 0, "STREAMING": 0, "NO_VERIFY_SSL": False, "PREGEN_JSON": "", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.S3_CAR, True)], indirect=True) def test_empty_argument_parsing_for_s3_car_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--buckets '0'", "--location ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "DURATION": 0, "WRITE_OBJ_SIZE": 0, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "NO_VERIFY_SSL": False, "MAX_WRITERS": 0, "MAX_READERS": 0, "MAX_DELETERS": 0, "PRE_ALLOC_DELETERS": 0, "PRE_ALLOC_READERS": 0, "PRE_ALLOC_WRITERS": 0, "PREGEN_JSON": "", "TIME_UNIT": "", "WRITE_RATE": 0, "READ_RATE": 0, "DELETE_RATE": 0, "READ_AGE": 0, "STREAMING": 0, } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.HTTP, True)], indirect=True) def test_empty_argument_parsing_for_http_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--containers '0'", "--policy ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "DURATION": 0, "WRITE_OBJ_SIZE": 0, "NO_VERIFY_SSL": False, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "WRITERS": 0, "READERS": 0, "DELETERS": 0, "READ_AGE": 0, "STREAMING": 0, "PREGEN_JSON": "", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize("load_params, set_empty", [(LoadScenario.LOCAL, True)], indirect=True) def test_empty_argument_parsing_for_local_scenario(self, load_params: LoadParams): expected_preset_args = [ "--size '0'", "--preload_obj '0'", "--out ''", "--workers '0'", "--containers '0'", "--policy ''", "--sleep '0'", "--acl ''", ] expected_env_vars = { "CONFIG_DIR": "", "CONFIG_FILE": "", "DURATION": 0, "WRITE_OBJ_SIZE": 0, "REGISTRY_FILE": "", "K6_OUT": "", "K6_MIN_ITERATION_DURATION": "", "K6_SETUP_TIMEOUT": "", "MAX_TOTAL_SIZE_GB": 0, "WRITERS": 0, "READERS": 0, "DELETERS": 0, "READ_AGE": 0, "STREAMING": 0, "PREGEN_JSON": "", } self._check_preset_params(load_params, expected_preset_args) self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize( "load_params, load_type, set_empty", [(LoadScenario.VERIFY, LoadType.S3, True)], indirect=True, ) def test_empty_argument_parsing_for_s3_verify_scenario(self, load_params: LoadParams): expected_env_vars = { "CLIENTS": 0, "REGISTRY_FILE": "", "K6_SETUP_TIMEOUT": "", "NO_VERIFY_SSL": False, "TIME_LIMIT": 0, } self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize( "load_params, load_type, set_empty", [(LoadScenario.VERIFY, LoadType.gRPC, True)], indirect=True, ) def test_argument_parsing_for_grpc_verify_scenario(self, load_params: LoadParams): expected_env_vars = { "CLIENTS": 0, "REGISTRY_FILE": "", "K6_SETUP_TIMEOUT": "", "NO_VERIFY_SSL": False, "TIME_LIMIT": 0, } self._check_env_vars(load_params, expected_env_vars) @pytest.mark.parametrize( "load_params, load_type", [(LoadScenario.gRPC, LoadType.gRPC)], indirect=True, ) @pytest.mark.parametrize( "load_time, expected_seconds", [ (300, 300), ("2d3h45min", 186300), ("1d6h", 108000), ("1d", 86400), ("1d1min", 86460), ("2h", 7200), ("2h2min", 7320), ], ) def test_convert_time_to_seconds(self, load_params: LoadParams, load_time: str | int, expected_seconds: int): load_params.load_time = load_time assert load_params.load_time == expected_seconds def _check_preset_params(self, load_params: LoadParams, expected_preset_args: list[str]): preset_parameters = load_params.get_preset_arguments() assert sorted(preset_parameters) == sorted(expected_preset_args) def _check_env_vars(self, load_params: LoadParams, expected_env_vars: dict[str, str]): env_vars = load_params.get_k6_vars() assert env_vars == expected_env_vars def _check_all_values_none(self, dataclass, skip_fields=None): if skip_fields is None: skip_fields = [] dataclass_fields = [field for field in fields(dataclass) if field.name not in skip_fields] for field in dataclass_fields: value = getattr(dataclass, field.name) assert value is None, f"{field.name} is not None" def _check_all_values_not_none(self, dataclass, skip_fields=None): if skip_fields is None: skip_fields = [] dataclass_fields = [field for field in fields(dataclass) if field.name not in skip_fields] for field in dataclass_fields: value = getattr(dataclass, field.name) assert value is not None, f"{field.name} is not None" def _get_filled_load_params(self, load_type: LoadType, load_scenario: LoadScenario, set_emtpy: bool = False) -> LoadParams: load_type_map = { LoadScenario.S3: LoadType.S3, LoadScenario.S3_CAR: LoadType.S3, LoadScenario.gRPC: LoadType.gRPC, LoadScenario.gRPC_CAR: LoadType.gRPC, LoadScenario.LOCAL: LoadType.gRPC, LoadScenario.HTTP: LoadType.HTTP, } load_type = load_type_map[load_scenario] if not load_type else load_type load_params = LoadParams(load_type) load_params.scenario = load_scenario load_params.preset = Preset() meta_fields = self._get_meta_fields(load_params) for field in meta_fields: if getattr(field.instance, field.field.name) is None and load_params.scenario in field.field.metadata["applicable_scenarios"]: value_to_set_map = { int: 0 if set_emtpy else len(field.field.name), float: 0 if set_emtpy else len(field.field.name), str: "" if set_emtpy else field.field.name, list[str]: "" if set_emtpy else [field.field.name, f"{field.field.name}_2"], bool: False if set_emtpy else True, } value_to_set = value_to_set_map[field.field_type] setattr(field.instance, field.field.name, value_to_set) return load_params def _get_actual_field_type(self, field: Field) -> type: return get_args(field.type)[0] if len(get_args(field.type)) else get_args(field.type) def _get_meta_fields(self, instance): data_fields = fields(instance) fields_with_data = [MetaTestField(field, self._get_actual_field_type(field), instance) for field in data_fields if field.metadata] for field in data_fields: actual_field_type = self._get_actual_field_type(field) if is_dataclass(actual_field_type) and getattr(instance, field.name): fields_with_data += self._get_meta_fields(getattr(instance, field.name)) return fields_with_data or []