You've already forked HeurAMS-legacy
fix
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,7 +16,8 @@ old/
|
|||||||
# config/
|
# config/
|
||||||
data/cache/
|
data/cache/
|
||||||
data/electron/
|
data/electron/
|
||||||
#data/nucleon/
|
data/nucleon/
|
||||||
|
!data/nucleon/test*
|
||||||
data/orbital/
|
data/orbital/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -7,14 +7,14 @@
|
|||||||
"潜进" 软件组项目包含多个子项目
|
"潜进" 软件组项目包含多个子项目
|
||||||
此仓库包含了 "潜进" 项目的核心和基于 Textual 的基本用户界面的实现
|
此仓库包含了 "潜进" 项目的核心和基于 Textual 的基本用户界面的实现
|
||||||
|
|
||||||
## 开发计划
|
## 开发进程
|
||||||
0.0.x: 简易调度器实现与最小原型.
|
- 0.0.x: 简易调度器实现与最小原型.
|
||||||
0.1.x: 命令行操作的调度器.
|
- 0.1.x: 命令行操作的调度器.
|
||||||
0.2.x: 使用 Textual 构建富文本终端用户界面, 项目可行性验证, 采用 SM-2 原始算法, 评估方式为用户自评估的原型.
|
- 0.2.x: 使用 Textual 构建富文本终端用户界面, 项目可行性验证, 采用 SM-2 原始算法, 评估方式为用户自评估的原型.
|
||||||
0.3.x: 简单的多文件项目, 创建了记忆内容/算法数据结构, 基于 SM-2 改进算法的自动复习测评评估. 重点设计古诗文记忆理解功能, 以及 TUI 界面实现, 简单的 TTS 集成.
|
- 0.3.x: 简单的多文件项目, 创建了记忆内容/算法数据结构, 基于 SM-2 改进算法的自动复习测评评估. 重点设计古诗文记忆理解功能, 以及 TUI 界面实现, 简单的 TTS 集成.
|
||||||
0.4.x (当前): 使用模块管理解耦设计, 增加文档与类型标注, 采用上下文设计模式的隐式依赖注入与遵从 IoC, 注册器设计的算法与功能实现, 支持其他调度算法模块 (SM-2, FSRS) 与谜题模块, 采用日志调试, 更新文件格式, 引入动态数据模式(宏驱动的动态内容生成), 与基于文件的策略调控, 更佳的用户数据处理, 加入模块化扩展集成, 将算法数据格式换为 json 提高性能, 采用 provider-service 抽象架构, 支持切换服务提供者, 整体兼容性改进.
|
- 0.4.x: 使用模块管理解耦设计, 增加文档与类型标注, 采用上下文设计模式的隐式依赖注入与遵从 IoC, 注册器设计的算法与功能实现, 支持其他调度算法模块 (SM-2, FSRS) 与谜题模块, 采用日志调试, 更新文件格式, 引入动态数据模式(宏驱动的动态内容生成), 与基于文件的策略调控, 更佳的用户数据处理, 加入模块化扩展集成, 将算法数据格式换为 json 提高性能, 采用 provider-service 抽象架构, 支持切换服务提供者, 整体兼容性改进.
|
||||||
下一步?
|
> 下一步?
|
||||||
使用 Flutter 构建酷酷的现代化前端, 增加云同步/文档源服务...
|
> 使用 Flutter 构建酷酷的现代化前端, 增加云同步/文档源服务...
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
@@ -84,55 +84,6 @@ python -m heurams.interface
|
|||||||
|
|
||||||
配置文件位于 `config/config.toml`(相对于工作目录)。如果不存在,会使用内置的默认配置。
|
配置文件位于 `config/config.toml`(相对于工作目录)。如果不存在,会使用内置的默认配置。
|
||||||
|
|
||||||
### 创建配置文件
|
|
||||||
复制示例配置:
|
|
||||||
```bash
|
|
||||||
cp -r config.example/config.toml config/
|
|
||||||
```
|
|
||||||
|
|
||||||
然后编辑 `config/config.toml` 以符合你的需求。
|
|
||||||
|
|
||||||
### 主要配置项
|
|
||||||
```toml
|
|
||||||
# 调试设置
|
|
||||||
persist_to_file = 1 # 是否将更改保存到文件
|
|
||||||
daystamp_override = -1 # 覆盖日戳(测试用)
|
|
||||||
timestamp_override = -1 # 覆盖时间戳(测试用)
|
|
||||||
quick_pass = 0 # 一键通过模式
|
|
||||||
|
|
||||||
# 每个会话默认新记忆单元数量
|
|
||||||
tasked_number = 8
|
|
||||||
|
|
||||||
# 时区偏移(秒),用于日戳计算
|
|
||||||
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
|
||||||
|
|
||||||
# 谜题默认配置
|
|
||||||
[puzzles.mcq]
|
|
||||||
max_riddles_num = 2 # 选择题最大谜题数
|
|
||||||
|
|
||||||
[puzzles.cloze]
|
|
||||||
min_denominator = 3 # 填空题最小分母
|
|
||||||
|
|
||||||
# 路径配置(相对于工作目录或绝对路径)
|
|
||||||
[paths]
|
|
||||||
nucleon_dir = "./data/nucleon"
|
|
||||||
electron_dir = "./data/electron"
|
|
||||||
orbital_dir = "./data/orbital"
|
|
||||||
cache_dir = "./data/cache"
|
|
||||||
template_dir = "./data/template"
|
|
||||||
|
|
||||||
# 服务提供者配置
|
|
||||||
[services]
|
|
||||||
audio = "playsound" # 可选: playsound, termux
|
|
||||||
tts = "edgetts" # 可选: edgetts
|
|
||||||
llm = "openai" # 可选: openai
|
|
||||||
|
|
||||||
# OpenAI 兼容 LLM 配置
|
|
||||||
[providers.llm.openai]
|
|
||||||
url = "" # API 端点 URL
|
|
||||||
key = "" # API 密钥
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
### 架构图
|
### 架构图
|
||||||
@@ -183,6 +134,7 @@ graph TB
|
|||||||
TUI --> AudioService
|
TUI --> AudioService
|
||||||
TUI --> TTSService
|
TUI --> TTSService
|
||||||
TUI --> OtherServices
|
TUI --> OtherServices
|
||||||
|
Config --> Files
|
||||||
Config --> Context
|
Config --> Context
|
||||||
AudioService --> AudioProvider
|
AudioService --> AudioProvider
|
||||||
TTSService --> TTSProvider
|
TTSService --> TTSProvider
|
||||||
@@ -192,7 +144,6 @@ graph TB
|
|||||||
Reactor --> Puzzles
|
Reactor --> Puzzles
|
||||||
Particles --> Files
|
Particles --> Files
|
||||||
Algorithms --> Files
|
Algorithms --> Files
|
||||||
Config --> Files
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 目录结构
|
### 目录结构
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ daystamp_override = -1
|
|||||||
timestamp_override = -1
|
timestamp_override = -1
|
||||||
|
|
||||||
# [调试] 一键通过
|
# [调试] 一键通过
|
||||||
quick_pass = 0
|
quick_pass = 1
|
||||||
|
|
||||||
# 对于每个项目的默认新记忆原子数量
|
# 对于每个项目的默认新记忆原子数量
|
||||||
tasked_number = 8
|
scheduled_num = 8
|
||||||
|
|
||||||
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
|
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
|
||||||
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "heurams"
|
name = "heurams"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
description = "Heuristic Assisted Memory Scheduler"
|
description = "Heuristic Assisted Memory Scheduler"
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
classifiers = [
|
||||||
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Education",
|
||||||
|
"Intended Audience :: Education",
|
||||||
|
]
|
||||||
|
keywords = ["spaced-repetition", "memory", "learning", "tui", "textual", "flashcards", "education"]
|
||||||
|
dependencies = [
|
||||||
|
"bidict==0.23.1",
|
||||||
|
"playsound==1.2.2",
|
||||||
|
"textual==5.3.0",
|
||||||
|
"toml==0.10.2",
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
bidict==0.23.1
|
bidict==0.23.1
|
||||||
playsound==1.2.2
|
playsound==1.2.2
|
||||||
textual==6.9.0
|
textual==5.3.0
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
|
|||||||
@@ -79,4 +79,8 @@ if is_subdir(Path(rootdir), Path(os.getcwd())):
|
|||||||
environment_check()
|
environment_check()
|
||||||
|
|
||||||
app = HeurAMSApp()
|
app = HeurAMSApp()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
def main():
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -88,9 +88,6 @@ class AboutScreen(Screen):
|
|||||||
def action_go_back(self):
|
def action_go_back(self):
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
def action_quit_app(self):
|
|
||||||
self.app.exit()
|
|
||||||
|
|
||||||
def on_button_pressed(self, event) -> None:
|
def on_button_pressed(self, event) -> None:
|
||||||
event.stop()
|
event.stop()
|
||||||
if event.button.id == "back_button":
|
if event.button.id == "back_button":
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class DashboardScreen(Screen):
|
|||||||
return
|
return
|
||||||
|
|
||||||
selected_label = event.item.query_one(Label)
|
selected_label = event.item.query_one(Label)
|
||||||
if "未找到任何 .toml 文件" in str(selected_label.renderable):
|
if "未找到任何 .toml 文件" in str(selected_label.renderable): # type: ignore
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_filename = pathlib.Path(
|
selected_filename = pathlib.Path(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ logger = get_logger(__name__)
|
|||||||
class MemScreen(Screen):
|
class MemScreen(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "pop_screen", "返回"),
|
("q", "pop_screen", "返回"),
|
||||||
("p", "prev", "复习上一个"),
|
# ("p", "prev", "复习上一个"),
|
||||||
("d", "toggle_dark", ""),
|
("d", "toggle_dark", ""),
|
||||||
("v", "play_voice", "朗读"),
|
("v", "play_voice", "朗读"),
|
||||||
("0,1,2,3", "app.push_screen('about')", ""),
|
("0,1,2,3", "app.push_screen('about')", ""),
|
||||||
@@ -91,6 +91,7 @@ class MemScreen(Screen):
|
|||||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||||
|
|
||||||
def load_puzzle(self):
|
def load_puzzle(self):
|
||||||
|
self.atom: pt.Atom = self.procession.current_atom
|
||||||
container = self.query_one("#puzzle-container")
|
container = self.query_one("#puzzle-container")
|
||||||
for i in container.children:
|
for i in container.children:
|
||||||
i.remove()
|
i.remove()
|
||||||
@@ -108,6 +109,8 @@ class MemScreen(Screen):
|
|||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def watch_rating(self, old_rating, new_rating) -> None:
|
def watch_rating(self, old_rating, new_rating) -> None:
|
||||||
|
if self.procession == 0:
|
||||||
|
return
|
||||||
if new_rating == -1:
|
if new_rating == -1:
|
||||||
return
|
return
|
||||||
forwards = 1 if new_rating >= 4 else 0
|
forwards = 1 if new_rating >= 4 else 0
|
||||||
@@ -131,6 +134,9 @@ class MemScreen(Screen):
|
|||||||
self.procession.append()
|
self.procession.append()
|
||||||
self.update_display()
|
self.update_display()
|
||||||
|
|
||||||
|
def action_quick_pass(self):
|
||||||
|
self.rating = 5
|
||||||
|
|
||||||
def action_play_voice(self):
|
def action_play_voice(self):
|
||||||
"""朗读当前内容"""
|
"""朗读当前内容"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from heurams.context import config_var
|
|||||||
|
|
||||||
class NucleonCreatorScreen(Screen):
|
class NucleonCreatorScreen(Screen):
|
||||||
BINDINGS = [("q", "go_back", "返回")]
|
BINDINGS = [("q", "go_back", "返回")]
|
||||||
|
SUB_TITLE = "单元集创建向导"
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(name=None, id=None, classes=None)
|
super().__init__(name=None, id=None, classes=None)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class PrecachingScreen(Screen):
|
|||||||
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
||||||
desc (list): 可选字符串, 包含对此次调用的文字描述
|
desc (list): 可选字符串, 包含对此次调用的文字描述
|
||||||
"""
|
"""
|
||||||
|
SUB_TITLE = "缓存管理器"
|
||||||
BINDINGS = [("q", "go_back", "返回")]
|
BINDINGS = [("q", "go_back", "返回")]
|
||||||
|
|
||||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||||
|
|||||||
@@ -10,10 +10,15 @@ from textual.widgets import (
|
|||||||
)
|
)
|
||||||
from textual.containers import ScrollableContainer
|
from textual.containers import ScrollableContainer
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
|
from heurams.context import config_var
|
||||||
import heurams.kernel.particles as pt
|
import heurams.kernel.particles as pt
|
||||||
import heurams.services.hasher as hasher
|
import heurams.services.hasher as hasher
|
||||||
from heurams.context import *
|
from heurams.context import *
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PreparationScreen(Screen):
|
class PreparationScreen(Screen):
|
||||||
@@ -27,6 +32,8 @@ class PreparationScreen(Screen):
|
|||||||
("0,1,2,3", "app.push_screen('about')", ""),
|
("0,1,2,3", "app.push_screen('about')", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
scheduled_num = reactive(config_var.get()['scheduled_num'])
|
||||||
|
|
||||||
def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None:
|
def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None:
|
||||||
super().__init__(name=None, id=None, classes=None)
|
super().__init__(name=None, id=None, classes=None)
|
||||||
self.nucleon_file = nucleon_file
|
self.nucleon_file = nucleon_file
|
||||||
@@ -38,9 +45,10 @@ class PreparationScreen(Screen):
|
|||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
with ScrollableContainer(id="vice_container"):
|
with ScrollableContainer(id="vice_container"):
|
||||||
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n")
|
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n")
|
||||||
yield Label(f"内容源文件对象: ./nucleon/[b]{self.nucleon_file.name}[/b]")
|
yield Label(f"内容源文件: {config_var.get()['paths']['nucleon_dir']}/[b]{self.nucleon_file.name}[/b]")
|
||||||
yield Label(f"元数据文件对象: ./electron/[b]{self.electron_file.name}[/b]")
|
yield Label(f"元数据文件: {config_var.get()['paths']['electron_dir']}/[b]{self.electron_file.name}[/b]")
|
||||||
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
|
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
|
||||||
|
yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label")
|
||||||
|
|
||||||
yield Button(
|
yield Button(
|
||||||
"开始记忆",
|
"开始记忆",
|
||||||
@@ -59,6 +67,14 @@ class PreparationScreen(Screen):
|
|||||||
yield Markdown(self._get_full_content().replace("/", ""), classes="full")
|
yield Markdown(self._get_full_content().replace("/", ""), classes="full")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
#def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
|
||||||
|
# logger.debug("响应", old_scheduled_num, "->", new_scheduled_num)
|
||||||
|
# try:
|
||||||
|
# one = self.query_one("#schnum_label")
|
||||||
|
# one.update(f"单次记忆数量: {new_scheduled_num}") # type: ignore
|
||||||
|
# except:
|
||||||
|
# pass
|
||||||
|
|
||||||
def _get_full_content(self):
|
def _get_full_content(self):
|
||||||
content = ""
|
content = ""
|
||||||
for nucleon, orbital in self.nucleons_with_orbital:
|
for nucleon, orbital in self.nucleons_with_orbital:
|
||||||
@@ -84,6 +100,7 @@ class PreparationScreen(Screen):
|
|||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
event.stop()
|
event.stop()
|
||||||
|
logger.debug("按下按钮")
|
||||||
if event.button.id == "start_memorizing_button":
|
if event.button.id == "start_memorizing_button":
|
||||||
atoms = list()
|
atoms = list()
|
||||||
for nucleon, orbital in self.nucleons_with_orbital:
|
for nucleon, orbital in self.nucleons_with_orbital:
|
||||||
@@ -101,9 +118,22 @@ class PreparationScreen(Screen):
|
|||||||
atom.link("electron_path", self.electron_file)
|
atom.link("electron_path", self.electron_file)
|
||||||
atom.link("orbital_path", None)
|
atom.link("orbital_path", None)
|
||||||
atoms.append(atom)
|
atoms.append(atom)
|
||||||
|
atoms_to_provide = list()
|
||||||
|
left_new = self.scheduled_num
|
||||||
|
for i in atoms:
|
||||||
|
i: pt.Atom
|
||||||
|
if i.registry["electron"].is_due():
|
||||||
|
atoms_to_provide.append(i)
|
||||||
|
else:
|
||||||
|
if i.registry["electron"].is_activated():
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
left_new -= 1
|
||||||
|
if left_new >= 0:
|
||||||
|
atoms_to_provide.append(i)
|
||||||
|
logger.debug(f"ATP: {atoms_to_provide}")
|
||||||
from .memorizor import MemScreen
|
from .memorizor import MemScreen
|
||||||
|
memscreen = MemScreen(atoms_to_provide)
|
||||||
memscreen = MemScreen(atoms)
|
|
||||||
self.app.push_screen(memscreen)
|
self.app.push_screen(memscreen)
|
||||||
elif event.button.id == "precache_button":
|
elif event.button.id == "precache_button":
|
||||||
self.action_precache()
|
self.action_precache()
|
||||||
|
|||||||
@@ -105,8 +105,11 @@ class Recognition(BasePuzzleWidget):
|
|||||||
self.handler(5)
|
self.handler(5)
|
||||||
|
|
||||||
def handler(self, rating):
|
def handler(self, rating):
|
||||||
if not self.atom.registry["electron"].is_activated() and not self.atom.registry["runtime"]["locked"]:
|
if not self.atom.registry["runtime"]["locked"]:
|
||||||
|
if not self.atom.registry["electron"].is_activated():
|
||||||
self.atom.registry["electron"].activate()
|
self.atom.registry["electron"].activate()
|
||||||
logger.debug(f"激活原子 {self.atom}")
|
logger.debug(f"激活原子 {self.atom}")
|
||||||
self.atom.lock(1)
|
self.atom.lock(1)
|
||||||
self.atom.minimize(5)
|
self.atom.minimize(5)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
@@ -67,7 +67,7 @@ class Electron:
|
|||||||
logger.debug("Electron.is_due: 检查 ident='%s'", self.ident)
|
logger.debug("Electron.is_due: 检查 ident='%s'", self.ident)
|
||||||
result = self.algo.is_due(self.algodata)
|
result = self.algo.is_due(self.algodata)
|
||||||
logger.debug("is_due 结果: %s", result)
|
logger.debug("is_due 结果: %s", result)
|
||||||
return result
|
return (result and self.is_activated())
|
||||||
|
|
||||||
def is_activated(self):
|
def is_activated(self):
|
||||||
result = self.algodata[self.algo.algo_name]["is_activated"]
|
result = self.algodata[self.algo.algo_name]["is_activated"]
|
||||||
|
|||||||
145
tests/interface/test_dashboard.py
Normal file
145
tests/interface/test_dashboard.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
DashboardScreen 的测试,包括单元测试和 pilot 测试。
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import pathlib
|
||||||
|
import time
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from textual.pilot import Pilot
|
||||||
|
|
||||||
|
from heurams.context import ConfigContext
|
||||||
|
from heurams.services.config import ConfigFile
|
||||||
|
from heurams.interface.__main__ import HeurAMSApp
|
||||||
|
from heurams.interface.screens.dashboard import DashboardScreen
|
||||||
|
|
||||||
|
|
||||||
|
class TestDashboardScreenUnit(unittest.TestCase):
|
||||||
|
"""DashboardScreen 的单元测试(不启动完整应用)。"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""在每个测试之前运行,设置临时目录和配置。"""
|
||||||
|
# 创建临时目录用于测试数据
|
||||||
|
self.temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.temp_path = pathlib.Path(self.temp_dir.name)
|
||||||
|
|
||||||
|
# 创建默认配置,并修改路径指向临时目录
|
||||||
|
default_config_path = (
|
||||||
|
pathlib.Path(__file__).parent.parent.parent
|
||||||
|
/ "src/heurams/default/config/config.toml"
|
||||||
|
)
|
||||||
|
self.config = ConfigFile(default_config_path)
|
||||||
|
# 更新配置中的路径
|
||||||
|
config_data = self.config.data
|
||||||
|
config_data["paths"]["nucleon_dir"] = str(self.temp_path / "nucleon")
|
||||||
|
config_data["paths"]["electron_dir"] = str(self.temp_path / "electron")
|
||||||
|
config_data["paths"]["orbital_dir"] = str(self.temp_path / "orbital")
|
||||||
|
config_data["paths"]["cache_dir"] = str(self.temp_path / "cache")
|
||||||
|
# 禁用快速通过,避免测试干扰
|
||||||
|
config_data["quick_pass"] = 0
|
||||||
|
# 禁用时间覆盖
|
||||||
|
config_data["daystamp_override"] = -1
|
||||||
|
config_data["timestamp_override"] = -1
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
for dir_key in ["nucleon_dir", "electron_dir", "orbital_dir", "cache_dir"]:
|
||||||
|
pathlib.Path(config_data["paths"][dir_key]).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 使用 ConfigContext 设置配置
|
||||||
|
self.config_ctx = ConfigContext(self.config)
|
||||||
|
self.config_ctx.__enter__()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""在每个测试之后清理。"""
|
||||||
|
self.config_ctx.__exit__(None, None, None)
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def test_compose(self):
|
||||||
|
"""测试 compose 方法返回正确的部件。"""
|
||||||
|
screen = DashboardScreen()
|
||||||
|
# 手动调用 compose 并收集部件
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
result = screen.compose()
|
||||||
|
widgets = list(result)
|
||||||
|
# 检查是否包含 Header 和 Footer
|
||||||
|
from textual.widgets import Header, Footer
|
||||||
|
header_present = any(isinstance(w, Header) for w in widgets)
|
||||||
|
footer_present = any(isinstance(w, Footer) for w in widgets)
|
||||||
|
self.assertTrue(header_present)
|
||||||
|
self.assertTrue(footer_present)
|
||||||
|
# 检查是否有 ScrollableContainer
|
||||||
|
from textual.containers import ScrollableContainer
|
||||||
|
container_present = any(isinstance(w, ScrollableContainer) for w in widgets)
|
||||||
|
self.assertTrue(container_present)
|
||||||
|
# 使用 query_one 查找 union-list,即使屏幕未挂载也可能有效
|
||||||
|
list_view = screen.query_one("#union-list")
|
||||||
|
self.assertIsNotNone(list_view)
|
||||||
|
self.assertEqual(list_view.id, "union-list")
|
||||||
|
self.assertEqual(list_view.__class__.__name__, "ListView")
|
||||||
|
|
||||||
|
def test_item_desc_generator(self):
|
||||||
|
"""测试 item_desc_generator 函数。"""
|
||||||
|
screen = DashboardScreen()
|
||||||
|
# 模拟一个文件名
|
||||||
|
filename = "test.toml"
|
||||||
|
result = screen.item_desc_generator(filename)
|
||||||
|
self.assertIsInstance(result, dict)
|
||||||
|
self.assertIn(0, result)
|
||||||
|
self.assertIn(1, result)
|
||||||
|
# 检查内容
|
||||||
|
self.assertIn("test.toml", result[0])
|
||||||
|
# 由于 electron 文件不存在,应显示“尚未激活”
|
||||||
|
self.assertIn("尚未激活", result[1])
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip("Pilot 测试需要进一步配置,暂不运行")
|
||||||
|
class TestDashboardScreenPilot(unittest.TestCase):
|
||||||
|
"""使用 Textual Pilot 的集成测试。"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""配置临时目录和配置。"""
|
||||||
|
self.temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.temp_path = pathlib.Path(self.temp_dir.name)
|
||||||
|
|
||||||
|
default_config_path = (
|
||||||
|
pathlib.Path(__file__).parent.parent.parent
|
||||||
|
/ "src/heurams/default/config/config.toml"
|
||||||
|
)
|
||||||
|
self.config = ConfigFile(default_config_path)
|
||||||
|
config_data = self.config.data
|
||||||
|
config_data["paths"]["nucleon_dir"] = str(self.temp_path / "nucleon")
|
||||||
|
config_data["paths"]["electron_dir"] = str(self.temp_path / "electron")
|
||||||
|
config_data["paths"]["orbital_dir"] = str(self.temp_path / "orbital")
|
||||||
|
config_data["paths"]["cache_dir"] = str(self.temp_path / "cache")
|
||||||
|
config_data["quick_pass"] = 0
|
||||||
|
config_data["daystamp_override"] = -1
|
||||||
|
config_data["timestamp_override"] = -1
|
||||||
|
|
||||||
|
for dir_key in ["nucleon_dir", "electron_dir", "orbital_dir", "cache_dir"]:
|
||||||
|
pathlib.Path(config_data["paths"][dir_key]).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.config_ctx = ConfigContext(self.config)
|
||||||
|
self.config_ctx.__enter__()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.config_ctx.__exit__(None, None, None)
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def test_dashboard_loads_with_pilot(self):
|
||||||
|
"""使用 Pilot 测试 DashboardScreen 加载。"""
|
||||||
|
with patch('heurams.interface.__main__.environment_check'):
|
||||||
|
app = HeurAMSApp()
|
||||||
|
# 注意:Pilot 在 Textual 6.9.0 中的用法可能不同
|
||||||
|
# 以下为示例代码,可能需要调整
|
||||||
|
pilot = Pilot(app)
|
||||||
|
# 等待应用启动
|
||||||
|
pilot.pause()
|
||||||
|
screen = app.screen
|
||||||
|
self.assertEqual(screen.__class__.__name__, "DashboardScreen")
|
||||||
|
union_list = app.query_one("#union-list")
|
||||||
|
self.assertIsNotNone(union_list)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user