Files

183 lines
7.8 KiB
Python

import sys
import os
import json
import pytest
from unittest.mock import MagicMock
import importlib.util
# ComfyUI 依存モジュールのモック化(__init__.pyのインポート時にエラーにならないようにするため)
for mod in ['server', 'folder_paths', 'nodes']:
sys.modules[mod] = MagicMock()
# 親パッケージをシミュレートして相対インポート (from . import snippets) を解決する
package_name = "webui_monaco_prompt_pkg"
# snippets.py をモジュールとしてロードして登録
snippets_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../snippets.py'))
spec_snippets = importlib.util.spec_from_file_location(f"{package_name}.snippets", snippets_path)
snippets_mod = importlib.util.module_from_spec(spec_snippets)
sys.modules[f"{package_name}.snippets"] = snippets_mod
spec_snippets.loader.exec_module(snippets_mod)
# __init__.py をパッケージとしてロードして登録
init_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../__init__.py'))
spec_init = importlib.util.spec_from_file_location(package_name, init_path)
init_mod = importlib.util.module_from_spec(spec_init)
init_mod.__package__ = package_name # 相対インポート解決のためのパッケージ名設定
sys.modules[package_name] = init_mod
spec_init.loader.exec_module(init_mod)
# 正常にロードされたモジュールから関数/クラスを抽出
# (初期状態では AttributeError になるため、TDD Red が達成されます)
try:
WebuiMonacoPromptTemplate = init_mod.WebuiMonacoPromptTemplate
expand_templates = init_mod.expand_templates
except AttributeError as e:
# TDD Red フェーズのために、属性がない場合はテスト実行で落ちるようにダミーを置くか、AttributeErrorをそのまま投げる
raise e
def test_expand_templates_static():
file_map = {
"main.txt": "A girl, <include:outfit/dress.txt>, in the garden.",
"outfit/dress.txt": "wearing a <include:color.txt> dress",
"color.txt": "vibrant blue"
}
# 正常な静的インクルードの展開
result = expand_templates("Hello, <include:main.txt>", file_map, seed=123)
assert result == "Hello, A girl, wearing a vibrant blue dress, in the garden."
def test_expand_templates_random():
file_map = {
"main.txt": "a <random:color.txt> cat",
"color.txt": "black\nwhite\norange\ngray\n\n# 空行や前後の空白テスト用\n brown "
}
# シード 42 での実行
res1 = expand_templates("<include:main.txt>", file_map, seed=42)
# シード 42 で再実行して結果が一致することを確認 (再現性)
res2 = expand_templates("<include:main.txt>", file_map, seed=42)
assert res1 == res2
# 有効な選択肢(black, white, orange, gray, brown)のいずれかであることを確認
valid_colors = ["black", "white", "orange", "gray", "brown"]
color_chosen = res1.replace("a ", "").replace(" cat", "")
assert color_chosen in valid_colors
# 異なるシードで別の値が選択される可能性があることを確認
# (確率的ですが、十分な回数試行すれば異なる値が出ます)
different_results = set()
for s in range(50):
res = expand_templates("<include:main.txt>", file_map, seed=s)
different_results.add(res)
assert len(different_results) > 1
def test_expand_templates_tag_in_tag():
file_map = {
"hoge.txt": "final_output",
"selector.txt": "hoge.txt"
}
# 内側のタグが展開されて hoge.txt になり、最終的に final_output になるテスト
result = expand_templates("<include:<include:selector.txt>>", file_map, seed=0)
assert result == "final_output"
def test_expand_templates_tag_in_tag_random():
file_map = {
"choices.txt": "a.txt\nb.txt",
"a.txt": "Apple",
"b.txt": "Banana"
}
# 動的にインクルード先ファイル名をランダム選択するテスト
results = set()
for s in range(20):
res = expand_templates("<include:<random:choices.txt>>", file_map, seed=s)
results.add(res)
assert results == {"Apple", "Banana"}
def test_expand_templates_mixed_nesting():
file_map = {
"main.txt": "a <random:style_choice.txt> portrait",
"style_choice.txt": "monochrome painting\ncolorful illustration, <include:details.txt>",
"details.txt": "with high contrast"
}
# ランダムに選択された結果にさらに include が含まれているケースのテスト
found_nested = False
for s in range(20):
result = expand_templates("<include:main.txt>", file_map, seed=s)
if "with high contrast" in result:
assert result == "a colorful illustration, with high contrast portrait"
found_nested = True
break
assert found_nested, "Could not trigger nested include from random choice with seed 0-19"
def test_expand_templates_nesting_limit():
# 10層を超える深いネストのテスト (max_depth=10)
file_map = {f"level_{i}.txt": f"<include:level_{i+1}.txt>" for i in range(12)}
file_map["level_12.txt"] = "deepest value"
# level_0.txt から展開すると 12層のネストになるため、上限10を超えて例外を投げるべき
with pytest.raises(ValueError) as excinfo:
expand_templates("<include:level_0.txt>", file_map, seed=0)
assert "[PromptTemplateError]" in str(excinfo.value)
assert "depth limit exceeded" in str(excinfo.value)
def test_expand_templates_circular():
file_map = {
"a.txt": "referencing <include:b.txt>",
"b.txt": "referencing <include:a.txt>"
}
# a.txt -> b.txt -> a.txt の循環参照
with pytest.raises(ValueError) as excinfo:
expand_templates("<include:a.txt>", file_map, seed=0)
# エラーメッセージに循環参照検知の情報と、エラーが発生したファイル名が含まれていることを確認
assert "[PromptTemplateError]" in str(excinfo.value)
assert "Circular reference detected" in str(excinfo.value)
assert "a.txt" in str(excinfo.value) or "b.txt" in str(excinfo.value)
def test_expand_templates_missing():
file_map = {
"main.txt": "referencing <include:nonexistent.txt>"
}
# 存在しないファイルの参照
with pytest.raises(ValueError) as excinfo:
expand_templates("<include:main.txt>", file_map, seed=0)
assert "[PromptTemplateError]" in str(excinfo.value)
assert "not found" in str(excinfo.value)
assert "nonexistent.txt" in str(excinfo.value)
assert "main.txt" in str(excinfo.value)
def test_node_process():
# WebuiMonacoPromptTemplate ノードの process メソッドの検証
node = WebuiMonacoPromptTemplate()
# source_templates (全ファイル)
source_templates = [
json.dumps({"name": "style.txt", "type": "file", "content": "masterpiece, ultra-detailed"}),
json.dumps({"name": "char.txt", "type": "file", "content": "1girl, <include:style.txt>"}),
json.dumps({"name": "background.txt", "type": "file", "content": "in the room"})
]
# entry_points (フィルタリング後の実際に出力したいファイル)
# ここでは char.txt のみを出力対象とし、style.txt は source_templates にのみ存在する状態にする
entry_points = [
json.dumps({"name": "char.txt", "type": "file", "path": "char.txt", "content": "1girl, <include:style.txt>"})
]
contents, jsons = node.process(source_templates, entry_points, seed=[0])
assert len(contents) == 1
assert contents[0] == "1girl, masterpiece, ultra-detailed"
assert len(jsons) == 1
item = json.loads(jsons[0])
assert item["name"] == "char.txt"
assert item["content"] == "1girl, masterpiece, ultra-detailed"