apitestgen 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apitestgen-1.0.1/PKG-INFO +71 -0
- apitestgen-1.0.1/README.md +46 -0
- apitestgen-1.0.1/apitestgen/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/core/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/core/base_generator.py +76 -0
- apitestgen-1.0.1/apitestgen/core/config.py +79 -0
- apitestgen-1.0.1/apitestgen/core/format_generator.py +108 -0
- apitestgen-1.0.1/apitestgen/core/generators/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/core/generators/array_generator.py +144 -0
- apitestgen-1.0.1/apitestgen/core/generators/boolean_generator.py +46 -0
- apitestgen-1.0.1/apitestgen/core/generators/integer_generator.py +127 -0
- apitestgen-1.0.1/apitestgen/core/generators/null_generator.py +33 -0
- apitestgen-1.0.1/apitestgen/core/generators/number_generator.py +111 -0
- apitestgen-1.0.1/apitestgen/core/generators/object_generator.py +60 -0
- apitestgen-1.0.1/apitestgen/core/generators/string_generator.py +194 -0
- apitestgen-1.0.1/apitestgen/core/ir.py +55 -0
- apitestgen-1.0.1/apitestgen/core/meta.py +37 -0
- apitestgen-1.0.1/apitestgen/formats/__init__.py +95 -0
- apitestgen-1.0.1/apitestgen/formats/json_schema/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/formats/json_schema/adapter.py +114 -0
- apitestgen-1.0.1/apitestgen/formats/json_schema/generator.py +25 -0
- apitestgen-1.0.1/apitestgen/formats/json_schema/logic.py +225 -0
- apitestgen-1.0.1/apitestgen/formats/openapi/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/formats/openapi/adapter.py +64 -0
- apitestgen-1.0.1/apitestgen/formats/openapi/generator.py +8 -0
- apitestgen-1.0.1/apitestgen/formats/openapi/logic.py +64 -0
- apitestgen-1.0.1/apitestgen/negative/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/negative/modifier.py +186 -0
- apitestgen-1.0.1/apitestgen/negative/result.py +167 -0
- apitestgen-1.0.1/apitestgen/negative/tag.py +70 -0
- apitestgen-1.0.1/apitestgen/orchestration/__init__.py +0 -0
- apitestgen-1.0.1/apitestgen/orchestration/multi_scene.py +77 -0
- apitestgen-1.0.1/apitestgen/orchestration/negative_runner.py +143 -0
- apitestgen-1.0.1/apitestgen/orchestration/validator.py +76 -0
- apitestgen-1.0.1/apitestgen/orchestration/variant_generator.py +256 -0
- apitestgen-1.0.1/apitestgen.egg-info/PKG-INFO +71 -0
- apitestgen-1.0.1/apitestgen.egg-info/SOURCES.txt +40 -0
- apitestgen-1.0.1/apitestgen.egg-info/dependency_links.txt +1 -0
- apitestgen-1.0.1/apitestgen.egg-info/requires.txt +2 -0
- apitestgen-1.0.1/apitestgen.egg-info/top_level.txt +1 -0
- apitestgen-1.0.1/setup.cfg +4 -0
- apitestgen-1.0.1/setup.py +25 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apitestgen
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: 智能测试数据生成器 - 支持 JSON Schema / OpenAPI 的正向和负向测试用例生成
|
|
5
|
+
Home-page: https://github.com/your/apitestgen
|
|
6
|
+
Author: 盼盼
|
|
7
|
+
Author-email: 2063769412@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Software Development :: Testing
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
15
|
+
Requires-Dist: rstr>=3.0.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# apitestgen
|
|
27
|
+
|
|
28
|
+
智能 API 测试数据生成器,支持 JSON Schema / OpenAPI 的正向和负向测试用例生成。
|
|
29
|
+
|
|
30
|
+
## 安装
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install apitestgen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
##快速开始
|
|
37
|
+
|
|
38
|
+
from apitestgen.formats import create_generator
|
|
39
|
+
|
|
40
|
+
schema = {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["name", "age"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"name": {"type": "string", "minLength": 2, "maxLength": 10},
|
|
45
|
+
"age": {"type": "integer", "minimum": 0, "maximum": 150}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
gen = create_generator(schema)
|
|
50
|
+
|
|
51
|
+
# 正向生成
|
|
52
|
+
print(gen.generate())
|
|
53
|
+
|
|
54
|
+
# 多场景
|
|
55
|
+
for v in gen.generates():
|
|
56
|
+
print(v["label"], v["data"])
|
|
57
|
+
|
|
58
|
+
# 负向测试
|
|
59
|
+
for case in gen.generate_negative():
|
|
60
|
+
print(case.sub_case_id, case.data)
|
|
61
|
+
|
|
62
|
+
## 特性
|
|
63
|
+
正向单次/多场景数据生成
|
|
64
|
+
|
|
65
|
+
负向测试用例生成(边界值、类型错误、枚举违反等)
|
|
66
|
+
|
|
67
|
+
Schema 校验
|
|
68
|
+
|
|
69
|
+
组合策略(single/pairwise/combinatorial)
|
|
70
|
+
|
|
71
|
+
支持 JSON Schema 和 OpenAPI
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# apitestgen
|
|
2
|
+
|
|
3
|
+
智能 API 测试数据生成器,支持 JSON Schema / OpenAPI 的正向和负向测试用例生成。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install apitestgen
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
##快速开始
|
|
12
|
+
|
|
13
|
+
from apitestgen.formats import create_generator
|
|
14
|
+
|
|
15
|
+
schema = {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["name", "age"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"name": {"type": "string", "minLength": 2, "maxLength": 10},
|
|
20
|
+
"age": {"type": "integer", "minimum": 0, "maximum": 150}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
gen = create_generator(schema)
|
|
25
|
+
|
|
26
|
+
# 正向生成
|
|
27
|
+
print(gen.generate())
|
|
28
|
+
|
|
29
|
+
# 多场景
|
|
30
|
+
for v in gen.generates():
|
|
31
|
+
print(v["label"], v["data"])
|
|
32
|
+
|
|
33
|
+
# 负向测试
|
|
34
|
+
for case in gen.generate_negative():
|
|
35
|
+
print(case.sub_case_id, case.data)
|
|
36
|
+
|
|
37
|
+
## 特性
|
|
38
|
+
正向单次/多场景数据生成
|
|
39
|
+
|
|
40
|
+
负向测试用例生成(边界值、类型错误、枚举违反等)
|
|
41
|
+
|
|
42
|
+
Schema 校验
|
|
43
|
+
|
|
44
|
+
组合策略(single/pairwise/combinatorial)
|
|
45
|
+
|
|
46
|
+
支持 JSON Schema 和 OpenAPI
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# api/core/generator.py
|
|
2
|
+
"""基础生成器 - 工具方法"""
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Optional, Tuple, List
|
|
7
|
+
|
|
8
|
+
from .meta import GeneratorMeta
|
|
9
|
+
from .config import GeneratorConfig
|
|
10
|
+
from apitestgen.negative.result import FieldNegativeValue
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseGenerator(metaclass=GeneratorMeta):
|
|
15
|
+
"""基础生成器:工具方法"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, field, config: Optional[GeneratorConfig] = None, depth: int = 0, **kwargs):
|
|
18
|
+
self.field = field
|
|
19
|
+
self.config = config or GeneratorConfig
|
|
20
|
+
self.depth = depth
|
|
21
|
+
|
|
22
|
+
def generate(self) -> Any:
|
|
23
|
+
if self.depth > self.config.MAX_DEPTH:
|
|
24
|
+
return None
|
|
25
|
+
return self._default_value()
|
|
26
|
+
|
|
27
|
+
def _default_value(self) -> Any:
|
|
28
|
+
return self.config.TYPE_DEFAULTS.get(self.field.type, None)
|
|
29
|
+
|
|
30
|
+
def _should_generate_optional(self, rate: float) -> bool:
|
|
31
|
+
return random.random() < rate
|
|
32
|
+
|
|
33
|
+
def _make(self, value) -> Any:
|
|
34
|
+
return FieldNegativeValue(value=value)
|
|
35
|
+
|
|
36
|
+
def parse_numeric_bounds(self) -> Tuple[Optional[float], Optional[float], bool, bool]:
|
|
37
|
+
return (
|
|
38
|
+
self.field.minimum,
|
|
39
|
+
self.field.maximum,
|
|
40
|
+
bool(self.field.exclusive_minimum),
|
|
41
|
+
bool(self.field.exclusive_maximum),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def generate_field_negative(self, tags: Optional[List[str]] = None) -> List:
|
|
45
|
+
"""自动发现 @negative_case 方法"""
|
|
46
|
+
if not self.config.enable_negative:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
from apitestgen.negative.tag import NegativeTag
|
|
50
|
+
|
|
51
|
+
cases = []
|
|
52
|
+
tag_set = set(tags) if tags else None
|
|
53
|
+
|
|
54
|
+
for attr_name in dir(self):
|
|
55
|
+
method = getattr(self, attr_name, None)
|
|
56
|
+
if not callable(method):
|
|
57
|
+
continue
|
|
58
|
+
if not getattr(method, '_is_negative_method', False):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
tag = method._negative_tag
|
|
62
|
+
if tag_set and tag not in tag_set:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
result = method()
|
|
67
|
+
if result and isinstance(result, list):
|
|
68
|
+
for fv in result:
|
|
69
|
+
fv.sub_case_id = tag
|
|
70
|
+
fv.description = getattr(method, '_negative_description', tag)
|
|
71
|
+
fv.expected_error_type = NegativeTag.error(tag)
|
|
72
|
+
cases.extend(result)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.warning(f"{self.__class__.__name__}.{attr_name} 失败: {e}")
|
|
75
|
+
|
|
76
|
+
return cases
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# api/core/config.py
|
|
2
|
+
"""生成器通用配置"""
|
|
3
|
+
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GeneratorConfig:
|
|
8
|
+
"""生成器通用配置"""
|
|
9
|
+
|
|
10
|
+
MAX_DEPTH: int = 10
|
|
11
|
+
OPTIONAL_RATE: float = 1.0 # 1.0=所有字段, 0.0=仅必填
|
|
12
|
+
|
|
13
|
+
GENERATE_MODE: str = "single" # single | combinatorial
|
|
14
|
+
EXPAND_BOOLEAN: bool = True # 展开布尔 True/False
|
|
15
|
+
EXPAND_ENUM: bool = True # 展开枚举所有值
|
|
16
|
+
EXPAND_TUPLE: bool = True # 展开元组数组
|
|
17
|
+
EXPAND_NESTED_REQUIRED: bool = True # 展开嵌套必填
|
|
18
|
+
MAX_COMBINATIONS: int = 100 # 最大组合数
|
|
19
|
+
INCLUDE_EMPTY: bool = True # 输出空对象/空数组
|
|
20
|
+
# ========== 组合策略 ==========
|
|
21
|
+
COMBINE_MODE: str = "single" # single | pairwise | combinatorial
|
|
22
|
+
|
|
23
|
+
# ========== 校验 ==========
|
|
24
|
+
ENABLE_VALIDATION: bool = False # 是否启用 Schema 校验
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# 字符串
|
|
28
|
+
DEFAULT_MIN_LENGTH: int = 0
|
|
29
|
+
DEFAULT_MAX_LENGTH: int = 100
|
|
30
|
+
STRING_CHARSET: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
31
|
+
DEFAULT_STRING_MAX_LEN: int = 50
|
|
32
|
+
DEFAULT_PATTERN_STRING_MAX_LEN: int = 256
|
|
33
|
+
MAX_STRING_LENGTH_INCREMENT: int = 20
|
|
34
|
+
AX_PATTERN_ATTEMPTS: int = 100
|
|
35
|
+
INVALID_CHARSET: str = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 数组
|
|
39
|
+
DEFAULT_MIN_ITEMS: int = 0
|
|
40
|
+
DEFAULT_MAX_ITEMS: int = 10
|
|
41
|
+
EXPAND_BOOLEAN: bool = True
|
|
42
|
+
EXPAND_TUPLE_ARRAY: bool = True
|
|
43
|
+
EXPAND_NESTED_REQUIRED: bool = True # 嵌套必填
|
|
44
|
+
EXPAND_IF_THEN_ELSE: bool = True # if-then-else 分支
|
|
45
|
+
EXPAND_ONEOF: bool = True # oneOf 分支
|
|
46
|
+
|
|
47
|
+
# 数值
|
|
48
|
+
DEFAULT_INT_MIN: int = -10000
|
|
49
|
+
DEFAULT_INT_MAX: int = 10000
|
|
50
|
+
DEFAULT_NUM_MIN: float = -1000000.0
|
|
51
|
+
DEFAULT_NUM_MAX: float = 1000000.0
|
|
52
|
+
DEFAULT_PRECISION: int = 10
|
|
53
|
+
|
|
54
|
+
# 精度
|
|
55
|
+
EPSILON: float = 1e-9
|
|
56
|
+
|
|
57
|
+
# 负向测试
|
|
58
|
+
enable_negative: bool = True
|
|
59
|
+
max_cases_per_tag: int = 1
|
|
60
|
+
boundary_offset_integer: int = 1
|
|
61
|
+
boundary_offset_number: float = 0.1
|
|
62
|
+
boundary_offset_string: int = 1
|
|
63
|
+
boundary_offset_array: int = 1
|
|
64
|
+
|
|
65
|
+
# 类型默认值
|
|
66
|
+
TYPE_DEFAULTS: Dict[str, Any] = {
|
|
67
|
+
'string': '', 'integer': 0, 'number': 0.0,
|
|
68
|
+
'boolean': False, 'array': [], 'object': {}, 'null': None,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
WRONG_TYPE_MAP: Dict[str, list] = {
|
|
72
|
+
'string': [123, 1.5, True, [], {}, None],
|
|
73
|
+
'integer': ["not_int", 1.5, True, [], {}, None],
|
|
74
|
+
'number': ["not_num", True, [], {}, None],
|
|
75
|
+
'boolean': ["not_bool", 0, 1, [], {}, None],
|
|
76
|
+
'array': ["not_array", 123, 1.5, True, {}, None],
|
|
77
|
+
'object': ["not_obj", 123, 1.5, True, [], None],
|
|
78
|
+
'null': ["not_null", 0, 1.5, True, [], {}],
|
|
79
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# api/formats/json_schema/format_generator.py
|
|
2
|
+
"""格式生成器 - 基于 Faker 生成 format 数据"""
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from faker import Faker
|
|
10
|
+
FAKER_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
FAKER_AVAILABLE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FormatGenerator:
|
|
16
|
+
"""格式生成器"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._faker = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def faker(self):
|
|
23
|
+
if self._faker is None and FAKER_AVAILABLE:
|
|
24
|
+
self._faker = Faker()
|
|
25
|
+
return self._faker
|
|
26
|
+
|
|
27
|
+
def generate(self, format_name: str) -> Optional[str]:
|
|
28
|
+
handlers = {
|
|
29
|
+
'email': self._email,
|
|
30
|
+
'uri': self._uri,
|
|
31
|
+
'url': self._url,
|
|
32
|
+
'date': self._date,
|
|
33
|
+
'date-time': self._date_time,
|
|
34
|
+
'time': self._time,
|
|
35
|
+
'ipv4': self._ipv4,
|
|
36
|
+
'ipv6': self._ipv6,
|
|
37
|
+
'uuid': self._uuid,
|
|
38
|
+
'hostname': self._hostname,
|
|
39
|
+
'phone': self._phone,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handler = handlers.get(format_name)
|
|
43
|
+
if handler:
|
|
44
|
+
return handler()
|
|
45
|
+
return self._fallback()
|
|
46
|
+
|
|
47
|
+
def _email(self):
|
|
48
|
+
if self.faker:
|
|
49
|
+
return self.faker.email()
|
|
50
|
+
return f"user{random.randint(1,9999)}@example.com"
|
|
51
|
+
|
|
52
|
+
def _uri(self):
|
|
53
|
+
if self.faker:
|
|
54
|
+
return self.faker.uri()
|
|
55
|
+
return f"https://www.example{random.randint(1,99)}.com/path"
|
|
56
|
+
|
|
57
|
+
def _url(self):
|
|
58
|
+
return self._uri()
|
|
59
|
+
|
|
60
|
+
def _date(self):
|
|
61
|
+
if self.faker:
|
|
62
|
+
return self.faker.date()
|
|
63
|
+
return "2024-01-15"
|
|
64
|
+
|
|
65
|
+
def _date_time(self):
|
|
66
|
+
if self.faker:
|
|
67
|
+
return self.faker.date_time().isoformat()
|
|
68
|
+
return "2024-01-15T10:30:00"
|
|
69
|
+
|
|
70
|
+
def _time(self):
|
|
71
|
+
if self.faker:
|
|
72
|
+
return self.faker.time()
|
|
73
|
+
return "10:30:00"
|
|
74
|
+
|
|
75
|
+
def _ipv4(self):
|
|
76
|
+
if self.faker:
|
|
77
|
+
return self.faker.ipv4()
|
|
78
|
+
return "192.168.1.1"
|
|
79
|
+
|
|
80
|
+
def _ipv6(self):
|
|
81
|
+
if self.faker:
|
|
82
|
+
return self.faker.ipv6()
|
|
83
|
+
return "::1"
|
|
84
|
+
|
|
85
|
+
def _uuid(self):
|
|
86
|
+
import uuid
|
|
87
|
+
return str(uuid.uuid4())
|
|
88
|
+
|
|
89
|
+
def _hostname(self):
|
|
90
|
+
if self.faker:
|
|
91
|
+
return self.faker.hostname()
|
|
92
|
+
return "host.example.com"
|
|
93
|
+
|
|
94
|
+
def _phone(self):
|
|
95
|
+
if self.faker:
|
|
96
|
+
return self.faker.phone_number()
|
|
97
|
+
return f"1{random.randint(3000000000, 9999999999)}"
|
|
98
|
+
|
|
99
|
+
def _fallback(self):
|
|
100
|
+
return f"format_{random.randint(1000, 9999)}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# 全局实例
|
|
104
|
+
_format_generator = FormatGenerator()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_format_generator() -> FormatGenerator:
|
|
108
|
+
return _format_generator
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# api/core/generators/array_generator.py
|
|
2
|
+
"""数组生成器:正负向"""
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ..base_generator import BaseGenerator
|
|
9
|
+
from ..meta import GeneratorMeta
|
|
10
|
+
from apitestgen.negative.tag import negative_case
|
|
11
|
+
from apitestgen.negative.result import FieldNegativeValue
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@GeneratorMeta.register('array', alias=['list', 'tuple'])
|
|
17
|
+
class ArrayGenerator(BaseGenerator):
|
|
18
|
+
"""数组:minItems / maxItems / uniqueItems"""
|
|
19
|
+
|
|
20
|
+
# ================================================================
|
|
21
|
+
# 正向
|
|
22
|
+
# ================================================================
|
|
23
|
+
|
|
24
|
+
def generate(self) -> list:
|
|
25
|
+
if self.field.const is not None:
|
|
26
|
+
return self.field.const
|
|
27
|
+
if self.field.enum:
|
|
28
|
+
return random.choice(self.field.enum)
|
|
29
|
+
if not self.field.items:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
lo = self.field.min_items if self.field.min_items is not None else self.config.DEFAULT_MIN_ITEMS
|
|
33
|
+
hi = self.field.max_items if self.field.max_items is not None else self.config.DEFAULT_MAX_ITEMS
|
|
34
|
+
|
|
35
|
+
if lo == 0 and hi > 0:
|
|
36
|
+
lo = 1
|
|
37
|
+
|
|
38
|
+
result = self._generate_items(lo, hi)
|
|
39
|
+
|
|
40
|
+
if self.field.unique_items and result:
|
|
41
|
+
seen = set()
|
|
42
|
+
unique = []
|
|
43
|
+
for item in result:
|
|
44
|
+
key = str(item)
|
|
45
|
+
if key not in seen:
|
|
46
|
+
seen.add(key)
|
|
47
|
+
unique.append(item)
|
|
48
|
+
result = unique
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# ================================================================
|
|
53
|
+
# 通用生成方法
|
|
54
|
+
# ================================================================
|
|
55
|
+
|
|
56
|
+
def _generate_items(self, min_items: int, max_items: int) -> list:
|
|
57
|
+
"""根据 min/max 生成数组元素"""
|
|
58
|
+
if min_items > max_items:
|
|
59
|
+
min_items = max_items
|
|
60
|
+
|
|
61
|
+
if len(self.field.items) > 1:
|
|
62
|
+
# 元组类型:所有位置都生成
|
|
63
|
+
result = []
|
|
64
|
+
for item in self.field.items:
|
|
65
|
+
gen = BaseGenerator(item, config=self.config, depth=self.depth + 1)
|
|
66
|
+
result.append(gen.generate())
|
|
67
|
+
|
|
68
|
+
# 少于 min_items:随机补充
|
|
69
|
+
while len(result) < min_items:
|
|
70
|
+
extra_item = random.choice(self.field.items)
|
|
71
|
+
gen = BaseGenerator(extra_item, config=self.config, depth=self.depth + 1)
|
|
72
|
+
result.append(gen.generate())
|
|
73
|
+
|
|
74
|
+
# 超过 max_items:随机截取
|
|
75
|
+
if len(result) > max_items:
|
|
76
|
+
result = random.sample(result, max_items)
|
|
77
|
+
else:
|
|
78
|
+
# 列表类型:随机长度
|
|
79
|
+
length = random.randint(min_items, max_items)
|
|
80
|
+
item_schema = self.field.items[0]
|
|
81
|
+
result = [
|
|
82
|
+
BaseGenerator(item_schema, config=self.config, depth=self.depth + 1).generate()
|
|
83
|
+
for _ in range(length)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
# ================================================================
|
|
89
|
+
# 负向
|
|
90
|
+
# ================================================================
|
|
91
|
+
|
|
92
|
+
@negative_case("F2_3", "数组元素过少")
|
|
93
|
+
def _too_few(self) -> List[FieldNegativeValue]:
|
|
94
|
+
if self.field.type != 'array':
|
|
95
|
+
return []
|
|
96
|
+
lo = self.field.min_items or 0
|
|
97
|
+
if lo <= 0:
|
|
98
|
+
return []
|
|
99
|
+
if not self.field.items:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
few = self._generate_items(0, max(0, lo - 1))
|
|
103
|
+
return [self._make(few)]
|
|
104
|
+
|
|
105
|
+
@negative_case("F2_4", "数组元素过多")
|
|
106
|
+
def _too_many(self) -> List[FieldNegativeValue]:
|
|
107
|
+
if self.field.type != 'array':
|
|
108
|
+
return []
|
|
109
|
+
hi = self.field.max_items
|
|
110
|
+
if hi is None:
|
|
111
|
+
return []
|
|
112
|
+
if not self.field.items:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
over = hi + self.config.boundary_offset_array
|
|
116
|
+
many = self._generate_items(over, over)
|
|
117
|
+
return [self._make(many)]
|
|
118
|
+
|
|
119
|
+
@negative_case("F4_1", "类型错误")
|
|
120
|
+
def _wrong_type(self) -> List[FieldNegativeValue]:
|
|
121
|
+
vals = self.config.WRONG_TYPE_MAP.get('array', [])
|
|
122
|
+
return [self._make(v) for v in vals[:self.config.max_cases_per_tag]]
|
|
123
|
+
|
|
124
|
+
@negative_case("F6_1", "枚举值违反")
|
|
125
|
+
def _enum_violation(self) -> List[FieldNegativeValue]:
|
|
126
|
+
if not self.field.enum:
|
|
127
|
+
return []
|
|
128
|
+
cases = []
|
|
129
|
+
for _ in range(self.config.max_cases_per_tag):
|
|
130
|
+
valid = random.choice(self.field.enum)
|
|
131
|
+
cases.append(self._make(valid + ["__INVALID__"]))
|
|
132
|
+
return cases
|
|
133
|
+
|
|
134
|
+
@negative_case("F7_1", "数组元素不唯一")
|
|
135
|
+
def _unique_violation(self) -> List[FieldNegativeValue]:
|
|
136
|
+
if not self.field.unique_items:
|
|
137
|
+
return []
|
|
138
|
+
if not self.field.items:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
item = random.choice(self.field.items)
|
|
142
|
+
gen = BaseGenerator(item, config=self.config)
|
|
143
|
+
dup = gen.generate()
|
|
144
|
+
return [self._make([dup, dup])]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# api/core/generators/boolean_generator.py
|
|
2
|
+
"""布尔生成器:正负向"""
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ..base_generator import BaseGenerator
|
|
8
|
+
from ..meta import GeneratorMeta
|
|
9
|
+
from apitestgen.negative.tag import negative_case
|
|
10
|
+
from apitestgen.negative.result import FieldNegativeValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@GeneratorMeta.register('boolean', alias=['bool'])
|
|
14
|
+
class BooleanGenerator(BaseGenerator):
|
|
15
|
+
"""布尔"""
|
|
16
|
+
|
|
17
|
+
def generate(self) -> bool:
|
|
18
|
+
if self.field.const is not None:
|
|
19
|
+
return self.field.const
|
|
20
|
+
if self.field.enum:
|
|
21
|
+
return random.choice(self.field.enum)
|
|
22
|
+
return random.choice([True, False])
|
|
23
|
+
|
|
24
|
+
@negative_case("F4_1", "类型错误")
|
|
25
|
+
def _wrong_type(self) -> List[FieldNegativeValue]:
|
|
26
|
+
vals = self.config.WRONG_TYPE_MAP.get('boolean', [])
|
|
27
|
+
return [self._make(v) for v in vals[:self.config.max_cases_per_tag]]
|
|
28
|
+
|
|
29
|
+
@negative_case("F6_1", "枚举值违反")
|
|
30
|
+
def _enum_violation(self) -> List[FieldNegativeValue]:
|
|
31
|
+
"""不在枚举值范围内"""
|
|
32
|
+
if not self.field.enum:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
cases = []
|
|
36
|
+
for _ in range(self.config.max_cases_per_tag):
|
|
37
|
+
# 布尔枚举特殊处理:取一个不在枚举中的值
|
|
38
|
+
if True not in self.field.enum:
|
|
39
|
+
cases.append(self._make(True))
|
|
40
|
+
elif False not in self.field.enum:
|
|
41
|
+
cases.append(self._make(False))
|
|
42
|
+
else:
|
|
43
|
+
# 两个都在,用非布尔值
|
|
44
|
+
cases.append(self._make("NOT_BOOL"))
|
|
45
|
+
|
|
46
|
+
return cases
|