feat: #1776 素材库 asset_count 自动同步维护 #1787

Merged
auto-approve-bot merged 2 commits from feature/asset-count-sync-1776 into develop 2026-09-08 10:30:21 +08:00
9 changed files with 636 additions and 32 deletions
+5
View File
@@ -208,6 +208,8 @@ def _create_pending_asset(
find-or-createprepare 阶段已按 file_hash/client_upload_id 预建的占位记录
会被 find_by_library_and_file_hash/find_by_library_and_client_upload_id 命中,
直接复用并补齐字段(避免 pre-create + complete 重复建两条)。
Issue #1776: 素材库计数由 asset_repository.create() 自动维护。
"""
# 1. 按 client_upload_id / file_hash 查找现有记录
existing = None
@@ -389,6 +391,7 @@ async def prepare_direct_upload(
file_size=request.file_size,
)
pending_asset_id = pending.id
# Issue #1776: 计数由 asset_repository.create() 自动维护
except Exception as error:
# 预建失败不阻塞签名:complete 仍可按 OSS 文件 + hash 兜底去重
logger.warning("预建 asset 占位失败,降级走 old flow: %s", error)
@@ -474,6 +477,7 @@ async def complete_direct_upload(
client_upload_id=request.client_upload_id,
file_size=request.file_size,
)
# Issue #1776: 计数由 asset_repository.create() 自动维护
job = _submit_ingest_job(
project_id=request.project_id,
@@ -568,6 +572,7 @@ async def upload_asset(
file_hash=file_hash,
client_upload_id=client_upload_id,
)
# Issue #1776: 计数由 asset_repository.create() 自动维护
job = _submit_ingest_job(
project_id=project_id,
@@ -35,18 +35,23 @@ class InMemoryAssetLibraryRepository:
return True
return False
def increment_asset_count(self, library_id: str, size_delta: int) -> None:
def increment_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
library = self._libraries.get(library_id)
if library:
library.asset_count += 1
library.asset_count += count_delta
library.total_size += size_delta
def decrement_asset_count(self, library_id: str, size_delta: int) -> None:
def decrement_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
library = self._libraries.get(library_id)
if library:
library.asset_count = max(0, library.asset_count - 1)
library.asset_count = max(0, library.asset_count - count_delta)
library.total_size = max(0, library.total_size - size_delta)
def recount_assets(self, library_id: str) -> int:
"""InMemory 实现无法真正重算(没有 asset 数据源),返回当前计数。"""
library = self._libraries.get(library_id)
return library.asset_count if library else 0
def get_or_create_default_library(
self,
project_id: str,
@@ -77,19 +77,79 @@ class SQLAlchemyAssetLibraryRepository:
return True
return False
async def increment_asset_count(self, library_id: str, size_delta: int) -> None:
model = self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).first()
if model:
model.asset_count = (model.asset_count or 0) + 1
model.total_size = (model.total_size or 0) + size_delta
self.session.commit()
def increment_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
"""原子递增素材计数(Issue #1776)。
async def decrement_asset_count(self, library_id: str, size_delta: int) -> None:
model = self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).first()
if model:
model.asset_count = max(0, (model.asset_count or 0) - 1)
model.total_size = max(0, (model.total_size or 0) - size_delta)
self.session.commit()
使用 SQL 级 UPDATE 保证并发安全,不单独 commit(由调用方统一事务提交)。
"""
from sqlalchemy import func
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).update(
{
AssetLibraryModel.asset_count: func.coalesce(AssetLibraryModel.asset_count, 0) + count_delta,
AssetLibraryModel.total_size: func.coalesce(AssetLibraryModel.total_size, 0) + size_delta,
}
)
def decrement_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
"""原子递减素材计数(Issue #1776),下限为 0 防止负数。
使用 SQL 级 UPDATE 保证并发安全,不单独 commit(由调用方统一事务提交)。
使用 CASE WHEN 兼容 SQLite(测试)和 PostgreSQL(生产)。
"""
from sqlalchemy import case, func
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).update(
{
AssetLibraryModel.asset_count: case(
(func.coalesce(AssetLibraryModel.asset_count, 0) - count_delta < 0, 0),
else_=func.coalesce(AssetLibraryModel.asset_count, 0) - count_delta,
),
AssetLibraryModel.total_size: case(
(func.coalesce(AssetLibraryModel.total_size, 0) - size_delta < 0, 0),
else_=func.coalesce(AssetLibraryModel.total_size, 0) - size_delta,
),
}
)
def recount_assets(self, library_id: str) -> int:
"""重算素材库计数(Issue #1776)。
直接查询实际素材数量(排除已删除),更新 asset_count 和 total_size。
返回重算后的实际计数。
"""
from sqlalchemy import func
from packages.adapters.sqlalchemy_impl.models import AssetModel
# 查询实际计数(排除 deleted)
actual_count = (
self.session.query(func.count(AssetModel.id))
.filter(
AssetModel.asset_library_id == library_id,
AssetModel.status != "deleted",
)
.scalar()
or 0
)
# 查询实际总大小
actual_size = (
self.session.query(func.coalesce(func.sum(AssetModel.file_size), 0))
.filter(
AssetModel.asset_library_id == library_id,
AssetModel.status != "deleted",
)
.scalar()
or 0
)
# 更新素材库记录
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).update(
{
AssetLibraryModel.asset_count: actual_count,
AssetLibraryModel.total_size: actual_size,
}
)
return actual_count
def get_or_create_default_library(
self,
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import AssetModel, AssetTagModel
from packages.adapters.sqlalchemy_impl.models import AssetLibraryModel, AssetModel, AssetTagModel
from packages.domain import Asset, AssetStatus, ClassificationStatus
@@ -141,6 +141,16 @@ class SQLAlchemyAssetRepository:
self.session.add(model)
self.session.flush()
self._sync_asset_tags(asset.id, asset.tag_ids)
# Issue #1776: 自动维护素材库计数(同事务内原子更新)
if asset.library_id and asset.status.value != "deleted":
from sqlalchemy import func
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == asset.library_id).update(
{
AssetLibraryModel.asset_count: func.coalesce(AssetLibraryModel.asset_count, 0) + 1,
AssetLibraryModel.total_size: func.coalesce(AssetLibraryModel.total_size, 0) + asset.file_size,
}
)
self.session.commit()
return asset
@@ -175,7 +185,27 @@ class SQLAlchemyAssetRepository:
def delete(self, asset_id: str) -> bool:
model = self.session.query(AssetModel).filter(AssetModel.id == asset_id).first()
if model:
library_id = model.asset_library_id
file_size = model.file_size or 0
# 只统计非 deleted 状态的素材
was_counted = model.status != "deleted"
self.session.delete(model)
# Issue #1776: 自动维护素材库计数
if library_id and was_counted:
from sqlalchemy import case, func
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library_id).update(
{
AssetLibraryModel.asset_count: case(
(func.coalesce(AssetLibraryModel.asset_count, 0) - 1 < 0, 0),
else_=func.coalesce(AssetLibraryModel.asset_count, 0) - 1,
),
AssetLibraryModel.total_size: case(
(func.coalesce(AssetLibraryModel.total_size, 0) - file_size < 0, 0),
else_=func.coalesce(AssetLibraryModel.total_size, 0) - file_size,
),
}
)
self.session.commit()
return True
return False
@@ -187,11 +217,44 @@ class SQLAlchemyAssetRepository:
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
# 先查询待删除素材的库分布(用于更新计数)
to_delete = (
self.session.query(AssetModel.asset_library_id, AssetModel.file_size)
.filter(AssetModel.id.in_(asset_ids), AssetModel.status != "deleted")
.all()
)
if not to_delete:
return 0
# 按库分组统计
library_deltas: dict[str, tuple[int, int]] = {} # library_id -> (count_delta, size_delta)
for lib_id, size in to_delete:
if lib_id not in library_deltas:
library_deltas[lib_id] = (0, 0)
c, s = library_deltas[lib_id]
library_deltas[lib_id] = (c + 1, s + (size or 0))
# 执行软删除
count = (
self.session.query(AssetModel)
.filter(AssetModel.id.in_(asset_ids), AssetModel.status != "deleted")
.update({AssetModel.status: "deleted", AssetModel.updated_at: now}, synchronize_session=False)
)
# Issue #1776: 自动维护各素材库计数
if library_deltas:
from sqlalchemy import case, func
for lib_id, (count_delta, size_delta) in library_deltas.items():
self.session.query(AssetLibraryModel).filter(AssetLibraryModel.id == lib_id).update(
{
AssetLibraryModel.asset_count: case(
(func.coalesce(AssetLibraryModel.asset_count, 0) - count_delta < 0, 0),
else_=func.coalesce(AssetLibraryModel.asset_count, 0) - count_delta,
),
AssetLibraryModel.total_size: case(
(func.coalesce(AssetLibraryModel.total_size, 0) - size_delta < 0, 0),
else_=func.coalesce(AssetLibraryModel.total_size, 0) - size_delta,
),
}
)
self.session.commit()
return count
+9 -2
View File
@@ -30,9 +30,16 @@ class AssetLibraryRepository(ABC):
pass
@abstractmethod
async def increment_asset_count(self, library_id: str, size_delta: int) -> None:
def increment_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
"""原子递增素材计数(Issue #1776)。"""
pass
@abstractmethod
async def decrement_asset_count(self, library_id: str, size_delta: int) -> None:
def decrement_asset_count(self, library_id: str, count_delta: int = 1, size_delta: int = 0) -> None:
"""原子递减素材计数(Issue #1776)。"""
pass
@abstractmethod
def recount_assets(self, library_id: str) -> int:
"""重算素材库计数(Issue #1776)。"""
pass
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""素材库计数重算脚本(Issue #1776)。
用法:
# Dry-run: 输出差异清单,不执行修改
python scripts/recount_asset_counts.py --dry-run
# 执行修正
python scripts/recount_asset_counts.py
# 只处理指定项目
python scripts/recount_asset_counts.py --project-id <project_id>
# 只处理指定素材库
python scripts/recount_asset_counts.py --library-id <library_id>
"""
import argparse
import sys
from pathlib import Path
# 添加项目根目录到 path
sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy import create_engine, func
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import AssetLibraryModel, AssetModel
def get_db_session() -> Session:
"""创建数据库 session。"""
import os
database_url = os.getenv("DATABASE_URL")
if not database_url:
print("ERROR: DATABASE_URL environment variable not set")
sys.exit(1)
engine = create_engine(database_url)
return Session(engine)
def check_discrepancies(session: Session, project_id: str | None = None, library_id: str | None = None) -> list[dict]:
"""检查素材库计数差异。
返回列表,每项包含:
- library_id: 素材库 ID
- library_name: 素材库名称
- recorded_count: 记录的计数
- actual_count: 实际计数
- delta: 差异 (actual - recorded)
"""
query = session.query(AssetLibraryModel)
if project_id:
query = query.filter(AssetLibraryModel.project_id == project_id)
if library_id:
query = query.filter(AssetLibraryModel.id == library_id)
libraries = query.all()
discrepancies = []
for lib in libraries:
# 查询实际计数(排除 deleted)
actual_count = (
session.query(func.count(AssetModel.id))
.filter(
AssetModel.asset_library_id == lib.id,
AssetModel.status != "deleted",
)
.scalar()
or 0
)
actual_size = (
session.query(func.coalesce(func.sum(AssetModel.file_size), 0))
.filter(
AssetModel.asset_library_id == lib.id,
AssetModel.status != "deleted",
)
.scalar()
or 0
)
recorded_count = int(lib.asset_count or 0)
recorded_size = int(lib.total_size or 0)
if actual_count != recorded_count or actual_size != recorded_size:
discrepancies.append(
{
"library_id": lib.id,
"library_name": lib.name,
"project_id": lib.project_id,
"kind": lib.kind,
"recorded_count": recorded_count,
"actual_count": actual_count,
"count_delta": actual_count - recorded_count,
"recorded_size": recorded_size,
"actual_size": actual_size,
"size_delta": actual_size - recorded_size,
}
)
return discrepancies
def fix_discrepancies(session: Session, discrepancies: list[dict]) -> int:
"""修正素材库计数。返回修正数量。"""
fixed = 0
for d in discrepancies:
session.query(AssetLibraryModel).filter(AssetLibraryModel.id == d["library_id"]).update(
{
AssetLibraryModel.asset_count: d["actual_count"],
AssetLibraryModel.total_size: d["actual_size"],
}
)
fixed += 1
session.commit()
return fixed
def main():
parser = argparse.ArgumentParser(description="素材库计数重算脚本(Issue #1776)")
parser.add_argument("--dry-run", action="store_true", help="只输出差异清单,不执行修正")
parser.add_argument("--project-id", type=str, help="只处理指定项目")
parser.add_argument("--library-id", type=str, help="只处理指定素材库")
args = parser.parse_args()
session = get_db_session()
try:
discrepancies = check_discrepancies(session, args.project_id, args.library_id)
if not discrepancies:
print("✅ 所有素材库计数一致,无需修正")
return
# 输出差异清单
print(f"发现 {len(discrepancies)} 个素材库计数不一致:\n")
print(f"{'Library ID':<40} {'Name':<20} {'Recorded':<10} {'Actual':<10} {'Delta':<10}")
print("-" * 90)
for d in discrepancies:
print(
f"{d['library_id']:<40} {d['library_name'][:20]:<20} {d['recorded_count']:<10} {d['actual_count']:<10} {d['count_delta']:+<10}"
)
total_delta = sum(d["count_delta"] for d in discrepancies)
print(f"\n总计差异: {total_delta:+d}")
if args.dry_run:
print("\n[DRY-RUN] 未执行修正。移除 --dry-run 参数以执行修正。")
else:
print("\n正在执行修正...")
fixed = fix_discrepancies(session, discrepancies)
print(f"✅ 已修正 {fixed} 个素材库计数")
finally:
session.close()
if __name__ == "__main__":
main()
+305
View File
@@ -0,0 +1,305 @@
"""Issue #1776: asset_libraries.asset_count 同步维护测试。
覆盖场景:
1. 素材创建 → count +1
2. 素材硬删除 → count -1
3. 素材软删除(batch_delete)→ count -N
4. 幂等上传(prepare 占位 + complete 复用)→ 不重复计数
5. 重试场景(complete 重试)→ 不重复计数
6. recount 方法修正计数
"""
import uuid
from datetime import datetime, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.asset_library_repository import SQLAlchemyAssetLibraryRepository
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from packages.adapters.sqlalchemy_impl.models import AssetLibraryModel, AssetModel, Base
from packages.domain import Asset, AssetLibrary, AssetLibraryKind, AssetStatus
@pytest.fixture
def db_session():
"""创建测试用 SQLite 内存数据库。"""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
session = Session(engine)
yield session
session.close()
@pytest.fixture
def asset_repo(db_session):
return SQLAlchemyAssetRepository(db_session)
@pytest.fixture
def library_repo(db_session):
return SQLAlchemyAssetLibraryRepository(db_session)
def _make_library(project_id: str, kind: AssetLibraryKind = AssetLibraryKind.VIDEO) -> AssetLibrary:
return AssetLibrary.create(project_id=project_id, name=f"测试{kind.value}", kind=kind)
def _make_asset(
library_id: str,
project_id: str,
*,
status: AssetStatus = AssetStatus.PROCESSING,
file_size: int = 1024,
file_hash: str = "",
client_upload_id: str = "",
) -> Asset:
return Asset.create(
project_id=project_id,
library_id=library_id,
name=f"test_{uuid.uuid4().hex[:8]}.mp4",
storage_key=f"uploads/{uuid.uuid4().hex[:8]}/test.mp4",
mime_type="video/mp4",
status=status,
uploaded_by_user_id="test-user",
file_hash=file_hash,
client_upload_id=client_upload_id,
file_size=file_size,
)
class TestAssetCountOnCreate:
"""素材创建时计数递增。"""
def test_create_asset_increments_count(self, library_repo, asset_repo, db_session):
"""创建一个素材 → count 从 0 变 1。"""
library = library_repo.create(_make_library("project-1"))
assert library.asset_count == 0
asset = _make_asset(library.id, "project-1")
asset_repo.create(asset)
# 重新查询验证计数
updated_library = library_repo.get(library.id)
assert updated_library.asset_count == 1
assert updated_library.total_size == 1024
def test_create_multiple_assets_increments_count(self, library_repo, asset_repo, db_session):
"""创建多个素材 → count 累加。"""
library = library_repo.create(_make_library("project-1"))
for _i in range(3):
asset = _make_asset(library.id, "project-1", file_size=100 * (_i + 1))
asset_repo.create(asset)
updated_library = library_repo.get(library.id)
assert updated_library.asset_count == 3
assert updated_library.total_size == 100 + 200 + 300
class TestAssetCountOnDelete:
"""素材删除时计数递减。"""
def test_hard_delete_decrements_count(self, library_repo, asset_repo, db_session):
"""硬删除素材 → count -1。"""
library = library_repo.create(_make_library("project-1"))
asset = asset_repo.create(_make_asset(library.id, "project-1"))
assert library_repo.get(library.id).asset_count == 1
asset_repo.delete(asset.id)
assert library_repo.get(library.id).asset_count == 0
def test_hard_delete_already_deleted_no_change(self, library_repo, asset_repo, db_session):
"""删除已删除的素材 → count 不变。"""
library = library_repo.create(_make_library("project-1"))
asset = asset_repo.create(_make_asset(library.id, "project-1"))
# 先软删除(count 已经 -1
asset_repo.batch_delete([asset.id])
assert library_repo.get(library.id).asset_count == 0
# 再硬删除(不应再 -1
asset_repo.delete(asset.id)
assert library_repo.get(library.id).asset_count == 0
def test_batch_delete_decrements_count(self, library_repo, asset_repo, db_session):
"""批量软删除 → count -N。"""
library = library_repo.create(_make_library("project-1"))
asset_ids = []
for _i in range(5):
asset = asset_repo.create(_make_asset(library.id, "project-1", file_size=200))
asset_ids.append(asset.id)
assert library_repo.get(library.id).asset_count == 5
# 删除 3 个
deleted_count = asset_repo.batch_delete(asset_ids[:3])
assert deleted_count == 3
assert library_repo.get(library.id).asset_count == 2
assert library_repo.get(library.id).total_size == 200 * 2
def test_batch_delete_skips_already_deleted(self, library_repo, asset_repo, db_session):
"""批量删除已删除的素材 → count 不变。"""
library = library_repo.create(_make_library("project-1"))
asset_ids = []
for _i in range(3):
asset = asset_repo.create(_make_asset(library.id, "project-1"))
asset_ids.append(asset.id)
assert library_repo.get(library.id).asset_count == 3
# 先删除 2 个
asset_repo.batch_delete(asset_ids[:2])
assert library_repo.get(library.id).asset_count == 1
# 再删除同样的 2 个(应被跳过)
deleted_count = asset_repo.batch_delete(asset_ids[:2])
assert deleted_count == 0
assert library_repo.get(library.id).asset_count == 1
def test_count_never_negative(self, library_repo, asset_repo, db_session):
"""计数下限为 0,不会出现负数。"""
library = library_repo.create(_make_library("project-1"))
# 手动设置计数为 0
library.asset_count = 0
library_repo.update(library)
# 尝试递减(通过直接调用 decrement)
library_repo.decrement_asset_count(library.id, count_delta=5)
db_session.commit()
updated = library_repo.get(library.id)
assert updated.asset_count == 0
class TestIdempotentUpload:
"""幂等上传场景:不重复计数。"""
def test_prepare_then_complete_no_double_count(self, library_repo, asset_repo, db_session):
"""prepare 创建占位 + complete 复用占位 → count 只 +1。"""
library = library_repo.create(_make_library("project-1"))
# prepare 阶段:创建占位
placeholder = _make_asset(
library.id,
"project-1",
status=AssetStatus.PROCESSING,
client_upload_id="upload-token-123",
)
asset_repo.create(placeholder)
assert library_repo.get(library.id).asset_count == 1
# complete 阶段:查找已有占位并复用(通过 client_upload_id
existing = asset_repo.find_by_library_and_client_upload_id(
library_id=library.id,
client_upload_id="upload-token-123",
)
assert existing is not None
# 复用占位,不创建新记录 → count 不变
assert library_repo.get(library.id).asset_count == 1
def test_complete_retry_no_double_count(self, library_repo, asset_repo, db_session):
"""complete 重试(通过 file_hash 去重)→ count 只 +1。"""
library = library_repo.create(_make_library("project-1"))
# 第一次 complete:创建素材
asset1 = _make_asset(
library.id,
"project-1",
file_hash="hash-abc-123",
)
asset_repo.create(asset1)
assert library_repo.get(library.id).asset_count == 1
# 重试 complete:通过 file_hash 查找已有
existing = asset_repo.find_by_library_and_file_hash(
library_id=library.id,
file_hash="hash-abc-123",
)
assert existing is not None
assert existing.id == asset1.id
# 不创建新记录 → count 不变
assert library_repo.get(library.id).asset_count == 1
class TestRecountAssets:
"""recount_assets 方法修正计数。"""
def test_recount_fixes_drift(self, library_repo, asset_repo, db_session):
"""计数漂移后,recount 能修正。"""
library = library_repo.create(_make_library("project-1"))
# 创建 3 个素材
for _ in range(3):
asset_repo.create(_make_asset(library.id, "project-1"))
assert library_repo.get(library.id).asset_count == 3
# 手动破坏计数(模拟历史数据问题)
db_session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library.id).update(
{AssetLibraryModel.asset_count: 999, AssetLibraryModel.total_size: 999999}
)
db_session.commit()
assert library_repo.get(library.id).asset_count == 999
# recount 修正
actual = library_repo.recount_assets(library.id)
db_session.commit()
assert actual == 3
assert library_repo.get(library.id).asset_count == 3
assert library_repo.get(library.id).total_size == 1024 * 3
def test_recount_excludes_deleted(self, library_repo, asset_repo, db_session):
"""recount 排除已删除素材。"""
library = library_repo.create(_make_library("project-1"))
assets = []
for _ in range(5):
asset = asset_repo.create(_make_asset(library.id, "project-1"))
assets.append(asset)
assert library_repo.get(library.id).asset_count == 5
# 软删除 2 个
asset_repo.batch_delete([assets[0].id, assets[1].id])
assert library_repo.get(library.id).asset_count == 3
# 破坏计数
db_session.query(AssetLibraryModel).filter(AssetLibraryModel.id == library.id).update(
{AssetLibraryModel.asset_count: 100}
)
db_session.commit()
# recount 应排除 deleted
actual = library_repo.recount_assets(library.id)
db_session.commit()
assert actual == 3
assert library_repo.get(library.id).asset_count == 3
class TestConcurrentSafety:
"""并发安全测试(SQLite 模拟有限并发)。"""
def test_increment_is_atomic(self, library_repo, db_session):
"""increment_asset_count 使用 SQL 级 UPDATE,并发安全。"""
library = library_repo.create(_make_library("project-1"))
# 多次递增
for _ in range(10):
library_repo.increment_asset_count(library.id, count_delta=1, size_delta=100)
db_session.commit()
updated = library_repo.get(library.id)
assert updated.asset_count == 10
assert updated.total_size == 1000
def test_decrement_with_floor_zero(self, library_repo, db_session):
"""decrement_asset_count 下限为 0。"""
library = library_repo.create(_make_library("project-1"))
library.asset_count = 3
library_repo.update(library)
# 尝试递减 10 次
for _ in range(10):
library_repo.decrement_asset_count(library.id, count_delta=1)
db_session.commit()
updated = library_repo.get(library.id)
assert updated.asset_count == 0
@@ -128,12 +128,12 @@ class TestAssetLibraryRepoCounting:
assert lib.asset_count == 0
assert lib.total_size == 0
repo.increment_asset_count(lib.id, 1024)
repo.increment_asset_count(lib.id, size_delta=1024)
fetched = repo.get(lib.id)
assert fetched.asset_count == 1
assert fetched.total_size == 1024
repo.increment_asset_count(lib.id, 2048)
repo.increment_asset_count(lib.id, size_delta=2048)
fetched = repo.get(lib.id)
assert fetched.asset_count == 2
assert fetched.total_size == 3072
@@ -144,7 +144,7 @@ class TestAssetLibraryRepoCounting:
lib.total_size = 3000
repo.create(lib)
repo.decrement_asset_count(lib.id, 1000)
repo.decrement_asset_count(lib.id, size_delta=1000)
fetched = repo.get(lib.id)
assert fetched.asset_count == 2
assert fetched.total_size == 2000
@@ -157,14 +157,14 @@ class TestAssetLibraryRepoCounting:
repo.create(lib)
# 减 2 次,应该被钳制到 0
repo.decrement_asset_count(lib.id, 200)
repo.decrement_asset_count(lib.id, size_delta=200)
fetched = repo.get(lib.id)
assert fetched.asset_count == 0
assert fetched.total_size == 0
def test_increment_nonexistent_library_no_error(self, repo):
"""对不存在的素材库操作,不抛异常也无效果."""
repo.increment_asset_count("nonexistent", 100)
repo.increment_asset_count("nonexistent", size_delta=100)
# 不报错
assert repo.get("nonexistent") is None
+8 -8
View File
@@ -97,40 +97,40 @@ class TestInMemoryAssetLibraryRepository:
def test_increment_asset_count(self, repo, lib_video):
repo.create(lib_video)
repo.increment_asset_count(lib_video.id, 1024)
repo.increment_asset_count(lib_video.id, size_delta=1024)
lib = repo.get(lib_video.id)
assert lib.asset_count == 1
assert lib.total_size == 1024
repo.increment_asset_count(lib_video.id, 512)
repo.increment_asset_count(lib_video.id, size_delta=512)
lib = repo.get(lib_video.id)
assert lib.asset_count == 2
assert lib.total_size == 1536
def test_increment_asset_count_nonexistent(self, repo):
# 不报错,静默忽略
repo.increment_asset_count("nonexistent", 100)
repo.increment_asset_count("nonexistent", size_delta=100)
def test_decrement_asset_count(self, repo, lib_video):
repo.create(lib_video)
repo.increment_asset_count(lib_video.id, 1024)
repo.increment_asset_count(lib_video.id, 512)
repo.increment_asset_count(lib_video.id, size_delta=1024)
repo.increment_asset_count(lib_video.id, size_delta=512)
repo.decrement_asset_count(lib_video.id, 512)
repo.decrement_asset_count(lib_video.id, size_delta=512)
lib = repo.get(lib_video.id)
assert lib.asset_count == 1
assert lib.total_size == 1024
def test_decrement_asset_count_not_below_zero(self, repo, lib_video):
repo.create(lib_video)
repo.decrement_asset_count(lib_video.id, 9999)
repo.decrement_asset_count(lib_video.id, size_delta=9999)
lib = repo.get(lib_video.id)
assert lib.asset_count == 0
assert lib.total_size == 0
def test_decrement_asset_count_nonexistent(self, repo):
repo.decrement_asset_count("nonexistent", 100)
repo.decrement_asset_count("nonexistent", size_delta=100)
# ==================== Tag ====================