优化 Runtime 安装进度体验
- 状态栏与窗口强制刷新,进度更新无需移动鼠标即可实时显示 - 安装过程拆分为 12 个阶段,每段进度从 0 到 100% - 安装阶段解析 uv 下载事件推进进度,模型下载阶段实时反映字节进度
This commit is contained in:
@@ -510,6 +510,46 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("DA3 image depth", starting_source)
|
||||
self.assertNotIn("DA3 depth plane", finish_source)
|
||||
|
||||
def test_update_status_formats_stage_progress(self):
|
||||
operator = self.addon.RunJob()
|
||||
|
||||
self.assertEqual(
|
||||
operator._format_status(
|
||||
{
|
||||
"progress": 0.5,
|
||||
"message": "Downloaded 3 packages",
|
||||
"stage": 3,
|
||||
"stages": 12,
|
||||
"stage_label": "Install PyTorch",
|
||||
},
|
||||
"fallback",
|
||||
),
|
||||
"Step 3/12 Install PyTorch: Downloaded 3 packages",
|
||||
)
|
||||
self.assertEqual(
|
||||
operator._format_status(
|
||||
{
|
||||
"progress": 0.0,
|
||||
"message": "",
|
||||
"stage": 10,
|
||||
"stages": 12,
|
||||
"stage_label": "Download DA3 Small",
|
||||
},
|
||||
"fallback",
|
||||
),
|
||||
"Step 10/12 Download DA3 Small",
|
||||
)
|
||||
self.assertEqual(
|
||||
operator._format_status(
|
||||
{
|
||||
"progress": 0.5,
|
||||
"message": "Estimating depth",
|
||||
},
|
||||
"fallback",
|
||||
),
|
||||
"Estimating depth",
|
||||
)
|
||||
|
||||
def test_depth_background_is_loaded_without_changing_source_empty(self):
|
||||
source = inspect.getsource(self.addon.RunJob._build_depth_plane)
|
||||
|
||||
@@ -1486,7 +1526,56 @@ class AddonTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_force_flag_does_not_trigger_window_draw_and_swap(self):
|
||||
def test_force_flag_triggers_window_draw_and_swap(self):
|
||||
redrawn = []
|
||||
context = SimpleNamespace(
|
||||
window_manager=SimpleNamespace(
|
||||
windows=(
|
||||
SimpleNamespace(
|
||||
screen=SimpleNamespace(
|
||||
areas=(
|
||||
SimpleNamespace(
|
||||
type="STATUSBAR",
|
||||
tag_redraw=lambda: redrawn.append(
|
||||
"status"
|
||||
),
|
||||
regions=(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
workspace=SimpleNamespace(
|
||||
status_text_set_internal=lambda _value: redrawn.append(
|
||||
"status-text"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
self.addon.status_bar.bpy,
|
||||
"ops",
|
||||
SimpleNamespace(
|
||||
wm=SimpleNamespace(
|
||||
redraw_timer=lambda **options: redrawn.append(
|
||||
("swap", options)
|
||||
),
|
||||
),
|
||||
),
|
||||
):
|
||||
self.addon.status_bar.redraw_ui(context, force=True)
|
||||
|
||||
self.assertEqual(
|
||||
redrawn,
|
||||
[
|
||||
"status",
|
||||
("swap", {"type": "DRAW_WIN_SWAP", "iterations": 1}),
|
||||
"status-text",
|
||||
],
|
||||
)
|
||||
|
||||
def test_force_flag_ignores_missing_redraw_helpers(self):
|
||||
redrawn = []
|
||||
context = SimpleNamespace(
|
||||
window_manager=SimpleNamespace(
|
||||
@@ -1511,6 +1600,3 @@ class AddonTest(unittest.TestCase):
|
||||
self.addon.status_bar.redraw_ui(context, force=True)
|
||||
|
||||
self.assertEqual(redrawn, ["status"])
|
||||
source = inspect.getsource(self.addon.status_bar)
|
||||
self.assertNotIn("redraw_timer", source)
|
||||
self.assertNotIn("DRAW_WIN_SWAP", source)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -35,10 +36,16 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
||||
ben2_commit="test-ben2-commit",
|
||||
status=str(root / "status.json"),
|
||||
)
|
||||
with patch.object(bootstrap, "run") as mocked_run:
|
||||
with patch.object(bootstrap, "run") as mocked_run, patch.object(
|
||||
bootstrap,
|
||||
"run_install",
|
||||
) as mocked_run_install:
|
||||
bootstrap.install(args)
|
||||
|
||||
commands = [call.args[0] for call in mocked_run.call_args_list]
|
||||
commands += [
|
||||
call.args[4] for call in mocked_run_install.call_args_list
|
||||
]
|
||||
da3_command = next(
|
||||
command
|
||||
for command in commands
|
||||
@@ -50,15 +57,15 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
||||
)
|
||||
gsplat_call = next(
|
||||
call
|
||||
for call in mocked_run.call_args_list
|
||||
if any("gsplat @" in str(value) for value in call.args[0])
|
||||
for call in mocked_run_install.call_args_list
|
||||
if any("gsplat @" in str(value) for value in call.args[4])
|
||||
)
|
||||
gsplat_command = gsplat_call.args[0]
|
||||
gsplat_command = gsplat_call.args[4]
|
||||
self.assertIn("--no-build-isolation", gsplat_command)
|
||||
self.assertTrue(
|
||||
any(bootstrap.GSPLAT_COMMIT in str(value) for value in gsplat_command)
|
||||
)
|
||||
self.assertEqual(gsplat_call.args[1], {"BUILD_NO_CUDA": "1"})
|
||||
self.assertEqual(gsplat_call.args[5], {"BUILD_NO_CUDA": "1"})
|
||||
|
||||
ben2_command = next(
|
||||
command
|
||||
@@ -106,10 +113,81 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
||||
for command in default_downloads
|
||||
if "PramaLLC/BEN2" in command
|
||||
)
|
||||
self.assertEqual(
|
||||
ben2_download[-1],
|
||||
'["config.json", "model.safetensors"]',
|
||||
patterns = [
|
||||
ben2_download[index + 1]
|
||||
for index, value in enumerate(ben2_download)
|
||||
if value == "--allow-pattern"
|
||||
]
|
||||
self.assertEqual(patterns, ["config.json", "model.safetensors"])
|
||||
self.assertIn("--stage", ben2_download)
|
||||
self.assertIn("--stage-label", ben2_download)
|
||||
self.assertEqual(ben2_download[-1], "Download BEN2 Base")
|
||||
|
||||
def test_run_stage_writes_stage_progress(self):
|
||||
bootstrap = load_bootstrap_runtime()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
status_path = Path(directory) / "status.json"
|
||||
with patch.object(bootstrap, "run") as mocked_run:
|
||||
bootstrap.run_stage(
|
||||
str(status_path),
|
||||
3,
|
||||
12,
|
||||
"Install PyTorch",
|
||||
["uv", "pip", "install", "torch"],
|
||||
None,
|
||||
"plain",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
mocked_run.call_args.args[0],
|
||||
["uv", "pip", "install", "torch"],
|
||||
)
|
||||
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(status["progress"], 1.0)
|
||||
self.assertEqual(status["stage"], 3)
|
||||
self.assertEqual(status["stages"], 12)
|
||||
self.assertEqual(status["stage_label"], "Install PyTorch")
|
||||
|
||||
def test_run_install_tracks_package_download_events(self):
|
||||
bootstrap = load_bootstrap_runtime()
|
||||
|
||||
class FakeProcess:
|
||||
def __init__(self, lines):
|
||||
self.stdout = lines
|
||||
|
||||
def wait(self):
|
||||
return 0
|
||||
|
||||
fake_process = FakeProcess(
|
||||
[
|
||||
"Resolved 3 packages in 1.0s",
|
||||
"Downloading torch (10MiB)",
|
||||
" Downloaded torch",
|
||||
"Prepared 3 packages in 2.0s",
|
||||
"Installed 3 packages in 0.5s",
|
||||
]
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
status_path = Path(directory) / "status.json"
|
||||
with patch.object(
|
||||
bootstrap.subprocess,
|
||||
"Popen",
|
||||
return_value=fake_process,
|
||||
):
|
||||
bootstrap.run_install(
|
||||
str(status_path),
|
||||
2,
|
||||
12,
|
||||
"Install PyTorch",
|
||||
["uv", "pip", "install", "torch"],
|
||||
)
|
||||
|
||||
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(status["progress"], 0.95)
|
||||
self.assertEqual(status["message"], "Installing packages")
|
||||
self.assertEqual(status["stage"], 2)
|
||||
self.assertEqual(status["stages"], 12)
|
||||
self.assertEqual(status["stage_label"], "Install PyTorch")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -29,7 +29,7 @@ class PackTest(unittest.TestCase):
|
||||
finally:
|
||||
pack.DIST_DIR = original_dist
|
||||
|
||||
self.assertEqual(output_path.name, "SnapForm.v0.9.28.zip")
|
||||
self.assertEqual(output_path.name, "SnapForm.v0.9.29.zip")
|
||||
with ZipFile(output_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user