fixed # 修改进度条表现

add # 新增Ruff代码格式工作流
This commit is contained in:
2026-10-27 23:02:43 +08:00
parent c6b504924e
commit b09d51c7c9
20 changed files with 486 additions and 136 deletions
+96 -12
View File
@@ -1,11 +1,13 @@
from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
from io import BytesIO, StringIO
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from unittest.mock import Mock, patch
from farm_render import bundle
@@ -103,24 +105,43 @@ class BundleTests(unittest.TestCase):
with self.assertRaises(ValueError):
bundle._copy_remote('https://farm.example/a', BytesIO(), 10)
def test_download_reports_throttled_progress_without_urls(self):
def test_download_refreshes_terminal_progress_on_same_line(self):
from rich.console import Console
class Response(BytesIO):
headers = {'Content-Length': '262144'}
def geturl(self):
return 'https://farm.example/private/assets.zip'
with patch.object(bundle, 'urlopen', return_value=Response(
terminal = StringIO()
terminal.isatty = lambda: True
with patch.dict('os.environ', {'TERM': 'xterm-256color'}), \
patch.object(bundle.sys, 'stderr', terminal), \
patch('rich.console.Console', side_effect=lambda **kwargs:
Console(**kwargs, force_terminal=True,
force_interactive=True, legacy_windows=False,
color_system='standard', width=120)), \
patch.object(bundle, 'urlopen', return_value=Response(
b'x' * 262144)), patch.object(
bundle.time, 'monotonic',
side_effect=[0, 0.2, 1.1, 1.2, 2.2]), \
bundle, 'time', SimpleNamespace(monotonic=Mock(
side_effect=[0, 1, 2, 10, 11]))), \
self.assertLogs(bundle.logger, level='INFO') as captured:
bundle._copy_remote('https://farm.example/private/assets.zip',
BytesIO(), 262144)
messages = '\n'.join(captured.output)
self.assertIn('下载开始', messages)
self.assertIn('50.0%', messages)
self.assertIn('100.0%', messages)
self.assertIn('下载完成', messages)
self.assertEqual(messages.count('下载进度'), 2)
self.assertNotIn('下载进度', messages)
raw = terminal.getvalue()
progress = re.sub(r'\x1b\[[0-9;?]*[a-zA-Z]', '', raw)
self.assertIn('0.0%', progress)
self.assertIn('50.0%', progress)
self.assertIn('100.0%', progress)
self.assertIn('eta', progress)
self.assertIn('kB', progress)
self.assertIn('/s', progress)
self.assertGreater(raw.count('\r'), 2)
self.assertIn('\x1b[2K', raw)
self.assertNotIn('farm.example', progress)
self.assertNotIn('farm.example', messages)
self.assertNotIn('private', messages)
@@ -129,12 +150,15 @@ class BundleTests(unittest.TestCase):
headers = {}
def geturl(self):
return 'https://farm.example/a'
with patch.object(bundle, 'urlopen', return_value=Response(b'abc')), \
patch.object(bundle.time, 'monotonic', side_effect=[0, 1.1]), \
output = StringIO()
with patch.object(bundle.sys, 'stderr', output), \
patch.object(bundle, 'urlopen', return_value=Response(b'abc')), \
patch.object(bundle, 'time', SimpleNamespace(
monotonic=Mock(side_effect=[0, 10]))), \
self.assertLogs(bundle.logger, level='INFO') as captured:
bundle._copy_remote('https://farm.example/a', BytesIO(), 10)
self.assertIn('下载完成', '\n'.join(captured.output))
self.assertIn('已下载', '\n'.join(captured.output))
self.assertEqual(output.getvalue(), '')
self.assertNotIn('%', '\n'.join(captured.output))
with patch.object(bundle, 'urlopen', side_effect=OSError('secret')), \
self.assertLogs(bundle.logger, level='INFO') as captured:
@@ -144,3 +168,63 @@ class BundleTests(unittest.TestCase):
self.assertIn('下载失败', messages)
self.assertNotIn('下载完成', messages)
self.assertNotIn('secret', messages)
def test_terminal_progress_is_throttled_and_unknown_size_has_no_percent(self):
terminal = StringIO()
terminal.isatty = lambda: True
with patch.object(bundle.sys, 'stderr', terminal), \
patch('rich.progress.Progress') as rich_progress:
progress = bundle._DownloadProgress('字体包')
progress.update(0, 0, 0)
progress.update(65536, 0, 0.1)
progress.update(131072, 0, 0.2)
progress.close()
display = rich_progress.return_value
self.assertEqual(display.update.call_count, 2)
self.assertEqual(display.update.call_args.kwargs['completed'], 131072)
self.assertEqual(display.update.call_args.kwargs['percentage'], '')
self.assertIsNone(display.add_task.call_args.kwargs['total'])
self.assertFalse(rich_progress.call_args.kwargs['auto_refresh'])
self.assertFalse(rich_progress.call_args.kwargs['redirect_stdout'])
self.assertFalse(rich_progress.call_args.kwargs['redirect_stderr'])
display.stop.assert_called_once()
def test_download_failure_ends_progress_line(self):
class Response(BytesIO):
headers = {'Content-Length': '10'}
def geturl(self):
return 'https://farm.example/a'
def read(self, size):
raise OSError('private error')
terminal = StringIO()
terminal.isatty = lambda: True
with patch.object(bundle.sys, 'stderr', terminal), \
patch.object(bundle, 'urlopen', return_value=Response()), \
self.assertLogs(bundle.logger, level='INFO') as captured:
with self.assertRaises(OSError):
bundle._copy_remote('https://farm.example/a', BytesIO(), 10)
self.assertTrue(terminal.getvalue().endswith('\n'))
self.assertNotIn('100.0%', terminal.getvalue())
self.assertNotIn('下载完成', '\n'.join(captured.output))
def test_progress_display_failure_does_not_abort_download(self):
class Response(BytesIO):
headers = {'Content-Length': '3'}
def geturl(self):
return 'https://farm.example/a'
terminal = StringIO()
terminal.isatty = lambda: True
destination = BytesIO()
with patch.object(bundle.sys, 'stderr', terminal), \
patch.object(bundle, 'urlopen', return_value=Response(b'abc')), \
patch('rich.progress.Progress') as rich_progress:
display = rich_progress.return_value
display.refresh.side_effect = OSError('broken terminal')
bundle._copy_remote('https://farm.example/a', destination, 10)
self.assertEqual(destination.getvalue(), b'abc')
display.stop.assert_called_once()
+1 -1
View File
@@ -32,7 +32,7 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(manifest['version'], version)
self.assertEqual(json.loads(files['metadata.yaml'])['version'], version)
metadata = json.loads(files['metadata.yaml'])
self.assertEqual(metadata['display_name'], '农场')
self.assertEqual(metadata['display_name'], '真寻农场')
self.assertEqual(metadata['support_platforms'], ['aiocqhttp'])
self.assertTrue(metadata['short_desc'])
self.assertEqual(files['logo.png'],
+48
View File
@@ -0,0 +1,48 @@
import ast
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from format_generated import format_generated
class FormatGeneratedTests(unittest.TestCase):
def test_formats_sorts_and_is_idempotent_without_changing_assets(self):
files = {
"package/main.py": (
b"import sys\r\nimport os\r\n\r\n"
b"def run( ):\r\n return 'ok',os.name,sys.version\r\n"
),
"images/example.png": b"unchanged binary\x00",
}
format_generated(ROOT, files)
source = files["package/main.py"]
self.assertTrue(source.startswith(b"import os\nimport sys\n"))
self.assertIn(b' return "ok", os.name, sys.version\n', source)
self.assertNotIn(b"\r", source)
ast.parse(source, feature_version=(3, 10))
self.assertEqual(files["images/example.png"], b"unchanged binary\x00")
first = files.copy()
format_generated(ROOT, files)
self.assertEqual(files, first)
def test_ruff_failure_does_not_partially_update_file_collection(self):
files = {"good.py": b"x=1\n", "bad.py": b"def broken(:\n"}
before = files.copy()
with self.assertRaisesRegex(RuntimeError, "failed Ruff"):
format_generated(ROOT, files)
self.assertEqual(files, before)
def test_rejects_unsafe_paths_and_missing_ruff(self):
for name in ("../outside.py", "C:/outside.py", "/outside.py"):
with self.subTest(name=name):
with self.assertRaisesRegex(ValueError, "Unsafe"):
format_generated(ROOT, {name: b"x=1\n"})
with patch(
"format_generated.importlib.util.find_spec", return_value=None
):
with self.assertRaisesRegex(RuntimeError, "requirements-build"):
format_generated(ROOT, {"main.py": b"x=1\n"})
+6 -2
View File
@@ -25,9 +25,13 @@ class NoneBotPackageTests(unittest.TestCase):
def test_reproducible_standard_package_and_shared_runtime(self):
other = build(ROOT, self.root / "second")
self.assertEqual(self.bundle.read_bytes(), other.read_bytes())
self.assertEqual(self.files["nonebot_plugin_farm/runtime.py"], (
from format_generated import format_generated
expected = {"nonebot_plugin_farm/runtime.py": (
ROOT / "integrations/shared/runtime.py"
).read_bytes())
).read_bytes()}
format_generated(ROOT, expected)
self.assertEqual(self.files["nonebot_plugin_farm/runtime.py"],
expected["nonebot_plugin_farm/runtime.py"])
for suffix in ("farm_core/data/plants.json",
"farm_render/data/asset-manifest.json",
"farm_render/data/font-manifest.json",