Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ jobs:
with:
enable-cache: true
- run: uv sync
- run: uv run pytest
# カバレッジはジョブログでの可視化のみ (バッジ・ゲートは設けない)
- run: uv run pytest --cov=structured_renamer --cov-report=term

# Tier 2: bpy wheel (5.0.1 / Python 3.11) による実 bpy テスト。
# ローカルの Blender 5.0.1 と同一バージョン。wheel は ~300MB のため
Expand Down
21 changes: 15 additions & 6 deletions dev-note/plans/2026-06-12-milestone.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,21 @@
物理ファイルの実リネームは1回だけ、Undo 不能はサマリーに明示
- [x] `__tmp_` スワップのサービス層テスト(A↔B 入れ替えで .001 なし・残留なし)

### 2. テストの残り
- [ ] **Phase 2.5**: facade / factory の PropertyGroup フェイクテスト
(現状 23% / 31%。UI 同期レイヤ改修と同時が効率的)
- [ ] Tier 2 ターゲット網羅の続き: ShapeKey / Strip / File / Collection
(ID 型は構造同一のため優先度低)
- [ ] props.py 正規表現修正(A-1-4)の自動テスト(Phase 2.5 の枠で)
### 2. テストの残り(2026-06-13 発注 #1 で完了)
- [x] **Phase 2.5**: facade / factory の PropertyGroup フェイクテスト
→ ダックタイピング代役(tests/unit/fakes.py)による Tier 1 で
**facade 94% / factory 100%**(test_pattern_facade.py 21件 /
test_pattern_factory.py 13件)。設計判断と経緯は
testing/01-phase-plan.md の Phase 2.5 を参照
- [x] Tier 2 ターゲット網羅の続き: ShapeKey / Strip / Collection / Material
(ID 型代表)→ test_shapekey_target.py / test_strip_target.py /
test_collection_target.py / test_id_targets.py 追記。
File はヘッドレスで FileSelectEntry を生成できないため、純関数
(サニタイズ・Windows 予約名・拡張子 allowlist)を Tier 1 で固定
(test_file_target_functions.py)
- [x] props.py 正規表現修正(A-1-4)の自動テスト
→ tests/blender/test_props_uniqueness.py(addon_utils.enable で実物
prefs.patterns を駆動。「Name.001.001 増殖」の反転テスト含む6件)

### 3. 負債返済(05章 A-3 / Phase 5)
- [ ] dead code 削除 約2,400行: class_diagram_generator / screen_utils /
Expand Down
62 changes: 57 additions & 5 deletions dev-note/testing/01-phase-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,57 @@ push で CI が緑 → 未確認(次回 push 時に確認)。
core/utils → target 層全体を import する(text_element は register() 内で
遅延 import — 不統一。遅延化すれば要素層→target 層の結合ごと消える)

## Phase 2.5 — facade / factory(PropertyGroup フェイク)と残りターゲット

Phase 2 で除外した UI 同期装置の固定と、Tier 2 ターゲット網羅の続き
(2026-06-13 発注 #1 で実施)。

- [x] PropertyGroup のダックタイピング代役を fakes.py に追加
(FakeElementProperty / FakePatternProperty / FakePrefs / make_fake_context)。
**フェイク戦略の根拠**: factory は入力を hasattr/getattr でしか読まず
(factory.py:103-141)、facade が prefs に求めるのも patterns /
get_active_pattern() の2点だけ (facade.py:48-49, 121, 150, 179)。
Tier 2(addon_utils.enable)案は不採用 — 実行時間と CI 依存を増やさず、
PropertyGroup 実物を通る経路は A-1-4 回帰テストが別途カバーする
- [x] test_pattern_factory.py(13件): ElementConfig 変換(`.values()` を持つ
コレクションの名前リスト化含む)・境界検出のパターン単位一括判定・
BlenderCounter 自動注入・「pattern.validate() エラーは握りつぶすのに
要素レベルの設定不備は ValueError 素通し」という非対称の固定
- [x] test_pattern_facade.py(21件): 初期同期・CRUD・synchronize/force_resync・
例外握りつぶし系(アクティブ ID 未キャッシュ → None、壊れたパターンの
スキップ、prefs 不在 context での構築は KeyError で死ぬ、等)
- [x] props.py A-1-4 の回帰テスト → tests/blender/test_props_uniqueness.py(6件)。
PropertyGroup メソッド内のロジックのため Tier 2 で addon_utils.enable
(default_set=True)→ 実物 prefs.patterns を駆動。「Name.001.001 増殖」の
反転テストを含む。終了時に必ず disable、userpref は保存しない
- [x] File ターゲットの純関数を Tier 1 で固定(test_file_target_functions.py、
32件)— FileSelectEntry はヘッドレス生成不可のため本体は Tier 3(手動)領域
- [x] CI の unit ジョブにカバレッジサマリー追加(`--cov` のジョブログ表示のみ。
バッジ・ゲートは設けない)

**受け入れ条件**: facade / factory のカバレッジ 70%以上 → **達成**
(facade 23%→**94%** / factory 31%→**100%**。
Tier 1 = 166 passed / 6 xfailed, 0.3s / Tier 2 = 39 passed, 0.2s)

**Phase 2.5 の新発見**:
1. factory.create_pattern は `pattern.validate()` のエラーを error ログだけで
握りつぶし、欠陥パターンをそのまま返す(factory.py:49-53 — raise が
コメントアウト)。numeric_counter 欠落はここを素通りして後段 parse_name の
StopIteration(model.py:78-83 FIXME)に到達し得る
2. 一方、要素レベルの設定不備(text の items 空など)は registry.create_element
の ValueError(registry.py:156-157)が factory.py:68 の
`except (KeyError, TypeError)` に捕まらず呼び出し元へ素通しする — 非対称
3. アドオン prefs が引けない context では PatternFacade の構築自体が
get_prefs の KeyError で死ぬ(facade.py:121 経由に例外処理がない)。
context=None なら warning ログだけで空キャッシュ構築に成功する — 不統一
4. addon_utils.enable/disable のサイクルは Blender 終了時に
「Not freed memory blocks: 1(61 bytes)」を残す(exit code への影響なし、
テスト結果にも無害。test_props_uniqueness.py 追加で観測)
5. facade の未カバー6行(facade.py:156-158, 161, 190-191)のうち
`_remove_deleted_patterns` の削除ループは、先行する
`_synchronize_modified_patterns` 内の整合性ループ(facade.py:136-139)が
常に先に削除するため実質到達不能の dead path(検証レビューで確認)

## Phase 3 — 実 bpy 層(Tier 2)

- [x] ローカル実行系: `tests/blender/run.sh`(隔離レシピ込み)→ `runner.py` が
Expand All @@ -116,11 +167,12 @@ push で CI が緑 → 未確認(次回 push 時に確認)。
MRO だけの問題」も併せて証明。xfail(strict) の反転テストも配置済み
- [x] `.001` 相互作用: 引き継ぎ分岐は Tier 1 (test_counter_takeover.py)、
実 bpy.data の重複挙動・ID.rename の3モードは Tier 2 (test_id_targets.py)
- [ ] `__tmp_` 二段リネーム: スワップケース — RenameService 全体の組み立て
(collector + context)が必要なため**保留**。Phase 4 で一括化を実装する際に
サービス層テストとして併せて書くのが効率的
- [ ] 各ターゲットの網羅 — Object / Bone / PoseBone 実施済み。残り(Material,
Mesh 等の ID 型は構造同一のため優先度低、ShapeKey / Strip / File は順次)
- [x] `__tmp_` 二段リネーム: スワップケース → A-2 のサービス層完成時に実施済み
(tests/blender/test_rename_service.py の TestTmpSwap — A↔B 入れ替えで
.001 なし・`__tmp_` 残留なし)
- [x] 各ターゲットの網羅 — Object / Bone / PoseBone / EditBone 実施済み。
2026-06-13 発注 #1 で ShapeKey / Strip / Collection / Material(ID 型代表)を
追加(Phase 2.5 参照)。File はヘッドレス制約により純関数を Tier 1 で固定

**受け入れ条件**: `pytest -m blender` がローカル(Blender 内)で緑 →
**達成**(13 passed / 1 xfailed, 0.13s)。MRO バグテストが「バグを記録」した状態で緑 → **達成**。
Expand Down
70 changes: 70 additions & 0 deletions tests/blender/test_collection_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Tier 2: CollectionRenameTarget の名前操作と名前空間。

Collection は ID 型なので set_name は core/contracts/base.py:342-343 の ID.rename 経路を
通り、Object と同じ enum 文字列 ("RENAMED_NO_COLLISION" 等) を返す。
名前空間は bpy.data.collections 全体 (collection.py:24-28)、
キーは "collections" 固定 (collection.py:18)。
"""

import bpy
import pytest

from structured_renamer.targets.collection import CollectionRenameTarget

pytestmark = pytest.mark.blender


@pytest.fixture()
def two_collections():
"""STDRT_ColA / STDRT_ColB の2コレクション(シーンにはリンクしない)"""
a = bpy.data.collections.new("STDRT_ColA")
b = bpy.data.collections.new("STDRT_ColB")
yield a, b
for col in (a, b):
try:
bpy.data.collections.remove(col)
except ReferenceError:
pass # テスト側で削除済み


class TestCollectionRenameTarget:
def test_get_and_set_name(self, two_collections):
a, _ = two_collections
target = CollectionRenameTarget(a, bpy.context)
assert target.get_name() == "STDRT_ColA"
result = target.set_name("STDRT_ColA_Renamed")
assert a.name == "STDRT_ColA_Renamed"
assert result == "RENAMED_NO_COLLISION" # ID.rename 経路 (core/contracts/base.py:342-343)

def test_create_namespace_sees_all_collections(self, two_collections):
"""名前空間は bpy.data.collections 全体 (collection.py:24-28)。
シーンにリンクしていないコレクションも見える = シーン非依存。
"""
a, b = two_collections
target = CollectionRenameTarget(a, bpy.context)
namespace = target.create_namespace()
assert a.name in namespace
assert b.name in namespace

def test_namespace_key_is_global(self, two_collections):
"""キーは "collections" 固定 (collection.py:18) — ファイル全体で1名前空間"""
a, _ = two_collections
target = CollectionRenameTarget(a, bpy.context)
assert target.get_namespace_key() == "collections"


class TestCollectionCollisionBehavior:
def test_collision_with_mode_never_gets_dot_suffix(self, two_collections):
"""characterization (Blender 5.0.1 で検証): Object と同型 —
ID.rename(mode='NEVER') は衝突時に拒否せず、自分に .001 を付けて
'RENAMED_COLLISION_ADJUSTED' を返す (test_id_targets.py の
test_collision_with_mode_never_gets_dot_suffix と同じ機構の
Collection 版。analyze/03章 §2.4)。
"""
a, b = two_collections
target = CollectionRenameTarget(a, bpy.context)
result = target.set_name(b.name) # 衝突する名前を要求
assert a.name == f"{b.name}.001", "拒否ではなく .001 付きで実行される"
assert b.name == "STDRT_ColB", "相手の名前は守られる"
assert result == "RENAMED_COLLISION_ADJUSTED"
40 changes: 40 additions & 0 deletions tests/blender/test_id_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@
import bpy
import pytest

from structured_renamer.targets.material import MaterialRenameTarget
from structured_renamer.targets.object import ObjectRenameTarget

pytestmark = pytest.mark.blender


@pytest.fixture()
def two_materials():
"""STDRT_MatA / STDRT_MatB の2マテリアル"""
a = bpy.data.materials.new("STDRT_MatA")
b = bpy.data.materials.new("STDRT_MatB")
yield a, b
for mat in (a, b):
try:
bpy.data.materials.remove(mat)
except ReferenceError:
pass # テスト側で削除済み


class TestBlenderNamespaceBehavior:
"""前提となる Blender 自体の挙動の固定 (research_namespace.md の追試)"""

Expand Down Expand Up @@ -75,3 +89,29 @@ def test_force_rename_steals_the_name(self, two_objects):
assert a.name == "STDRT_B"
assert b.name == "STDRT_B.001"
assert result == "RENAMED_COLLISION_FORCED"


class TestMaterialRenameTarget:
"""ID 型代表その2: Material は Object と構造同一 (ID.rename 経路 +
bpy.data.materials 全体の名前空間) であることの最小確認。
詳細な衝突挙動は ID 共通機構として TestObjectRenameTarget 側で固定済み。
"""

def test_get_and_set_name(self, two_materials):
a, _ = two_materials
target = MaterialRenameTarget(a, bpy.context)
assert target.get_name() == "STDRT_MatA"
result = target.set_name("STDRT_MatA_Renamed")
assert a.name == "STDRT_MatA_Renamed"
assert result == "RENAMED_NO_COLLISION" # Object と同じ ID.rename 経路

def test_create_namespace_and_key(self, two_materials):
"""名前空間は bpy.data.materials 全体 (material.py:20-24)、
キーは "materials" 固定 (material.py:14)。
"""
a, b = two_materials
target = MaterialRenameTarget(a, bpy.context)
namespace = target.create_namespace()
assert a.name in namespace
assert b.name in namespace
assert target.get_namespace_key() == "materials"
85 changes: 85 additions & 0 deletions tests/blender/test_props_uniqueness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Tier 2: NamingPatternProperty.add_element の表示名ユニーク化 (A-1-4 の回帰テスト)。

2026-06-12 修正 (commit 35690ab, 05章 A-1-4): props.py:376,381 の
r"(.+)\\.(\\d+)$" は raw 文字列内のリテラルバックスラッシュで "Name.001" に
永遠にマッチせず、重複表示名が "Name.001.001" と増殖していた。

経路が PropertyGroup メソッド内のため、addon_utils.enable で実際に登録された
prefs.patterns 上で検証する (Phase 2.5)。userpref は保存しない
(--factory-startup + 後始末で他テスト・ユーザー設定を汚さない)。
"""

import addon_utils
import bpy
import pytest

from structured_renamer.addon import ADDON_ID

pytestmark = pytest.mark.blender


@pytest.fixture(scope="module")
def addon_prefs():
"""アドオンを有効化して prefs を返す。

default_set=True が必須 (False だと prefs エントリが作られず register が
KeyError で一部失敗する — dev-note/plans/2026-06-12-milestone.md の知見)。
モジュール終了時に必ず disable して素の状態に戻す。
"""
addon_utils.enable(ADDON_ID, default_set=True)
entry = bpy.context.preferences.addons.get(ADDON_ID)
assert entry is not None, "アドオンの有効化に失敗"
yield entry.preferences
addon_utils.disable(ADDON_ID, default_set=True)


@pytest.fixture()
def pattern(addon_prefs):
"""テスト専用パターンを prefs.patterns に追加し、終了時に確実に除去する"""
p = addon_prefs.patterns.add()
p.id = "STDRT_props_uniqueness"
yield p
for i, q in enumerate(addon_prefs.patterns):
if q.id == "STDRT_props_uniqueness":
addon_prefs.patterns.remove(i)
break


class TestAddElementDisplayNameUniqueness:
def test_first_name_is_kept_as_is(self, pattern):
elem = pattern.add_element("e1", "text", "Name")
assert elem.display_name == "Name"

def test_duplicate_gets_dot_001(self, pattern):
pattern.add_element("e1", "text", "Name")
elem = pattern.add_element("e2", "text", "Name")
assert elem.display_name == "Name.001"

def test_third_duplicate_increments_to_002(self, pattern):
pattern.add_element("e1", "text", "Name")
pattern.add_element("e2", "text", "Name")
elem = pattern.add_element("e3", "text", "Name")
assert elem.display_name == "Name.002"

def test_dotted_duplicate_increments_instead_of_growing(self, pattern):
"""A-1-4 の核心: 既に .NNN 付きの名前を重複追加したとき、修正前は
正規表現が一致せず 'Name.001.001' に増殖していた (props.py:373-376)。
正しくは base 'Name' の最大連番 + 1 = 'Name.002'"""
pattern.add_element("e1", "text", "Name")
pattern.add_element("e2", "text", "Name") # → Name.001
elem = pattern.add_element("e3", "text", "Name.001")
assert elem.display_name == "Name.002"

def test_increment_starts_after_existing_max_index(self, pattern):
"""characterization: 連番は既存の最大値 + 1 から (props.py:378-384)。
欠番 (.002〜.004) は再利用されない"""
pattern.add_element("e1", "text", "Name.001")
pattern.add_element("e2", "text", "Name.005")
elem = pattern.add_element("e3", "text", "Name.001")
assert elem.display_name == "Name.006"

def test_unrelated_names_do_not_collide(self, pattern):
pattern.add_element("e1", "text", "Prefix")
elem = pattern.add_element("e2", "text", "Suffix")
assert elem.display_name == "Suffix"
Loading