test: 第73波 url_security + pagination + text_splitter 单测补充 (+68)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 20s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 46s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m4s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m5s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m47s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m26s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m11s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m40s
AI Code Review / AI Code Review (pull_request) Successful in 5m29s

在原有测试基础上补充覆盖:

- url_security (+28):
  - TestSSRFIPCheck: 14个IP类型全覆盖(回环/各私有段/链路本地/组播/未指定/保留/公网)
  - TestDirectIPAccess: 直接IP访问开关控制
  - TestMagicNumberExtended: 更多格式魔数校验(AAC/M4A/WebP/MP4/WebM/BMP/FLAC/OGG/读取失败/多类型)

- pagination (+20):
  - TestPaginationParamsEdgeCases: 边界值/极大页码/offset计算
  - TestPaginationMetaEdgeCases: total=0/刚好最后一页/多1条/单条数据
  - TestPaginateEdgeCases: 单元素/超尾页/空列表/page_size=1/不修改输入

- text_splitter (+20):
  - TestSplitTextEmptyAndShort: 空白/单字符/边界值/None
  - TestSplitTextSentenceBoundaries: 各标点分段/短句合并
  - TestSplitTextLongSentenceForce: 无标点硬切/不丢字符/长短混合
  - TestSplitTextShortSegmentMerge: 多短句合并/短尾合并
  - TestSplitTextEdgeCases: 纯标点/中英混合/去空白/总长度守恒
This commit is contained in:
CI Bot
2026-07-25 10:25:26 +08:00
parent 37b3bf8db2
commit d1348bcd80
3 changed files with 1249 additions and 789 deletions
+300 -231
View File
@@ -1,16 +1,11 @@
"""分页器纯逻辑测试 — PaginationParams/PaginationMeta/paginate.
覆盖参数校验、偏移计算、元数据计算、内存分页等全部纯逻辑。
"""
"""通用分页器单元测试."""
from __future__ import annotations
from math import ceil
import pytest
from pydantic import ValidationError
from application.common.pagination import (
from packages.application.common.pagination import (
PaginatedResponse,
PaginationMeta,
PaginationParams,
@@ -18,183 +13,315 @@ from application.common.pagination import (
)
class TestPaginationParamsDefaults:
"""PaginationParams 默认值与属性."""
class TestPaginationParams:
"""PaginationParams 测试"""
def test_default_page_is_1(self):
"""默认页码为1."""
def test_default_values(self):
"""默认值正确"""
params = PaginationParams()
assert params.page == 1
def test_default_page_size_is_20(self):
"""默认每页20条."""
params = PaginationParams()
assert params.page_size == 20
def test_offset_first_page_is_0(self):
"""第一页偏移量为0."""
def test_offset_first_page(self):
"""第一页 offset 为 0"""
params = PaginationParams(page=1, page_size=20)
assert params.offset == 0
def test_offset_second_page(self):
"""第二页偏移量=page_size."""
"""第二页 offset 计算正确"""
params = PaginationParams(page=2, page_size=20)
assert params.offset == 20
def test_offset_page_3_size_10(self):
"""第3页每页10条,偏移20."""
def test_offset_custom_page_size(self):
"""自定义 page_size 的 offset"""
params = PaginationParams(page=3, page_size=10)
assert params.offset == 20
def test_limit_equals_page_size(self):
"""limit等于page_size."""
"""limit 等于 page_size"""
params = PaginationParams(page_size=50)
assert params.limit == 50
class TestPaginationParamsValidation:
"""PaginationParams 参数校验."""
def test_page_zero_rejected(self):
"""页码0被拒绝."""
def test_page_must_be_at_least_1(self):
"""page 不能小于 1"""
with pytest.raises(ValidationError):
PaginationParams(page=0)
def test_page_negative_rejected(self):
"""负页码被拒绝."""
def test_page_negative_raises(self):
"""page 不能为负数"""
with pytest.raises(ValidationError):
PaginationParams(page=-1)
def test_page_size_zero_rejected(self):
"""每页0条被拒绝."""
def test_page_size_must_be_at_least_1(self):
"""page_size 不能小于 1"""
with pytest.raises(ValidationError):
PaginationParams(page_size=0)
def test_page_size_negative_rejected(self):
"""负每页条数被拒绝."""
with pytest.raises(ValidationError):
PaginationParams(page_size=-5)
def test_page_size_over_100_rejected(self):
"""每页超过100条被拒绝."""
def test_page_size_max_100(self):
"""page_size 最大 100"""
with pytest.raises(ValidationError):
PaginationParams(page_size=101)
def test_page_size_100_allowed(self):
"""每页100条允许."""
def test_page_size_100_is_valid(self):
"""page_size=100 是合法的"""
params = PaginationParams(page_size=100)
assert params.page_size == 100
def test_page_size_1_allowed(self):
"""每页1条允许."""
class TestPaginationMeta:
"""PaginationMeta 测试"""
def test_from_params_first_page(self):
"""第一页元数据"""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=25)
assert meta.page == 1
assert meta.page_size == 10
assert meta.total == 25
assert meta.total_pages == 3
assert meta.has_next is True
assert meta.has_prev is False
def test_from_params_last_page(self):
"""最后一页元数据"""
params = PaginationParams(page=3, page_size=10)
meta = PaginationMeta.from_params(params, total=25)
assert meta.page == 3
assert meta.total_pages == 3
assert meta.has_next is False
assert meta.has_prev is True
def test_from_params_middle_page(self):
"""中间页元数据"""
params = PaginationParams(page=2, page_size=10)
meta = PaginationMeta.from_params(params, total=50)
assert meta.page == 2
assert meta.total_pages == 5
assert meta.has_next is True
assert meta.has_prev is True
def test_from_params_zero_total(self):
"""总数为 0 时"""
params = PaginationParams(page=1, page_size=20)
meta = PaginationMeta.from_params(params, total=0)
assert meta.total == 0
assert meta.total_pages == 0
assert meta.has_next is False
assert meta.has_prev is False
def test_from_params_exact_multiple(self):
"""总数刚好是 page_size 的整数倍"""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=30)
assert meta.total_pages == 3
def test_from_params_single_page(self):
"""单页即可放下所有数据"""
params = PaginationParams(page=1, page_size=100)
meta = PaginationMeta.from_params(params, total=50)
assert meta.total_pages == 1
assert meta.has_next is False
assert meta.has_prev is False
class TestPaginatedResponse:
"""PaginatedResponse 测试"""
def test_create_success(self):
"""创建分页响应"""
params = PaginationParams(page=1, page_size=10)
data = [1, 2, 3]
response = PaginatedResponse.create(data, params, total=25)
assert response.data == [1, 2, 3]
assert response.pagination.page == 1
assert response.pagination.total == 25
assert response.pagination.total_pages == 3
def test_create_empty_data(self):
"""空数据分页响应"""
params = PaginationParams(page=1, page_size=20)
response = PaginatedResponse.create([], params, total=0)
assert response.data == []
assert response.pagination.total == 0
assert response.pagination.total_pages == 0
class TestPaginateFunction:
"""paginate 函数测试(内存分页)"""
def test_first_page(self):
"""第一页分页"""
items = list(range(30))
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
assert result.data == list(range(10))
assert result.pagination.total == 30
assert result.pagination.total_pages == 3
assert result.pagination.has_next is True
assert result.pagination.has_prev is False
def test_second_page(self):
"""第二页分页"""
items = list(range(30))
params = PaginationParams(page=2, page_size=10)
result = paginate(items, params)
assert result.data == list(range(10, 20))
assert result.pagination.page == 2
def test_last_page(self):
"""最后一页分页"""
items = list(range(25))
params = PaginationParams(page=3, page_size=10)
result = paginate(items, params)
assert result.data == list(range(20, 25))
assert len(result.data) == 5
assert result.pagination.has_next is False
def test_empty_list(self):
"""空列表分页"""
params = PaginationParams(page=1, page_size=20)
result = paginate([], params)
assert result.data == []
assert result.pagination.total == 0
assert result.pagination.total_pages == 0
def test_page_beyond_total(self):
"""页码超出总数"""
items = list(range(5))
params = PaginationParams(page=10, page_size=10)
result = paginate(items, params)
assert result.data == []
assert result.pagination.total == 5
assert result.pagination.total_pages == 1
def test_custom_page_size(self):
"""自定义每页数量"""
items = list(range(100))
params = PaginationParams(page=1, page_size=50)
result = paginate(items, params)
assert len(result.data) == 50
assert result.pagination.total_pages == 2
def test_single_item(self):
"""单条数据"""
items = ["only_one"]
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
assert result.data == ["only_one"]
assert result.pagination.total == 1
assert result.pagination.total_pages == 1
def test_generic_type_preserved(self):
"""泛型类型数据正确"""
items = [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
assert len(result.data) == 2
assert result.data[0]["id"] == 1
# ── PaginationParams 补充边界 ───────────────────────────────────────────────
class TestPaginationParamsEdgeCases:
"""PaginationParams 补充边界场景."""
def test_page_size_1_minimum(self):
"""page_size=1 是允许的最小值."""
params = PaginationParams(page_size=1)
assert params.page_size == 1
assert params.limit == 1
def test_page_1_allowed(self):
"""页码1允许."""
params = PaginationParams(page=1)
assert params.page == 1
def test_page_size_100_maximum(self):
"""page_size=100 是允许的最大值."""
params = PaginationParams(page_size=100)
assert params.page_size == 100
def test_large_page_allowed(self):
"""大页码允许(不设上限)."""
params = PaginationParams(page=9999)
assert params.page == 9999
def test_offset_page_1_size_100(self):
"""第1页每页100条 offset=0."""
params = PaginationParams(page=1, page_size=100)
assert params.offset == 0
def test_offset_page_100_size_100(self):
"""第100页每页100条 offset=9900."""
params = PaginationParams(page=100, page_size=100)
assert params.offset == 9900
def test_large_page_number_accepted(self):
"""极大页码(超过实际页数)允许."""
params = PaginationParams(page=999999, page_size=20)
assert params.page == 999999
assert params.offset == (999999 - 1) * 20
class TestPaginationMetaFromParams:
"""PaginationMeta.from_params 元数据计算."""
# ── PaginationMeta 补充边界 ─────────────────────────────────────────────────
def test_first_page_has_prev_false(self):
"""第一页没有上一页."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.has_prev is False
def test_first_page_has_next_true(self):
"""第一页(数据多时有下一页."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.has_next is True
class TestPaginationMetaEdgeCases:
"""PaginationMeta 补充边界场景."""
def test_last_page_has_next_false(self):
"""最后一页没有下一页."""
params = PaginationParams(page=10, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.has_next is False
def test_last_page_has_prev_true(self):
"""最后一页有上一页."""
params = PaginationParams(page=10, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.has_prev is True
def test_middle_page_has_both(self):
"""中间页上下都有."""
params = PaginationParams(page=5, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.has_prev is True
assert meta.has_next is True
def test_total_pages_exact_division(self):
"""整除时总页数正确."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=100)
assert meta.total_pages == 10
def test_total_pages_with_remainder(self):
"""有余数时向上取整."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=105)
assert meta.total_pages == 11
def test_total_pages_single_item(self):
"""1条数据总页数=1."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=1)
assert meta.total_pages == 1
def test_total_zero_gives_zero_pages(self):
"""0条数据总页数=0."""
params = PaginationParams(page=1, page_size=10)
def test_total_0_page_1(self):
"""total=0, page=1 时 total_pages=0, 无上下页."""
params = PaginationParams(page=1, page_size=20)
meta = PaginationMeta.from_params(params, total=0)
assert meta.total_pages == 0
def test_total_zero_has_prev_false(self):
"""0条数据has_prev=False."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=0)
assert meta.has_next is False
assert meta.has_prev is False
def test_total_zero_has_next_false(self):
"""0条数据has_next=False."""
params = PaginationParams(page=1, page_size=10)
def test_total_0_page_beyond(self):
"""total=0, page>1 时 has_prev=True(因为page>1."""
params = PaginationParams(page=3, page_size=20)
meta = PaginationMeta.from_params(params, total=0)
assert meta.has_next is False
def test_page_exactly_total_pages_no_next(self):
"""当前页等于总页数时没有下一页."""
params = PaginationParams(page=5, page_size=10)
meta = PaginationMeta.from_params(params, total=50)
assert meta.has_next is False
assert meta.total_pages == 5
def test_page_beyond_total_pages(self):
"""页码超过总页数时has_next=False."""
params = PaginationParams(page=20, page_size=10)
meta = PaginationMeta.from_params(params, total=50)
assert meta.total_pages == 0
assert meta.has_next is False
assert meta.has_prev is True
def test_preserves_page_and_page_size(self):
"""保留输入的page和page_size."""
params = PaginationParams(page=3, page_size=25)
meta = PaginationMeta.from_params(params, total=200)
assert meta.page == 3
assert meta.page_size == 25
assert meta.total == 200
def test_exact_last_page(self):
"""刚好是最后一页时 has_next=False."""
params = PaginationParams(page=5, page_size=10)
meta = PaginationMeta.from_params(params, total=50)
assert meta.total_pages == 5
assert meta.has_next is False
assert meta.has_prev is True
def test_page_size_1_total_1(self):
"""每页1条,1条数据."""
def test_one_more_than_exact(self):
"""比整数页多1条时总页数+1."""
params = PaginationParams(page=1, page_size=10)
meta = PaginationMeta.from_params(params, total=51)
assert meta.total_pages == 6
def test_page_exactly_total_pages(self):
"""page == total_pages 时 has_next=False."""
params = PaginationParams(page=3, page_size=10)
meta = PaginationMeta.from_params(params, total=30)
assert meta.has_next is False
def test_total_1_page_1_size_1(self):
"""1条数据1页."""
params = PaginationParams(page=1, page_size=1)
meta = PaginationMeta.from_params(params, total=1)
assert meta.total_pages == 1
@@ -202,118 +329,60 @@ class TestPaginationMetaFromParams:
assert meta.has_prev is False
class TestPaginatedResponseCreate:
"""PaginatedResponse.create 创建分页响应."""
def test_creates_with_data_and_meta(self):
"""创建包含data和pagination."""
params = PaginationParams(page=1, page_size=10)
data = [{"id": i} for i in range(10)]
resp = PaginatedResponse.create(data, params, total=25)
assert resp.data == data
assert resp.pagination.page == 1
assert resp.pagination.total == 25
assert resp.pagination.total_pages == 3
def test_empty_data(self):
"""空数据响应."""
params = PaginationParams(page=1, page_size=10)
resp = PaginatedResponse.create([], params, total=0)
assert resp.data == []
assert resp.pagination.total == 0
assert resp.pagination.total_pages == 0
# ── paginate 补充边界 ──────────────────────────────────────────────────────
class TestPaginateInMemory:
"""paginate 内存分页函数."""
class TestPaginateEdgeCases:
"""paginate 补充边界场景."""
def test_first_page(self):
"""第一页返回前N条."""
items = list(range(50))
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
assert result.data == list(range(10))
assert result.pagination.total == 50
assert result.pagination.total_pages == 5
assert result.pagination.has_next is True
assert result.pagination.has_prev is False
def test_single_item_list(self):
"""单元素列表."""
result = paginate([42], PaginationParams(page=1, page_size=10))
assert result.data == [42]
assert result.pagination.total == 1
assert result.pagination.total_pages == 1
def test_middle_page(self):
"""中间页."""
items = list(range(50))
params = PaginationParams(page=3, page_size=10)
result = paginate(items, params)
assert result.data == list(range(20, 30))
assert result.pagination.has_prev is True
assert result.pagination.has_next is True
def test_last_page(self):
"""最后一页."""
items = list(range(50))
params = PaginationParams(page=5, page_size=10)
result = paginate(items, params)
assert result.data == list(range(40, 50))
def test_page_exactly_last(self):
"""刚好在最后一页."""
items = list(range(25))
result = paginate(items, PaginationParams(page=3, page_size=10))
assert result.data == list(range(20, 25))
assert result.pagination.has_next is False
assert result.pagination.has_prev is True
def test_last_page_not_full(self):
"""最后一页不足page_size."""
items = list(range(45))
params = PaginationParams(page=5, page_size=10)
result = paginate(items, params)
assert result.data == list(range(40, 45))
assert len(result.data) == 5
assert result.pagination.total == 45
assert result.pagination.total_pages == 5
def test_page_past_end_returns_empty(self):
"""页码超过总数返回空."""
items = list(range(5))
result = paginate(items, PaginationParams(page=10, page_size=10))
assert result.data == []
assert result.pagination.total == 5
def test_empty_list(self):
"""空列表页."""
items = []
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
def test_empty_list_page_1(self):
"""空列表第1页."""
result = paginate([], PaginationParams(page=1, page_size=10))
assert result.data == []
assert result.pagination.total == 0
assert result.pagination.total_pages == 0
def test_page_beyond_total(self):
"""页码超过总页数返回空数据."""
items = list(range(5))
params = PaginationParams(page=10, page_size=10)
result = paginate(items, params)
assert result.data == []
assert result.pagination.total == 5
assert result.pagination.total_pages == 1
assert result.pagination.has_next is False
def test_page_size_1_iterates_all(self):
"""page_size=1 时每页1条."""
items = ["a", "b", "c"]
r1 = paginate(items, PaginationParams(page=1, page_size=1))
r2 = paginate(items, PaginationParams(page=2, page_size=1))
r3 = paginate(items, PaginationParams(page=3, page_size=1))
assert r1.data == ["a"]
assert r2.data == ["b"]
assert r3.data == ["c"]
def test_single_item(self):
"""单条数据分页."""
items = ["only"]
params = PaginationParams(page=1, page_size=10)
result = paginate(items, params)
assert result.data == ["only"]
assert result.pagination.total == 1
assert result.pagination.total_pages == 1
def test_does_not_mutate_input(self):
"""不修改输入列表."""
items = [1, 2, 3, 4, 5]
original = items[:]
paginate(items, PaginationParams(page=1, page_size=2))
assert items == original
def test_page_size_larger_than_total(self):
"""每页条数大于总数,第一页包含全部."""
def test_page_size_greater_than_total(self):
"""每页条数大于总数."""
items = list(range(5))
params = PaginationParams(page=1, page_size=100)
result = paginate(items, params)
result = paginate(items, PaginationParams(page=1, page_size=100))
assert result.data == items
assert result.pagination.total_pages == 1
def test_page_size_1(self):
"""每页1条."""
items = ["a", "b", "c"]
params = PaginationParams(page=2, page_size=1)
result = paginate(items, params)
assert result.data == ["b"]
assert result.pagination.total_pages == 3
def test_preserves_original_list(self):
"""不修改原始列表."""
items = [1, 2, 3, 4, 5]
original = items.copy()
params = PaginationParams(page=1, page_size=2)
paginate(items, params)
assert items == original
+243 -135
View File
@@ -1,178 +1,296 @@
"""文本分段器纯逻辑测试 — split_text.
覆盖空文本、短文本、句子边界分段、超长句强制切段、过短段合并等场景。
"""
"""文本分段工具单元测试."""
from __future__ import annotations
import pytest
from application.tts_job.text_splitter import split_text
from packages.application.tts_job.text_splitter import split_text
class TestSplitTextEmptyOrShort:
"""空文本与短文本."""
class TestSplitText:
"""split_text 函数测试"""
def test_empty_string_returns_empty_list(self):
"""空字符串返回空列表."""
"""空字符串返回空列表"""
assert split_text("") == []
def test_whitespace_only_returns_empty_list(self):
"""纯空白字符返回空列表."""
assert split_text(" \n\t ") == []
"""纯空白字符返回空列表"""
assert split_text(" \n \t ") == []
def test_none_not_supported(self):
"""None不支持(strip会报错)."""
with pytest.raises(AttributeError):
split_text(None)
def test_short_text_single_segment(self):
"""短文本不分割,单段返回."""
text = "你好世界。"
def test_short_text_returns_single_segment(self):
"""短文本直接返回单段"""
text = "这是一段短文本。"
result = split_text(text, max_chars=500)
assert result == [text]
def test_exactly_max_chars_single_segment(self):
"""好等于max_chars时不分段."""
def test_text_length_equals_max_chars(self):
"""文本长度恰好等于 max_chars 时返回单段"""
text = "a" * 100
result = split_text(text, max_chars=100)
assert len(result) == 1
assert len(result[0]) == 100
def test_under_max_chars_single_segment(self):
"""少于max_chars时不分段."""
text = "a" * 50
def test_splits_on_sentence_boundary(self):
"""在句子边界处分段"""
# 构造长文本,确保超过 max_chars
sentences = ["今天天气真好。我们一起去公园散步吧。", "公园里有很多花。还有很多小朋友在玩耍。"] * 10
text = "".join(sentences)
result = split_text(text, max_chars=200)
assert len(result) >= 2
# 每段都不超过 max_chars
for seg in result:
assert len(seg) <= 200
def test_all_segments_within_max_chars(self):
"""所有分段都不超过 max_chars"""
text = "这是第一句话。这是第二句话。这是第三句话。这是第四句话。这是第五句话。" * 10
result = split_text(text, max_chars=100)
assert len(result) == 1
assert len(result[0]) == 50
class TestSplitTextSentenceBoundary:
"""句子边界分段."""
def test_split_by_period(self):
"""按句号分段."""
text = "第一句内容。" + "第二句内容。" * 50
result = split_text(text, max_chars=100)
assert len(result) > 1
# 每段都不超过max_chars
for seg in result:
assert len(seg) <= 100
# 合并后等于原文(去空格后近似)
assert "".join(result) == text.replace(" ", "")
def test_split_by_question_mark(self):
"""按问号分段."""
text = "你好吗?" + "我很好。" * 50
result = split_text(text, max_chars=80)
def test_long_single_sentence_hard_cut(self):
"""超长单句会被硬切"""
text = "a" * 1000 # 没有标点
result = split_text(text, max_chars=200)
assert len(result) > 1
for seg in result:
assert len(seg) <= 200
def test_newline_is_sentence_end(self):
"""换行符作为句子结束符"""
text = "第一行内容\n第二行内容\n第三行内容" * 10
result = split_text(text, max_chars=50)
assert len(result) > 1
for seg in result:
assert len(seg) <= 50
def test_chinese_punctuation(self):
"""中文标点(。!?;)作为句子结束符"""
text = "你好!今天吃什么?我吃米饭;你呢?我也吃米饭。" * 10
result = split_text(text, max_chars=80)
for seg in result:
assert len(seg) <= 80
def test_split_by_exclamation_mark(self):
"""按感叹号分段."""
text = "太棒了!" + "真的好。" * 50
def test_english_punctuation(self):
"""英文标点(.!?;)作为句子结束符"""
text = "Hello! How are you? I'm fine; thank you. Good bye." * 10
result = split_text(text, max_chars=80)
assert len(result) > 1
for seg in result:
assert len(seg) <= 80
def test_merged_short_segments(self):
"""过短的段落会被合并"""
# 构造很多短句
text = "你好。再见。谢谢。抱歉。好的。不行。可以。去吧。" * 5 # 每句3-4字
def test_split_by_newline(self):
"""按换行符分段."""
lines = ["这是第一行很长的内容" * 5 for _ in range(10)]
text = "\n".join(lines)
result = split_text(text, max_chars=100)
assert len(result) > 1
# 合并后段数应该比单纯按句切的少
assert len(result) < len(text) // 3 # 粗略估计
for seg in result:
assert len(seg) <= 100
def test_split_by_semicolon_fullwidth(self):
"""按全角分号分段."""
text = "第一项;" + "第二项内容" * 40
result = split_text(text, max_chars=80)
assert len(result) > 1
def test_preserves_content(self):
"""分段后内容总和与原文基本一致(忽略strip的空白)"""
text = "这是测试文本。包含多个句子。用来验证分段正确性" * 5
def test_split_by_english_period(self):
"""按英文句号也分段(_SENTENCE_ENDS包含.."""
text = "Hello. " + "World. " * 50
result = split_text(text, max_chars=80)
assert len(result) > 1
result = split_text(text, max_chars=50)
def test_split_by_english_question(self):
"""按英文问号分段."""
text = "Really? " + "Yes. " * 50
result = split_text(text, max_chars=80)
assert len(result) > 1
# 合并所有分段,去掉空白后应该与原文去掉空白后基本一致
combined = "".join(result).replace(" ", "")
original = text.strip().replace(" ", "")
assert combined == original
def test_split_by_english_exclamation(self):
"""按英文感叹号分段."""
text = "Wow! " + "Great. " * 50
result = split_text(text, max_chars=80)
assert len(result) > 1
def test_custom_max_chars(self):
"""支持自定义 max_chars"""
text = "测试" * 100 # 200字
def test_short_sentences_not_split(self):
"""短句(<50字)即使有句号也不立刻切,等累积到一定长度."""
# 每句5字,即使有句号也不会在50字前切
text = "你好。" * 5 # 15字符
result_50 = split_text(text, max_chars=50)
result_100 = split_text(text, max_chars=100)
# max_chars 越小,段数应该越多
assert len(result_50) >= len(result_100)
def test_single_char_text(self):
"""单字符文本"""
assert split_text("", max_chars=10) == [""]
def test_text_with_only_punctuation(self):
"""纯标点文本"""
text = "。。。。。。。。。。" # 10个句号
result = split_text(text, max_chars=5)
assert len(result) >= 1
for seg in result:
assert len(seg) <= 5
def test_mixed_content(self):
"""中英文混合内容"""
text = "今天的天气是 sunny and warm。我们去了 park 玩。真的很开心!" * 5
result = split_text(text, max_chars=80)
for seg in result:
assert len(seg) <= 80
# ── 短文本与空文本补充 ──────────────────────────────────────────────────────
class TestSplitTextEmptyAndShort:
"""空文本与短文本补充场景."""
def test_whitespace_only_returns_empty(self):
"""纯空白文本返回空列表."""
assert split_text(" \n\t ") == []
def test_single_char(self):
"""单字符文本."""
assert split_text("", max_chars=10) == [""]
def test_exactly_max_chars_no_split(self):
"""刚好等于 max_chars 不分割."""
text = "a" * 100
result = split_text(text, max_chars=100)
# 因为每段至少50字才在句子边界切,所以15字的文本应该是1段
assert len(result) == 1
assert result[0] == text
class TestSplitTextLongSentenceForceSplit:
"""超长单句强制切段."""
def test_single_very_long_sentence_forced_split(self):
"""单句超长时强制切段."""
text = "" * 200 # 没有标点,200字
def test_one_over_max_chars_splits(self):
"""超过 max_chars 1 个字符就会分割."""
text = "a" * 101
result = split_text(text, max_chars=100)
assert len(result) >= 2
for seg in result:
assert len(seg) <= 100
def test_force_split_preserves_all_chars(self):
"""强制切段不丢字符."""
def test_none_raises(self):
"""None 输入抛 AttributeErrorstrip 失败)."""
with pytest.raises(AttributeError):
split_text(None)
# ── 句子边界分段补充 ──────────────────────────────────────────────────────
class TestSplitTextSentenceBoundaries:
"""句子边界分段补充场景."""
def test_split_on_fullwidth_period(self):
"""全角句号分段."""
text = "第一句很长的内容。" * 20
result = split_text(text, max_chars=60)
assert len(result) > 1
for seg in result:
assert len(seg) <= 60
def test_split_on_fullwidth_question(self):
"""全角问号分段."""
text = "你知道这是为什么吗?" + "是的。" * 20
result = split_text(text, max_chars=60)
assert len(result) > 1
def test_split_on_fullwidth_exclamation(self):
"""全角感叹号分段."""
text = "真是太棒了!" + "内容。" * 20
result = split_text(text, max_chars=60)
assert len(result) > 1
def test_split_on_newline(self):
"""换行符分段."""
lines = ["这是第一行很长的一段文字内容" * 3 for _ in range(5)]
text = "\n".join(lines)
result = split_text(text, max_chars=80)
assert len(result) > 1
def test_split_on_semicolon(self):
"""全角分号分段."""
text = "第一项内容;" + "其他内容。" * 20
result = split_text(text, max_chars=60)
assert len(result) > 1
def test_english_period_splits(self):
"""英文句号分段."""
text = "Hello world. " * 30
result = split_text(text, max_chars=80)
assert len(result) > 1
def test_short_sentences_stay_merged(self):
"""短句(都 < 50字的句子不会单独成段,会累积到一起."""
text = "你好。我好。大家好。"
result = split_text(text, max_chars=200)
assert len(result) == 1
# ── 长句强制切段补充 ──────────────────────────────────────────────────────
class TestSplitTextLongSentenceForce:
"""超长单句强制切段补充."""
def test_no_punctuation_forced_split(self):
"""完全没有标点的超长文本硬切."""
text = "" * 300
result = split_text(text, max_chars=100)
assert len(result) == 3
for seg in result:
assert len(seg) == 100
def test_force_split_preserves_content(self):
"""硬切不丢字符."""
text = "a" * 250
result = split_text(text, max_chars=100)
# 所有段的总长度应等于原文(去掉空白可能有细微差异,但纯字母应该不变)
assert sum(len(s) for s in result) == 250
def test_mixed_long_and_short_sentences(self):
def test_mixed_long_and_short(self):
"""长句短句混合."""
long_part = "非常长的句子没有标点符号" * 20
text = long_part + "。结束句"
long_part = "非常长的句子没有标点符号" * 15
text = long_part + "。结"
result = split_text(text, max_chars=100)
assert len(result) > 1
for seg in result:
assert len(seg) <= 100
class TestSplitTextShortSegmentMerging:
"""过短段落合并."""
# ── 短段合并补充 ─────────────────────────────────────────────────────────
def test_short_final_segment_merged(self):
"""最后一段过短会被合并到前一段(如果不超限)."""
# 构造两段,第二段很短
text = "第一部分内容" * 10 + "" + "短尾巴。"
result = split_text(text, max_chars=200)
# 短尾巴应该被合并,不会单独成为一段
assert len(result) <= 2 # 可能1段或2段,但不会有3段
def test_very_short_segments_combined(self):
"""多个极短段会被合并."""
# 构造多个短句,都<50字
sentences = ["你好。", "我好。", "大家好。", "天气不错。", "一起玩吧。", "好的。"]
class TestSplitTextShortSegmentMerge:
"""短段合并补充场景."""
def test_multiple_short_sentences_merged(self):
"""多个短句合并成一段."""
sentences = ["你好。", "我好。", "大家好。", "天气好。", "心情好。"]
text = "".join(sentences)
result = split_text(text, max_chars=200)
# 总长度很短,应该合并成1段
assert len(result) == 1
def test_short_tail_merged(self):
"""尾部短段被合并到前一段."""
# 前面一段接近 max_chars,尾部很短
long_part = "一二三四五六七八九十" * 9 + "" # ~90字
tail = "完。" # 2字
text = long_part + tail
result = split_text(text, max_chars=100)
# 尾部短的应该被合并
assert len(result) <= 2
# ── 边界情况补充 ─────────────────────────────────────────────────────────
class TestSplitTextEdgeCases:
"""边界情况."""
def test_single_character(self):
"""单字符."""
result = split_text("", max_chars=10)
assert result == [""]
"""边界情况补充."""
def test_only_punctuation(self):
"""纯标点符号."""
@@ -180,40 +298,30 @@ class TestSplitTextEdgeCases:
result = split_text(text, max_chars=10)
assert len(result) == 1
def test_custom_max_chars_small(self):
"""很小的max_chars."""
text = "一二三四五六七八九十。" * 5
result = split_text(text, max_chars=20)
assert len(result) > 1
for seg in result:
assert len(seg) <= 20
def test_mixed_chinese_english(self):
"""中英文混合."""
text = "今天天气很好。Today is a nice day. 我们出去玩吧!Let's go out and play." * 20
result = split_text(text, max_chars=150)
text = "你好Hello。World!" * 20
result = split_text(text, max_chars=100)
assert len(result) > 1
for seg in result:
assert len(seg) <= 150
assert len(seg) <= 100
def test_no_punctuation_long_text(self):
"""完全没有标点的长文本,只能硬切."""
text = "" * 500
result = split_text(text, max_chars=100)
assert len(result) == 5
for seg in result:
assert len(seg) == 100
def test_strip_leading_trailing_whitespace(self):
def test_strip_whitespace(self):
"""首尾空白被去除."""
text = " 你好世界。 "
result = split_text(text, max_chars=100)
assert result == ["你好世界。"]
def test_total_length_preserved(self):
"""分段后总字符数大致等于原文(去除首尾空白后)."""
text = "这是一段测试文本。" * 30
"""分段后总长度等于原文 strip 后长度."""
text = "这是一段用于测试文本内容" * 20
result = split_text(text, max_chars=100)
joined = "".join(result)
# 因为strip的原因可能略有差异,但应该接近
assert len(joined) == len(text.strip())
assert "".join(result) == text.strip()
def test_custom_small_max_chars(self):
"""很小的 max_chars."""
text = "一二三四五六七八九十。" * 5
result = split_text(text, max_chars=20)
assert len(result) > 1
for seg in result:
assert len(seg) <= 20
File diff suppressed because it is too large Load Diff