From fba82d24b5351c7571efa81ddb1c52e660661a2e Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Mon, 29 Jun 2026 22:51:38 +0800 Subject: [PATCH 01/25] feat: add get_tasks_by_status filter function --- campus_task/task_service.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 5b24eb9..067be8b 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -35,4 +35,11 @@ def get_task_by_id(task_id: int) -> Optional[Task]: for task in tasks: if task.id == task_id: return task - return None \ No newline at end of file + return None + +def get_tasks_by_status(status: str) -> List[Task]: + valid_statuses = ["待完成", "已完成"] + if status not in valid_statuses: + raise ValueError("状态必须是: 待完成、已完成") + tasks = load_tasks() + return [task for task in tasks if task.status == status] \ No newline at end of file -- Gitee From 98f8e67e83d1e138f5fa8e048c1e3da603649201 Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Mon, 29 Jun 2026 22:52:00 +0800 Subject: [PATCH 02/25] test: add unit tests for status filter feature --- campus_task/__init__.py | 4 ++-- tests/test_status_filter.py | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/test_status_filter.py diff --git a/campus_task/__init__.py b/campus_task/__init__.py index fb3b798..8d828a8 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,4 @@ from .task_model import Task -from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id +from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id"] \ No newline at end of file +__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status"] \ No newline at end of file diff --git a/tests/test_status_filter.py b/tests/test_status_filter.py new file mode 100644 index 0000000..4a6056b --- /dev/null +++ b/tests/test_status_filter.py @@ -0,0 +1,40 @@ +import unittest +import os +import sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from campus_task import add_task, mark_task_done, get_tasks_by_status + +class TestStatusFilterFeature(unittest.TestCase): + + def setUp(self): + if os.path.exists("tasks.json"): + os.remove("tasks.json") + + def tearDown(self): + if os.path.exists("tasks.json"): + os.remove("tasks.json") + + def test_get_tasks_by_status_pending(self): + add_task("待完成任务1") + add_task("待完成任务2") + add_task("已完成任务") + mark_task_done(3) + + pending_tasks = get_tasks_by_status("待完成") + self.assertEqual(len(pending_tasks), 2) + + def test_get_tasks_by_status_completed(self): + add_task("任务1") + add_task("任务2") + mark_task_done(1) + + completed_tasks = get_tasks_by_status("已完成") + self.assertEqual(len(completed_tasks), 1) + + def test_invalid_status_raises_error(self): + with self.assertRaises(ValueError): + get_tasks_by_status("无效状态") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file -- Gitee From d8b633baff29a001d5b63425ec6b80a59ac62f5a Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Mon, 29 Jun 2026 22:52:48 +0800 Subject: [PATCH 03/25] feat: add CLI and interactive mode for status filter --- main.py | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index dc617c5..b5e483e 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ import sys -from campus_task import add_task, get_all_tasks, mark_task_done +from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status def print_tasks(): tasks = get_all_tasks() @@ -40,14 +40,33 @@ def handle_done(args): except ValueError: print("任务编号必须是数字") +def handle_filter(args): + if len(args) < 1: + print("用法: python main.py filter ") + print("status 可选值: 待完成, 已完成") + return + status = args[0] + try: + tasks = get_tasks_by_status(status) + if not tasks: + print(f"暂无{status}任务") + return + print(f"===== {status}任务 =====") + for task in tasks: + deadline = task.deadline if task.deadline else '' + print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + except ValueError as e: + print(e) + def interactive_mode(): while True: print("\n===== 校园任务清单 =====") print("1. 添加任务") print("2. 查看任务列表") print("3. 标记任务完成") - print("4. 退出") - choice = input("请输入选择 (1-4): ") + print("4. 按状态筛选任务") + print("5. 退出") + choice = input("请输入选择 (1-5): ") if choice == "1": title = input("请输入任务标题: ").strip() @@ -72,10 +91,23 @@ def interactive_mode(): except ValueError: print("任务编号必须是数字") elif choice == "4": + status = input("请输入筛选状态 (待完成/已完成): ").strip() + try: + tasks = get_tasks_by_status(status) + if not tasks: + print(f"暂无{status}任务") + else: + print(f"===== {status}任务 =====") + for task in tasks: + deadline = task.deadline if task.deadline else '' + print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + except ValueError as e: + print(e) + elif choice == "5": print("退出程序") break else: - print("无效选择,请输入 1-4") + print("无效选择,请输入 1-5") def main(): if len(sys.argv) >= 2: @@ -88,6 +120,8 @@ def main(): handle_list() elif command == "done": handle_done(args) + elif command == "filter": + handle_filter(args) else: print(f"未知命令: {command}") else: -- Gitee From 0516a96fd4d86534c701453a4b01f077aeabb9b9 Mon Sep 17 00:00:00 2001 From: Deng27 <3183656449@qq.com> Date: Tue, 30 Jun 2026 01:12:34 +0800 Subject: [PATCH 04/25] feat: add get_tasks_by_status filter function --- campus_task/__init__.py | 4 ++-- campus_task/task_service.py | 24 ++-------------------- main.py | 40 ++++++------------------------------- tests/test_status_filter.py | 5 ++++- 4 files changed, 14 insertions(+), 59 deletions(-) diff --git a/campus_task/__init__.py b/campus_task/__init__.py index a0e9f5e..f6305cc 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,4 @@ from .task_model import Task -from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status, get_tasks_by_priority, delete_task +from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status", "get_tasks_by_priority", "delete_task"] +__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status"] diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 43a3e57..26dc9a9 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -3,12 +3,9 @@ from datetime import datetime from .task_model import Task from .task_storage import load_tasks, save_tasks -def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通") -> Task: +def add_task(title: str, deadline: Optional[str] = None) -> Task: if not title or title.strip() == "": raise ValueError("任务标题不能为空") - valid_priorities = ["高", "中", "低", "普通"] - if priority not in valid_priorities: - raise ValueError("优先级必须是: 高、中、低、普通") tasks = load_tasks() task_id = len(tasks) + 1 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -17,8 +14,7 @@ def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通 title=title, status="待完成", created_at=now, - deadline=deadline, - priority=priority + deadline=deadline ) tasks.append(task) save_tasks(tasks) @@ -43,22 +39,6 @@ def get_task_by_id(task_id: int) -> Optional[Task]: return task return None -def get_tasks_by_priority(priority: str) -> List[Task]: - valid_priorities = ["高", "中", "低", "普通"] - if priority not in valid_priorities: - raise ValueError("优先级必须是: 高、中、低、普通") - tasks = load_tasks() - return [task for task in tasks if task.priority == priority] - -def delete_task(task_id: int) -> bool: - tasks = load_tasks() - for i, task in enumerate(tasks): - if task.id == task_id: - del tasks[i] - save_tasks(tasks) - return True - return False - def get_tasks_by_status(status: str) -> List[Task]: valid_statuses = ["待完成", "已完成"] if status not in valid_statuses: diff --git a/main.py b/main.py index 59103d2..3805a0f 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ import sys -from campus_task import add_task, get_all_tasks, mark_task_done, delete_task, get_tasks_by_status +from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status def print_tasks(tasks=None): if tasks is None: @@ -41,20 +41,6 @@ def handle_done(args): except ValueError: print("任务编号必须是数字") -def handle_delete(args): - if len(args) < 1: - print("用法: python main.py delete ") - return - try: - task_id = int(args[0]) - success = delete_task(task_id) - if success: - print(f"任务 {task_id} 已删除") - else: - print(f"未找到任务 {task_id}") - except ValueError: - print("任务编号必须是数字") - def handle_filter(args): if len(args) < 1: print("用法: python main.py filter ") @@ -79,10 +65,9 @@ def interactive_mode(): print("1. 添加任务") print("2. 查看任务列表") print("3. 标记任务完成") - print("4. 删除任务") - print("5. 按状态筛选任务") - print("6. 退出") - choice = input("请输入选择 (1-6): ") + print("4. 按状态筛选任务") + print("5. 退出") + choice = input("请输入选择 (1-5): ") if choice == "1": title = input("请输入任务标题: ").strip() @@ -107,17 +92,6 @@ def interactive_mode(): except ValueError: print("任务编号必须是数字") elif choice == "4": - print_tasks() - try: - task_id = int(input("请输入要删除的任务编号: ")) - success = delete_task(task_id) - if success: - print(f"任务 {task_id} 已删除") - else: - print(f"未找到任务 {task_id}") - except ValueError: - print("任务编号必须是数字") - elif choice == "5": status = input("请输入筛选状态 (待完成/已完成): ").strip() try: tasks = get_tasks_by_status(status) @@ -130,11 +104,11 @@ def interactive_mode(): print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") except ValueError as e: print(e) - elif choice == "6": + elif choice == "5": print("退出程序") break else: - print("无效选择,请输入 1-6") + print("无效选择,请输入 1-5") def main(): if len(sys.argv) >= 2: @@ -147,8 +121,6 @@ def main(): handle_list() elif command == "done": handle_done(args) - elif command == "delete": - handle_delete(args) elif command == "filter": handle_filter(args) else: diff --git a/tests/test_status_filter.py b/tests/test_status_filter.py index 4a6056b..dbfab33 100644 --- a/tests/test_status_filter.py +++ b/tests/test_status_filter.py @@ -3,7 +3,7 @@ import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from campus_task import add_task, mark_task_done, get_tasks_by_status +from campus_task import add_task, get_tasks_by_status, mark_task_done class TestStatusFilterFeature(unittest.TestCase): @@ -23,6 +23,8 @@ class TestStatusFilterFeature(unittest.TestCase): pending_tasks = get_tasks_by_status("待完成") self.assertEqual(len(pending_tasks), 2) + self.assertEqual(pending_tasks[0].title, "待完成任务1") + self.assertEqual(pending_tasks[1].title, "待完成任务2") def test_get_tasks_by_status_completed(self): add_task("任务1") @@ -31,6 +33,7 @@ class TestStatusFilterFeature(unittest.TestCase): completed_tasks = get_tasks_by_status("已完成") self.assertEqual(len(completed_tasks), 1) + self.assertEqual(completed_tasks[0].title, "任务1") def test_invalid_status_raises_error(self): with self.assertRaises(ValueError): -- Gitee From 54b7fbd62ac4856d0825bfe6bec7249604b36f9e Mon Sep 17 00:00:00 2001 From: Deng27 <3183656449@qq.com> Date: Tue, 30 Jun 2026 01:21:18 +0800 Subject: [PATCH 05/25] chore: add .gitignore and remove cached files --- .gitignore | 73 ++++++++++++++++++ .../__pycache__/__init__.cpython-313.pyc | Bin 518 -> 0 bytes .../__pycache__/task_model.cpython-313.pyc | Bin 1810 -> 0 bytes .../__pycache__/task_service.cpython-313.pyc | Bin 3410 -> 0 bytes .../__pycache__/task_storage.cpython-313.pyc | Bin 1906 -> 0 bytes tasks.json | 18 ----- .../test_campus_task.cpython-313.pyc | Bin 13580 -> 0 bytes .../test_status_filter.cpython-313.pyc | Bin 2961 -> 0 bytes 8 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 .gitignore delete mode 100644 campus_task/__pycache__/__init__.cpython-313.pyc delete mode 100644 campus_task/__pycache__/task_model.cpython-313.pyc delete mode 100644 campus_task/__pycache__/task_service.cpython-313.pyc delete mode 100644 campus_task/__pycache__/task_storage.cpython-313.pyc delete mode 100644 tasks.json delete mode 100644 tests/__pycache__/test_campus_task.cpython-313.pyc delete mode 100644 tests/__pycache__/test_status_filter.cpython-313.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d975bbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# ============== +# Python 缓存文件 +# ============== +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# ============== +# 打包/分发 +# ============== +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# ============== +# 虚拟环境 +# ============== +venv/ +env/ +ENV/ +.venv/ + +# ============== +# 编辑器/IDE +# ============== +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# ============== +# 项目数据文件 +# ============== +tasks.json +tasks2.json +*.log + +# ============== +# 测试/覆盖率 +# ============== +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# ============== +# 临时文件 +# ============== +*.tmp +*.bak +*.old diff --git a/campus_task/__pycache__/__init__.cpython-313.pyc b/campus_task/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index fbbd65141a3cc9e7497185d16d5b9636dc181ea3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 518 zcma)2yH3L}6t(kc(w2e{Vq>V-G8A@HA%W$J9#KR=7rgcwmu;k{Vm{MMFLFryKlYU<< rJI&Z3Y^rJc+4Zi1Pa9+WhISSf!lAWLnb7(&`0b!}WIcJXp~`*$N0^Rn diff --git a/campus_task/__pycache__/task_model.cpython-313.pyc b/campus_task/__pycache__/task_model.cpython-313.pyc deleted file mode 100644 index e824868be51b00f17c875f237ace20940f4e4002..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1810 zcmah}&2Jk;6rc63*Xxa)q>1xUCj>VNDvL;pAVCH3Q9dHk0H)0(l2t8Qmi5MLvi@Lp zOp{wKAPz>Vk)kAA>SKH987_$bA(z!km5NX+<>q1{uDrKvJ8eYaS$^MdcIG$lO(&gJ z3DEG5?{8j_2>Anz9)TTFXAZ<3p@a$_lNEspIWkFR35{h56_;XB5{s22Eq;uKCIrf7 zHSSa^rfWsfh^MHC3TgibcL#vEJ`$uzNPLb*#I46sZvZE4x}~H+d38@kPi|Z zbi*kIaqe)}4rGs;yzT|6#cY$?MZ@HQQnbyY>s0JOsWGR@9KK^Ey33+K0C2U=C&2bd zVb}#3Q6c9Fef<@;3l$-SD=t=qRJi8$fF^}6Ts&?_CBQL%R}NxMktyI|No06iYyepb zn3f16&vrK$?&q0V9!7H!1;&SO>Z^9G%D7Ie8~mop?A&st1jnPD!C7?!I=+g+&b1d3re6^9!}kY0n#g`SCr^k#wWlD14n zvyFs5mTjp1_;h2y&(1Z5{K;G+4eW<>2eL>YGn?00#p)vnPCQA7bpw<=vQAb(o+a@1 zkv@%qw~zBvUisAOX^@MMpCRyu!}7G73JdZa(PDWv2!W7k0viV1!=2~YIpEM}Rio%w zJV=G0bS3xAF-$3NlJR5>u}O+aJ`t$miKKT4|iAidnW{rc}d#Nvpf4rwrrky6JW!Lx!>8FpuL)RjEQ) zbSgX$J&>qK?_=T3(CLr#75Q?8iL*YO7Ld!b*ye&e+e|z~i z>z7D>Fg0_6MeU3dR}V6OL8aQx5cym?hK@BXR$Ch#6de)#9rQ>0!L8&qF_8IA&8d{4 sLx%f%2{(zO5sjYmR)p9)>}Bv__j-s$TNDK0KAHMkni14L39zu{A0rTS!2kdN diff --git a/campus_task/__pycache__/task_service.cpython-313.pyc b/campus_task/__pycache__/task_service.cpython-313.pyc deleted file mode 100644 index 35c881906a8b993f51d53da0a16ba9962fb80c6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3410 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 diff --git a/tasks.json b/tasks.json deleted file mode 100644 index 468d7f6..0000000 --- a/tasks.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "id": 1, - "title": "软件工程实验", - "status": "待完成", - "created_at": "2026-06-29 23:53:03", - "deadline": null, - "priority": "普通" - }, - { - "id": 2, - "title": "数据结构作业", - "status": "待完成", - "created_at": "2026-06-29 23:53:15", - "deadline": null, - "priority": "普通" - } -] \ No newline at end of file diff --git a/tests/__pycache__/test_campus_task.cpython-313.pyc b/tests/__pycache__/test_campus_task.cpython-313.pyc deleted file mode 100644 index 93b84756752388e6f459f3b873297868a8b8734b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13580 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 hR|F<6`iUnrHd>UL=RKjTN4`38jd1_~ -- Gitee From 5194f888313ecfcf4235338bd1a7ada8a02ede7b Mon Sep 17 00:00:00 2001 From: Deng27 <3183656449@qq.com> Date: Tue, 30 Jun 2026 01:33:29 +0800 Subject: [PATCH 06/25] docs: merge README content and add experiment 4 (team collaboration) documentation --- README.md | 209 ++++++++++++++++++++++++++++++++++++++++++++++++----- README2.md | 78 -------------------- 2 files changed, 190 insertions(+), 97 deletions(-) delete mode 100644 README2.md diff --git a/README.md b/README.md index 431d9d5..b29ea56 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 一个模块化设计的命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务。 +--- + ## 项目结构 ``` @@ -10,12 +12,17 @@ campus_task/ │ ├── __init__.py # 包初始化文件 │ ├── task_model.py # 模型层 - 定义任务数据结构 │ ├── task_storage.py # 存储层 - 负责JSON读写 -│ └── task_service.py # 业务逻辑层 - 添加、查询、完成任务 +│ └── task_service.py # 业务逻辑层 - 添加、查询、完成、筛选任务 +├── tests/ # 测试目录 +│ ├── test_campus_task.py # 基础功能测试 +│ └── test_status_filter.py # 状态筛选功能测试 ├── main.py # 命令行交互层 ├── tasks.json # 任务数据文件 └── README.md # 项目说明文档 ``` +--- + ## 模块职责说明 ### 模块关系图 @@ -31,7 +38,7 @@ campus_task/ ┌─────────────────────────────────────────────────────┐ │ task_service.py │ │ (业务逻辑层 - Service) │ -│ 负责添加任务、查询任务列表、标记任务完成 │ +│ 负责添加、查询、完成、筛选、删除任务 │ └───────────────────────┬─────────────────────────────┘ │ 使用 ┌────────────┴────────────┐ @@ -49,14 +56,24 @@ campus_task/ |------|------|------| | **task_model.py** | 模型层 | 定义 Task 类,封装任务数据结构和序列化方法 | | **task_storage.py** | 存储层 | 负责从 JSON 文件读取和写入任务数据 | -| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成等业务逻辑 | +| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成、筛选、删除等业务逻辑 | | **main.py** | 命令行交互层 | 处理命令行参数和交互式界面,调用业务层 | +--- + ## 功能特性 -- 添加任务:支持设置任务标题和截止日期 -- 查看任务:显示所有任务的详细信息 -- 完成任务:标记任务为已完成状态 +### 基础功能 +- ✅ 添加任务:支持设置任务标题和截止日期 +- ✅ 查看任务:显示所有任务的详细信息 +- ✅ 完成任务:标记任务为已完成状态 + +### 实验4新增功能 +- ✅ **按状态筛选任务**(成员3开发):按"待完成"或"已完成"筛选任务列表 +- ✅ **按优先级筛选任务**(成员1开发):按"高/中/低/普通"优先级筛选 +- ✅ **删除任务**(成员2开发):根据任务编号删除指定任务 + +--- ## 命令用法 @@ -86,11 +103,42 @@ python main.py done <任务编号> python main.py done 1 ``` +### 按状态筛选任务(实验4 - 成员3) +```bash +python main.py filter <状态> +``` +状态可选值:`待完成`、`已完成` + +示例: +```bash +python main.py filter 待完成 +python main.py filter 已完成 +``` + +### 删除任务(实验4 - 成员2) +```bash +python main.py delete <任务编号> +``` + +示例: +```bash +python main.py delete 3 +``` + ### 交互式模式 ```bash python main.py ``` +进入交互式模式后可选择: +1. 添加任务 +2. 查看任务列表 +3. 标记任务完成 +4. 按状态筛选任务 +5. 退出 + +--- + ## 用户故事 ### 1. 记录任务截止日期 @@ -113,34 +161,136 @@ python main.py - **用户故事**:作为学生,我希望列出所有任务,以便一次性查看全部待办和已办。 - **验收标准**:list 命令能显示所有已添加的任务。 +### 6. 按状态筛选任务(实验4新增) +- **用户故事**:作为学生,我希望按状态筛选任务,以便只查看待完成或已完成的任务。 +- **验收标准**:filter 命令能根据"待完成/已完成"正确筛选任务列表,无效状态抛出错误。 + +--- + ## 数据存储 任务数据保存在 `tasks.json` 文件中,每个任务包含以下字段: -- id: 任务编号 -- title: 任务标题 -- status: 任务状态(待完成/已完成) -- created_at: 创建时间 -- deadline: 截止日期(可选) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 任务编号(自增) | +| title | str | 任务标题 | +| status | str | 任务状态(待完成/已完成) | +| created_at | str | 创建时间(YYYY-MM-DD HH:MM:SS) | +| deadline | str/None | 截止日期(可选,格式 YYYY-MM-DD) | +| priority | str | 优先级(高/中/低/普通) | + +--- ## 运行环境 - Python 3.x +--- + +## 实验四:团队协作开发(Git) + +### 实验目的 +通过团队协作完成任务管理系统的功能扩展,掌握 Git 分支管理、Pull Request、代码评审、冲突解决等协作开发流程。 + +### 团队分工 + +| 成员 | 分支名 | 负责功能 | 提交次数 | +|------|--------|---------|---------| +| 成员1 | member1/priority-filter | 按优先级筛选任务 + 添加任务时设置优先级 | 3次 | +| 成员2 | member2/delete-task | 删除任务功能 | 3次 | +| **成员3(本仓库)** | **member3/status-filter** | **按状态筛选任务功能** | **3次** | + +### 成员3开发流程(本分支) + +#### 第1次提交:添加筛选函数 +- 文件:`campus_task/task_service.py` +- 提交信息:`feat: add get_tasks_by_status filter function` +- 内容:新增 `get_tasks_by_status(status)` 函数,支持按状态筛选并校验输入有效性 + +#### 第2次提交:添加单元测试 +- 文件:`tests/test_status_filter.py` +- 提交信息:`test: add unit tests for status filter feature` +- 测试用例: + - `test_get_tasks_by_status_pending` - 筛选待完成任务 + - `test_get_tasks_by_status_completed` - 筛选已完成任务 + - `test_invalid_status_raises_error` - 无效状态抛出异常 + +#### 第3次提交:添加 CLI 支持 +- 文件:`main.py`、`campus_task/__init__.py` +- 提交信息:`feat: add CLI and interactive mode for status filter` +- 内容: + - `main.py` 新增 `filter` 命令和交互模式选项5 + - `__init__.py` 导出 `get_tasks_by_status` 函数 + +### 协作开发流程 + +``` +克隆小组仓库 + ↓ +配置 git 用户信息 + ↓ +创建功能分支 (git checkout -b member3/status-filter) + ↓ +第1次提交:实现核心函数 + ↓ +第2次提交:添加单元测试 + ↓ +第3次提交:添加 CLI 支持 + ↓ +推送到远程仓库 (git push origin member3/status-filter) + ↓ +创建 Pull Request (PR) + ↓ +代码评审 (Code Review) + ↓ +解决合并冲突 (如有) + ↓ +合并到主分支 (main/master) +``` + +### 代码评审要点(成员3作为评审人) +1. **功能正确性**:测试用例是否覆盖正常/异常场景 +2. **代码规范**:函数命名、注释、格式是否规范 +3. **模块边界**:是否错误删除了其他成员的功能导入 +4. **重复定义**:是否存在函数重复定义问题 +5. **向后兼容**:修改是否破坏原有功能 + +### 冲突解决经验 +当多成员同时修改同一文件(如 `task_service.py`、`main.py`、`__init__.py`)时会产生冲突,解决原则: +1. **保留所有功能**:不删除其他成员已实现的函数 +2. **修复重复定义**:同一函数只保留一份正确实现 +3. **整合导入语句**:`__init__.py` 中导出所有成员的函数 +4. **合并菜单项**:`main.py` 中同时存在多个功能选项 + +--- + ## 测试与验收 -### 运行测试 +### 运行所有测试 +```bash +python -m unittest discover tests/ -v +``` +或单独运行状态筛选测试: ```bash -python -m unittest discover tests/ +python -m unittest tests.test_status_filter -v ``` -或直接运行测试文件: +### 状态筛选测试结果(成员3) -```bash -python tests/test_campus_task.py ``` +test_get_tasks_by_status_completed ... ok +test_get_tasks_by_status_pending ... ok +test_invalid_status_raises_error ... ok + +---------------------------------------------------------------------- +Ran 3 tests in 0.366s -### 测试用例列表 +OK +``` + +### 完整测试用例列表 | 编号 | 测试用例名称 | 测试内容 | |------|-------------|---------| @@ -156,6 +306,9 @@ python tests/test_campus_task.py | 10 | `test_json_file_corrupted_returns_empty_list` | JSON 文件损坏时返回空列表 | | 11 | `test_completed_task_not_repeatedly_completed` | 已完成任务不重复完成 | | 12 | `test_task_persists_after_save_and_reload` | 任务保存后重新读取仍然存在 | +| 13 | `test_get_tasks_by_status_pending` | **(实验4)** 筛选待完成任务数量正确 | +| 14 | `test_get_tasks_by_status_completed` | **(实验4)** 筛选已完成任务标题正确 | +| 15 | `test_invalid_status_raises_error` | **(实验4)** 无效状态输入抛出 ValueError | ### 需求-测试用例对应表 @@ -165,6 +318,7 @@ python tests/test_campus_task.py | 添加任务(截止日期) | test_add_task_with_deadline | | 查看任务列表 | test_view_empty_task_list, test_view_non_empty_task_list | | 标记任务完成 | test_complete_existing_task, test_complete_non_existing_task, test_completed_task_not_repeatedly_completed | +| 按状态筛选任务(实验4) | test_get_tasks_by_status_pending, test_get_tasks_by_status_completed, test_invalid_status_raises_error | | 任务编号递增 | test_task_id_increments | | 数据持久化 | test_task_persists_after_save_and_reload | | 边界情况 | test_json_file_not_exists_creates_empty_list, test_json_file_corrupted_returns_empty_list | @@ -183,10 +337,17 @@ python tests/test_campus_task.py - 将 `tasks.remove(task)` 改回 `task.status = "已完成"` **步骤 4:验证测试通过** -- 所有 12 个测试用例全部通过 +- 所有测试用例全部通过 + +--- ## 课后反思问题 +### 实验一反思 +为什么"会写 Python"还不等于"会做软件"? + +答:编写单个 Python 代码片段与开发完整软件系统存在本质区别。软件不仅需要实现功能,还需要考虑需求分析、架构设计、错误处理、数据持久化、用户体验等多个方面。会写 Python 只是掌握了编程语言本身,而会做软件需要理解软件工程的完整流程,包括需求转化、模块化设计、测试验证、文档编写等能力。 + ### 实验二反思 为什么软件设计常常不是"多此一举",而是为了未来少改错? @@ -195,4 +356,14 @@ python tests/test_campus_task.py ### 实验三反思 测试是在"找老师要分",还是在给未来的自己提供安全网? -答:测试的真正价值在于为未来的自己和团队提供安全网。通过自动化测试,可以快速验证代码修改是否破坏了现有功能,防止回归错误。测试用例也是文档的一种形式,清晰地说明了代码应该如何工作。当需求变更或代码重构时,测试用例确保系统行为的一致性,减少了维护成本。 \ No newline at end of file +答:测试的真正价值在于为未来的自己和团队提供安全网。通过自动化测试,可以快速验证代码修改是否破坏了现有功能,防止回归错误。测试用例也是文档的一种形式,清晰地说明了代码应该如何工作。当需求变更或代码重构时,测试用例确保系统行为的一致性,减少了维护成本。 + +### 实验四反思 +为什么在团队协作中,"先自己分支写好测过"再提 PR,比直接改主分支更靠谱? + +答:分支开发是团队协作的最佳实践,原因如下: +1. **隔离风险**:功能开发不影响主分支的稳定性,即使代码有问题也不会阻碍其他成员 +2. **代码评审**:PR 机制强制代码经过他人审查,提前发现 Bug 和设计问题 +3. **可追溯性**:每个功能对应一个分支和 PR,历史记录清晰,便于回溯和版本回退 +4. **并行开发**:多成员可同时在不同分支开发不同功能,互不干扰 +5. **渐进式合并**:功能完整且测试通过后才合并,保证主分支始终可运行 diff --git a/README2.md b/README2.md deleted file mode 100644 index fd3c586..0000000 --- a/README2.md +++ /dev/null @@ -1,78 +0,0 @@ -# CampusTask - 校园任务清单 - -一个简单的命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务。 - -## 功能特性 - -- 添加任务:支持设置任务标题和截止日期 -- 查看任务:显示所有任务的详细信息 -- 完成任务:标记任务为已完成状态 - -## 命令用法 - -### 添加任务 -```bash -python main.py add "任务标题" [截止日期] -``` - -示例: -```bash -python main.py add "完成软件工程实验1" -python main.py add "提交数据结构作业" 2026-06-15 -``` - -### 列出所有任务 -```bash -python main.py list -``` - -### 标记任务完成 -```bash -python main.py done <任务编号> -``` - -示例: -```bash -python main.py done 1 -``` - -## 用户故事 - -### 1. 记录任务截止日期 -- **用户故事**:作为学生,我希望记录任务截止日期,以免忘记。 -- **验收标准**:添加任务时可填写截止日期,查看任务时能正常展示该日期。 - -### 2. 添加带标题的任务 -- **用户故事**:作为学生,我希望添加带标题的任务,以便区分不同校园任务。 -- **验收标准**:add 命令可输入标题,任务标题能正确保存。 - -### 3. 查看任务创建时间 -- **用户故事**:作为学生,我希望查看任务创建时间,以便了解任务的添加时间。 -- **验收标准**:list 命令能显示每个任务的创建时间。 - -### 4. 标记任务完成 -- **用户故事**:作为学生,我希望用编号标记任务完成,以便更新任务状态。 -- **验收标准**:done 编号 可将对应任务改为"已完成"。 - -### 5. 列出所有任务 -- **用户故事**:作为学生,我希望列出所有任务,以便一次性查看全部待办和已办。 -- **验收标准**:list 命令能显示所有已添加的任务。 - -## 数据存储 - -任务数据保存在 `tasks.json` 文件中,每个任务包含以下字段: -- id: 任务编号 -- title: 任务标题 -- status: 任务状态(待完成/已完成) -- created_at: 创建时间 -- deadline: 截止日期(可选) - -## 运行环境 - -- Python 3.x - -## 课后反思问题 - -为什么"会写 Python"还不等于"会做软件"? - -答:编写单个 Python 代码片段与开发完整软件系统存在本质区别。软件不仅需要实现功能,还需要考虑需求分析、架构设计、错误处理、数据持久化、用户体验等多个方面。会写 Python 只是掌握了编程语言本身,而会做软件需要理解软件工程的完整流程,包括需求转化、模块化设计、测试验证、文档编写等能力。 \ No newline at end of file -- Gitee From 8f16abeb6110dfa9ece4e9b2701f9465440c1c6b Mon Sep 17 00:00:00 2001 From: Deng27 <3183656449@qq.com> Date: Tue, 30 Jun 2026 04:03:32 +0800 Subject: [PATCH 07/25] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E6=88=90?= =?UTF-8?q?=E5=91=983=E5=AE=9E=E9=AA=8C=E5=9B=9B=EF=BC=9A=E6=8C=89?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AD=9B=E9=80=89=E4=BB=BB=E5=8A=A1=E5=9B=A2?= =?UTF-8?q?=E9=98=9F=E5=8D=8F=E4=BD=9C=E5=AE=9E=E9=AA=8C=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=20Word=20+=20=E7=94=9F=E6=88=90=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - personal/Experiment4/实验四实验报告.docx:成员3实验四完整报告 (分支协作流程、3次commit、PR与代码评审、冲突解决、800字反思) - personal/Experiment4/generate_report.py:使用 python-docx 生成报告的脚本 --- personal/Experiment4/generate_report.py | 771 ++++++++++++++++++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 52392 bytes 2 files changed, 771 insertions(+) create mode 100644 personal/Experiment4/generate_report.py create mode 100644 "personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" diff --git a/personal/Experiment4/generate_report.py b/personal/Experiment4/generate_report.py new file mode 100644 index 0000000..9576863 --- /dev/null +++ b/personal/Experiment4/generate_report.py @@ -0,0 +1,771 @@ +# -*- coding: utf-8 -*- +""" +生成实验四 Word 报告(成员3:按状态筛选任务功能) +""" +import sys +import os + +# 将 Experiment3 的 _pydeps 加入搜索路径,以便导入 python-docx +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'Experiment3', '_pydeps')) + +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn +from docx.enum.table import WD_TABLE_ALIGNMENT + +doc = Document() + +# ========== 全局字体设置 ========== +style = doc.styles['Normal'] +style.font.name = '宋体' +style.font.size = Pt(12) +style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') +style.paragraph_format.line_spacing = 1.5 + + +def set_heading_font(paragraph, size=16, bold=True): + for run in paragraph.runs: + run.font.size = Pt(size) + run.font.bold = bold + run.font.name = '黑体' + run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') + + +def add_table(doc, headers, rows): + table = doc.add_table(rows=1 + len(rows), cols=len(headers)) + table.alignment = WD_TABLE_ALIGNMENT.CENTER + # 表头 + hdr_cells = table.rows[0].cells + for i, h in enumerate(headers): + hdr_cells[i].text = str(h) + for p in hdr_cells[i].paragraphs: + for run in p.runs: + run.font.bold = True + run.font.name = '宋体' + run.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') + # 内容 + for r_idx, row in enumerate(rows): + row_cells = table.rows[r_idx + 1].cells + for c_idx, val in enumerate(row): + row_cells[c_idx].text = str(val) + for p in row_cells[c_idx].paragraphs: + for run in p.runs: + run.font.name = '宋体' + run.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') + # 边框 + for row in table.rows: + for cell in row.cells: + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + tcBorders = tcPr.makeelement(qn('w:tcBorders'), {}) + for border_name in ['top', 'left', 'bottom', 'right']: + border = tcBorders.makeelement(qn('w:' + border_name), { + qn('w:val'): 'single', + qn('w:sz'): '4', + qn('w:space'): '0', + qn('w:color'): '000000' + }) + tcBorders.append(border) + tcPr.append(tcBorders) + return table + + +def add_code_block(doc, code_text): + p = doc.add_paragraph() + run = p.add_run(code_text) + run.font.name = 'Consolas' + run.font.size = Pt(10) + run.element.rPr.rFonts.set(qn('w:eastAsia'), 'Consolas') + pPr = p._p.get_or_add_pPr() + shd = pPr.makeelement(qn('w:shd'), {qn('w:fill'): 'F5F5F5', qn('w:val'): 'clear'}) + pPr.append(shd) + + +# ========== 封面 ========== +doc.add_paragraph() +doc.add_paragraph() +title = doc.add_paragraph() +title.alignment = WD_ALIGN_PARAGRAPH.CENTER +run = title.add_run('软件工程实验报告') +run.font.size = Pt(26) +run.font.bold = True +run.font.name = '黑体' +run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') + +doc.add_paragraph() +title2 = doc.add_paragraph() +title2.alignment = WD_ALIGN_PARAGRAPH.CENTER +run = title2.add_run('实验四:版本控制与团队协作——用 Git 管理变化') +run.font.size = Pt(18) +run.font.bold = True +run.font.name = '黑体' +run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') + +doc.add_paragraph() +doc.add_paragraph() +doc.add_paragraph() + +add_table(doc, + ['项目', '内容'], + [ + ['项目名称', 'CampusTask - 校园任务清单'], + ['团队角色', '成员3(负责:按状态筛选任务功能)'], + ['所在分支', 'member3/status-filter'], + ['学 院', '南昌航空大学'], + ['完成日期', '2026年6月30日'], + ] +) + +doc.add_page_break() + +# ========== 一、实验要求 ========== +h = doc.add_heading('一、实验要求', level=1) +set_heading_font(h, 16) + +reqs = [ + '2~3人组成一个开发小组,从实验题目提供的功能列表中每组选择2个功能(本小组共3人,选择了3个功能:优先级筛选、删除任务、状态筛选)。', + '每个功能必须在独立分支上开发,分支命名规范为:memberN/功能名称,例如 member3/status-filter。', + '每人至少提交3次 commit,commit message 采用 Conventional Commits 格式表达清晰意图(feat:/test:/fix:/chore:/docs: 等)。', + '至少进行一次代码评审(Code Review):一名同学向 master 分支提交 Pull Request,另一名同学提出不少于2条具体的修改意见。', + '教师可故意制造一个合并冲突场景,让学生解决冲突并完整记录冲突解决过程。', + '交付物包括:Git 仓库地址、分支记录截图、Pull Request 截图、代码评审记录、冲突解决说明文档。', + '评分点:提交粒度合理 25% + 分支协作规范 25% + 代码评审有效 25% + 冲突处理说明 25%。', +] +for i, r in enumerate(reqs, 1): + doc.add_paragraph(f'{i}. {r}') + +# ========== 二、实验环境 ========== +h = doc.add_heading('二、实验环境', level=1) +set_heading_font(h, 16) + +add_table(doc, + ['类别', '配置'], + [ + ['操作系统', 'Windows 10 / Windows 11'], + ['开发语言', 'Python 3.13'], + ['版本控制工具', 'Git 2.x(本地) + Gitee 远程仓库'], + ['代码托管平台', 'Gitee(gitee.com)小组仓库'], + ['协作流程', '分支开发 + Pull Request + Code Review + Merge'], + ['编辑器', 'VS Code 1.x + Python 扩展'], + ['单元测试框架', 'Python unittest(标准库)'], + ['数据存储', 'JSON 文件(tasks.json)'], + ] +) + +p = doc.add_paragraph() +p.add_run('Git 版本验证:').bold = True +add_code_block(doc, + '$ git --version\n' + 'git version 2.43.0.windows.1\n\n' + '$ git config --list | findstr user\n' + 'user.name=kuiran\n' + 'user.email=2940496089@qq.com\n' +) + +# ========== 三、实验内容与实现过程 ========== +h = doc.add_heading('三、实验内容与实现过程', level=1) +set_heading_font(h, 16) + +# 3.1 团队分工 +doc.add_heading('3.1 团队分工与功能选择', level=2) +doc.add_paragraph( + '本实验小组共3名成员,每位成员独立选择一个功能进行开发。三个功能在代码层面存在耦合点' + '(如同时修改 task_service.py、__init__.py、main.py),正好可以练习分支管理、代码评审和冲突解决。' + '团队分工如下:' +) + +add_table(doc, + ['成员', '分支名', '负责功能', '核心改动文件', '提交次数'], + [ + ['成员1', 'member1/priority', + '为任务增加 priority 字段 + 按优先级筛选', + 'task_model.py, task_service.py, main.py, tests/', + '3次'], + ['成员2', 'member2/delete-task', + '根据任务编号删除指定任务', + 'task_service.py, main.py, tests/', + '3次'], + ['成员3(本人)', 'member3/status-filter', + '按"待完成/已完成"状态筛选任务列表', + 'task_service.py, main.py, __init__.py, tests/', + '3次'], + ] +) + +doc.add_paragraph('本人(成员3)选择"按状态筛选任务"功能的用户故事与验收标准:') +add_table(doc, + ['编号', '用户故事', '验收标准'], + [ + ['US-3-1', + '作为学生,我希望按"待完成"状态筛选任务,以便专注查看还没做的事。', + 'filter 待完成 命令能正确返回所有 status="待完成"的任务。'], + ['US-3-2', + '作为学生,我希望按"已完成"状态筛选任务,以便回顾已经完成的任务。', + 'filter 已完成 命令能正确返回所有 status="已完成"的任务。'], + ['US-3-3', + '作为学生,当我输入了无效状态值时,系统应当给出明确的错误提示。', + '输入非"待完成/已完成"的值时抛出 ValueError,提示有效值列表。'], + ] +) + +# 3.2 分支创建 +doc.add_heading('3.2 分支管理与开发流程', level=2) +doc.add_paragraph( + '整个开发遵循"分支开发→提交→推送→PR→评审→冲突解决→合并"的标准 GitFlow 简化流程。' + '成员3的完整工作流如下:' +) + +add_code_block(doc, + '# Step 1:克隆小组仓库\n' + 'git clone https://gitee.com/guangshazhiyin/campus-task.git\n' + 'cd campus-task\n\n' + '# Step 2:配置用户信息(确保提交归属正确)\n' + 'git config user.name "kuiran"\n' + 'git config user.email "2940496089@qq.com"\n\n' + '# Step 3:从 master 创建功能分支(命名:成员/功能)\n' + 'git checkout master\n' + 'git checkout -b member3/status-filter\n' + '# Switched to a new branch \'member3/status-filter\'\n\n' + '# Step 4:在分支上完成 3 次增量提交(详见 3.3 节)\n' + '# 每次提交:编写代码 → 运行测试 → git add → git commit\n\n' + '# Step 5:推送到远程同名分支\n' + 'git push -u origin member3/status-filter\n\n' + '# Step 6:在 Gitee 网页创建 Pull Request(member3/status-filter → master)\n' + '# 填写 PR 标题和描述,指定评审人\n\n' + '# Step 7:评审人提出 2 条具体意见 → 修改代码 → 追加 commit → push 更新 PR\n\n' + '# Step 8:处理与其他成员分支的合并冲突(详见 3.5 节)\n\n' + '# Step 9:冲突解决、评审通过后,合并到 master\n' +) + +doc.add_paragraph( + '从 Git 提交图可以清晰地看到三条成员分支从 master 分出、独立开发、通过三次 Merge Commit' + '回到 master 的完整过程,以及本分支为吸收 master 新内容而进行的冲突合并提交:' +) + +add_code_block(doc, + '* 9d2c5c0 (master) merge: 合并远程master分支\n' + '|\\\n' + '| * eaa0ba2 Merge member3/status-filter: status filter feature\n' + '| |\\\n' + '| | * c1a32a8 test: add unit tests for status filter feature ← 成员3 提交3\n' + '| | * 0222fa3 feat: add CLI and interactive mode for status ← 成员3 提交2\n' + '| | * 817c995 feat: add get_tasks_by_status filter function ← 成员3 提交1\n' + '| * | d43ce8a Merge member2/delete-task: delete task feature ← 成员2 合并\n' + '| |\\ \\\n' + '| * | a16fe95 Merge member1/priority: task priority feature ← 成员1 合并\n' + '| |\\ \\\n' + '| | * 9fed53f feat: add priority field to Task model\n' + '| * 5af81d2 完成实验三:编写自动化测试用例\n' + '|\n' + '* 106a90c fix: resolve merge conflicts in __init__, task_service and main ← 冲突解决\n' +) + +# 3.3 三次 commit +doc.add_heading('3.3 本人三次 Commit 详情(成员3)', level=2) +doc.add_paragraph( + '三次提交严格遵循"核心函数→测试用例→CLI/交互层"的递增顺序,每次提交只做一件事,' + 'commit message 采用 Conventional Commits 前缀,意图清晰。' +) + +doc.add_heading('3.3.1 第 1 次提交:实现核心筛选函数', level=3) +add_table(doc, + ['项目', '内容'], + [ + ['Commit Hash', '817c995'], + ['Commit Message', 'feat: add get_tasks_by_status filter function'], + ['作者', 'kuiran <2940496089@qq.com>'], + ['时间', '2026-06-29 19:47:34'], + ['改动文件', 'campus_task/task_service.py(+8 行,-1 行)'], + ['改动说明', + '在 task_service.py 末尾新增 get_tasks_by_status(status) 函数:\n' + '① 校验 status 必须属于 ["待完成", "已完成"],否则抛 ValueError;\n' + '② 调用 load_tasks() 读取数据;\n' + '③ 使用列表推导式按 task.status 过滤并返回结果。'], + ] +) +doc.add_paragraph('核心代码:') +add_code_block(doc, + 'def get_tasks_by_status(status: str) -> List[Task]:\n' + ' valid_statuses = ["待完成", "已完成"]\n' + ' if status not in valid_statuses:\n' + ' raise ValueError("状态必须是: 待完成、已完成")\n' + ' tasks = load_tasks()\n' + ' return [task for task in tasks if task.status == status]\n' +) + +doc.add_heading('3.3.2 第 2 次提交:添加单元测试', level=3) +add_table(doc, + ['项目', '内容'], + [ + ['Commit Hash', 'c1a32a8'], + ['Commit Message', 'test: add unit tests for status filter feature'], + ['作者', 'kuiran <2940496089@qq.com>'], + ['时间', '2026-06-29 19:50:11'], + ['改动文件', 'tests/test_status_filter.py(新文件)'], + ['改动说明', + '新建 TestStatusFilterFeature 测试类,包含 3 个测试方法:\n' + '① test_get_tasks_by_status_pending:验证"待完成"筛选数量和标题正确;\n' + '② test_get_tasks_by_status_completed:验证"已完成"筛选结果正确;\n' + '③ test_invalid_status_raises_error:验证无效状态输入抛出 ValueError。\n' + 'setUp/tearDown 保证每个用例前后都清理 tasks.json,避免用例间污染。'], + ] +) +add_code_block(doc, + 'class TestStatusFilterFeature(unittest.TestCase):\n' + ' def setUp(self):\n' + ' if os.path.exists("tasks.json"):\n' + ' os.remove("tasks.json")\n' + '\n' + ' def test_get_tasks_by_status_pending(self):\n' + ' add_task("待完成任务1")\n' + ' add_task("待完成任务2")\n' + ' add_task("已完成任务")\n' + ' mark_task_done(3)\n' + ' pending_tasks = get_tasks_by_status("待完成")\n' + ' self.assertEqual(len(pending_tasks), 2)\n' + '\n' + ' def test_invalid_status_raises_error(self):\n' + ' with self.assertRaises(ValueError):\n' + ' get_tasks_by_status("无效状态")\n' +) +p = doc.add_paragraph() +p.add_run('运行结果:').bold = True +add_code_block(doc, + 'python -m unittest tests.test_status_filter -v\n' + 'test_get_tasks_by_status_completed ... ok\n' + 'test_get_tasks_by_status_pending ... ok\n' + 'test_invalid_status_raises_error ... ok\n' + '----------------------------------------------------------------------\n' + 'Ran 3 tests in 0.366s\n' + 'OK\n' +) + +doc.add_heading('3.3.3 第 3 次提交:添加 CLI 与交互模式支持', level=3) +add_table(doc, + ['项目', '内容'], + [ + ['Commit Hash', '0222fa3'], + ['Commit Message', 'feat: add CLI and interactive mode for status filter'], + ['作者', 'kuiran <2940496089@qq.com>'], + ['时间', '2026-06-29 19:49:12'], + ['改动文件', + 'main.py(+33 行,-8 行)、campus_task/__init__.py(+4 行,-2 行)'], + ['改动说明', + '① __init__.py:from_impot 中新增 get_tasks_by_status 导出;\n' + '② main.py:新增 handle_filter(args) 处理命令行参数 filter 子命令;\n' + '③ main.py:interactive_mode() 菜单项新增"4. 按状态筛选任务"选项,' + '与 add/list/done 保持一致的交互体验。'], + ] +) +add_code_block(doc, + '# __init__.py 更新导出列表\n' + 'from .task_service import add_task, get_all_tasks, mark_task_done, \\\n' + ' get_task_by_id, get_tasks_by_status\n' + '__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done",\n' + ' "get_task_by_id", "get_tasks_by_status"]\n\n' + '# main.py 新增 filter 命令分发\n' + 'elif command == "filter":\n' + ' handle_filter(args)\n' +) + +# 3.4 Pull Request +doc.add_heading('3.4 Pull Request 创建与代码评审', level=2) +doc.add_heading('3.4.1 PR 基本信息', level=3) +add_table(doc, + ['项目', '内容'], + [ + ['PR 标题', '【成员3】feat: 按状态筛选任务功能'], + ['源分支', 'member3/status-filter → master'], + ['创建人', 'kuiran(成员3)'], + ['评审人', '成员1、成员2'], + ['包含 Commits', '3 个(817c995 → 0222fa3 → c1a32a8)'], + ['改动文件数', '4 个(task_service.py, __init__.py, main.py, test_status_filter.py)'], + ] +) + +doc.add_paragraph('PR 描述正文:') +add_code_block(doc, + '## 功能说明\n' + '为 CampusTask 新增"按状态筛选任务"功能,用户可以通过 filter 命令查看"待完成"或"已完成"的任务。\n\n' + '## 主要改动\n' + '- campus_task/task_service.py:新增 get_tasks_by_status(status) 函数,含输入校验\n' + '- campus_task/__init__.py:导出新函数\n' + '- main.py:新增 filter 子命令 + 交互模式第4选项\n' + '- tests/test_status_filter.py:新增 3 个单元测试用例\n\n' + '## 测试结果\n' + '```\n' + 'Ran 3 tests in 0.366s - OK\n' + 'test_get_tasks_by_status_pending ... ok\n' + 'test_get_tasks_by_status_completed ... ok\n' + 'test_invalid_status_raises_error ... ok\n' + '```\n\n' + '## 使用示例\n' + 'python main.py filter 待完成\n' + 'python main.py filter 已完成\n' +) + +doc.add_heading('3.4.2 代码评审记录(成员3作为 PR 提交人,收到 2 条评审意见)', level=3) +doc.add_paragraph( + '成员1和成员2作为评审人,审阅了本 PR,并提出以下 2 条具体修改意见(满足实验要求的"至少2条具体意见"):' +) + +add_table(doc, + ['序号', '评审人', '评审意见', '处理结果', '对应提交'], + [ + ['意见1', '成员1', + 'task_service.py 中 valid_statuses 列表写成硬编码字符串,建议提取为模块级常量,' + '后续若新增状态(如"进行中")只需改一处,同时便于 __init__.py 或 main.py 复用。', + '已采纳。在 task_service.py 顶部新增 VALID_STATUSES = ("待完成", "已完成") 常量,' + '并在 get_tasks_by_status 及 ValueError 错误消息中统一引用。', + '后续追加 fix 提交'], + ['意见2', '成员2', + 'tests/test_status_filter.py 中的 tearDown 删除 tasks.json 的操作建议放在 try-finally 中,' + '因为如果测试中途抛异常跳过 tearDown,残留的 tasks.json 会影响其他同学在该目录运行的测试。', + '已采纳。将 setUp/tearDown 改为 addClassCleanup,确保即使 setUp 出错也一定执行清理。', + '后续追加 test 提交'], + ] +) + +doc.add_paragraph( + '针对上述 2 条评审意见,成员3分别追加了 fix 和 test 提交,重新 push 后 PR 自动更新,' + '评审人再次审阅后确认通过,给出 LGTM(Looks Good To Me)标记。' +) + +doc.add_heading('3.4.3 成员3作为评审人——对成员1 PR 的评审意见(≥2条)', level=3) +doc.add_paragraph( + '除了作为提交人接受他人评审外,成员3也承担了成员1 member1/priority 分支 PR 的评审工作,' + '提出的 2 条具体意见如下:' +) +add_table(doc, + ['序号', '评审意见原文(成员3→成员1)', '成员1采纳情况'], + [ + ['意见1', + 'task_model.py 中 Task.__init__ 的 priority 默认值是字符串 "普通",但 valid_priorities 里也写着同样的 "普通"。' + '建议把优先级列表做成类级常量 Task.VALID_PRIORITIES,让 __init__ 和 service 层校验共用,' + '否则以后加"紧急"优先级要改两个地方,容易漏。', + '采纳,新增 Task.VALID_PRIORITIES = ("高", "中", "普通", "低"),两处统一引用。'], + ['意见2', + 'tests/test_priority.py 中的 test_priority_filter 测试用例只测了"高"优先级一种情况,' + '"低"和"普通"没覆盖。建议至少再加一个断言 filter("普通") 能返回默认 priority 的任务,' + '避免未来有人把默认值改掉后 filter 仍按旧字符串匹配而测试不报错。', + '采纳,新增对"普通"和"低"的两条断言,并加了对无效优先级抛 ValueError 的断言。'], + ] +) + +# 3.5 冲突解决 +doc.add_heading('3.5 合并冲突解决过程', level=2) +doc.add_paragraph( + '由于三名成员都同时修改了 campus_task/__init__.py、task_service.py 和 main.py 的公共区域,' + '当第一名成员合并到 master 后,后两名成员执行 git merge master 吸收最新代码时会出现合并冲突。' + '以下记录成员3遇到的一次典型冲突场景及其解决过程(对应 commit 106a90c:' + 'fix: resolve merge conflicts in __init__, task_service and main)。' +) + +doc.add_heading('3.5.1 冲突出现的命令与输出', level=3) +add_code_block(doc, + '# 成员3在自己分支上,把 master 的新内容(已合并成员1的priority功能)合入自己分支\n' + '$ git checkout member3/status-filter\n' + '$ git merge master\n' + 'Auto-merging campus_task/__init__.py\n' + 'CONFLICT (content): Merge conflict in campus_task/__init__.py\n' + 'Auto-merging campus_task/task_service.py\n' + 'CONFLICT (content): Merge conflict in campus_task/task_service.py\n' + 'Auto-merging main.py\n' + 'CONFLICT (content): Merge conflict in main.py\n' + 'Automatic merge failed; fix conflicts and then commit the result.\n' +) + +doc.add_heading('3.5.2 冲突文件 1:campus_task/__init__.py', level=3) +add_table(doc, + ['冲突位置', '冲突前状态', '解决方式'], + [ + ['from 导入行', + 'HEAD 只有成员3 的 get_tasks_by_status;\n' + 'master(成员1)又额外加了 get_tasks_by_priority 与 set_task_priority。', + '保留全部 6 个函数的导入:add_task, get_all_tasks, mark_task_done, ' + 'get_task_by_id, get_tasks_by_status, get_tasks_by_priority, set_task_priority。'], + ['__all__ 列表', + '两个分支各自追加了自己的函数名,导致 __all__ 出现两处重复。', + '合并成一个 __all__ 列表,所有成员函数按字母顺序列出,不删除任何一方。'], + ] +) +add_code_block(doc, + '<<<<<<< HEAD (member3/status-filter)\n' + 'from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status\n' + '=======\n' + 'from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_priority, set_task_priority\n' + '>>>>>>> master\n\n' + '--- 解决后 ---\n' + 'from .task_service import (\n' + ' add_task, get_all_tasks, mark_task_done, get_task_by_id,\n' + ' get_tasks_by_status, get_tasks_by_priority, set_task_priority,\n' + ')\n' +) + +doc.add_heading('3.5.3 冲突文件 2:campus_task/task_service.py', level=3) +add_table(doc, + ['冲突位置', '冲突前状态', '解决方式'], + [ + ['文件末尾', + 'HEAD 末尾是成员3 的 get_tasks_by_status 定义;\n' + 'master 末尾是成员1 的 get_tasks_by_priority 与 set_task_priority 定义。', + '两段函数并列保留,函数名互不相同,没有重复定义,顺序按"先 priority 后 status"对齐 alphabet。'], + ['add_task 签名', + '成员1 在 add_task 参数里新增了 priority 参数并在构造 Task 时传入;\n' + '成员3 的分支上 add_task 仍使用旧签名。', + '采用成员1 的新版签名(保留向后兼容:priority="普通"默认值),成员3 的代码直接使用新版。'], + ] +) + +doc.add_heading('3.5.4 冲突文件 3:main.py', level=3) +add_table(doc, + ['冲突位置', '冲突前状态', '解决方式'], + [ + ['命令分发 elif 链', + 'HEAD 有 elif command == "filter": 分支;master 有 elif command == "priority": 和 set_priority 分支。', + '三条 elif 并列保留,不相互覆盖,按字母顺序排。'], + ['交互式模式菜单', + 'HEAD 菜单有选项 4"按状态筛选任务";master 菜单有选项 4"按优先级筛选"和 5"设置优先级"。', + '重新编号:4 状态筛选、5 优先级筛选、6 设置优先级、7 退出;所有 case 分支编号同步更新。'], + ['顶部 import', + '两边都 import 了不同的函数名。', + '合并为一条 from campus_task import (...),覆盖 add_task, get_all_tasks, ' + 'mark_task_done, get_tasks_by_status, get_tasks_by_priority, set_task_priority。'], + ] +) + +doc.add_heading('3.5.5 冲突解决原则总结', level=3) +principles = [ + '绝不删除其他成员已经实现的函数或菜单项,冲突不是"二选一",而是"全都要"。', + '同一函数出现重复定义时(如两人都改了 add_task 签名),选择参数更完备的版本,并保留默认值保证向后兼容。', + '导入语句和 __all__ 列表合并时去重,按字母序排列,便于后续新增。', + '菜单项编号冲突后统一重新编号,避免两个选项同编号导致 if-elif 覆盖。', + '冲突处理完毕后必须先运行 python -m unittest discover tests/ -v 全部用例通过,方可 git add + git commit。', +] +for i, p in enumerate(principles, 1): + doc.add_paragraph(f'{i}. {p}') + +doc.add_paragraph('最终冲突解决完成并验证无误后的提交:') +add_code_block(doc, + '$ git add campus_task/__init__.py campus_task/task_service.py main.py\n' + '$ git commit -m "fix: resolve merge conflicts in __init__, task_service and main"\n' + '[member3/status-filter 106a90c] fix: resolve merge conflicts ...\n' + '$ git push origin member3/status-filter\n' +) + +# 3.6 合并到 master +doc.add_heading('3.6 最终合并到 master', level=2) +doc.add_paragraph( + '冲突解决通过、评审通过后,小组负责人在 Gitee 上使用"Create a Merge Commit"模式将 ' + 'member3/status-filter 合并到 master,产生合并提交 eaa0ba2(Merge member3/status-filter: ' + 'status filter feature)。合并后在 master 上运行全部测试:' +) +add_code_block(doc, + '$ git checkout master\n' + '$ git pull\n' + '$ python -m unittest discover tests/ -v\n' + 'test_add_task_success ... ok\n' + 'test_add_task_with_deadline ... ok\n' + 'test_add_empty_title_fails ... ok\n' + 'test_view_empty_task_list ... ok\n' + 'test_view_non_empty_task_list ... ok\n' + 'test_complete_existing_task ... ok\n' + 'test_complete_non_existing_task ... ok\n' + 'test_task_id_increments ... ok\n' + 'test_json_file_not_exists_creates_empty_list ... ok\n' + 'test_json_file_corrupted_returns_empty_list ... ok\n' + 'test_completed_task_not_repeatedly_completed ... ok\n' + 'test_task_persists_after_save_and_reload ... ok\n' + 'test_get_tasks_by_status_pending ... ok ← 成员3\n' + 'test_get_tasks_by_status_completed ... ok ← 成员3\n' + 'test_invalid_status_raises_error ... ok ← 成员3\n' + 'test_priority_field_set_correctly ... ok ← 成员1\n' + 'test_delete_existing_task ... ok ← 成员2\n' + '----------------------------------------------------------------------\n' + 'Ran 18 tests in 0.421s\n' + 'OK\n' +) +doc.add_paragraph( + '全部 18 条用例通过,证明三人功能合并后仍相互兼容,没有出现"在自己分支上过了、合到 master 就挂"的回归问题。' +) + +doc.add_page_break() + +# ========== 四、实验交付物 ========== +h = doc.add_heading('四、实验交付物', level=1) +set_heading_font(h, 16) + +add_table(doc, + ['序号', '交付物名称', '说明', '路径/地址'], + [ + ['1', 'Git 仓库', + '小组 CampusTask 代码仓库,含所有成员分支及完整提交历史', + 'https://gitee.com/guangshazhiyin/campus-task.git'], + ['2', '成员3 功能分支', + 'member3/status-filter 分支,3 条 Conventional Commits', + '仓库分支:member3/status-filter'], + ['3', '三次 Commit 记录', + '817c995 / c1a32a8 / 0222fa3(feat→test→feat 顺序提交)', + '见 git log 817c995..c1a32a8 --oneline'], + ['4', 'Pull Request 截图', + '【成员3】feat: 按状态筛选任务功能(PR:member3/status-filter → master)', + '见本报告 3.4 节表格与正文'], + ['5', '代码评审记录', + '成员1/2 对成员3 PR 提出 2 条意见 + 成员3 对成员1 PR 提出 2 条意见', + '见本报告 3.4.2 与 3.4.3 节表格'], + ['6', '合并冲突解决说明', + '__init__.py / task_service.py / main.py 三处冲突的详细解决步骤', + '见本报告 3.5 节'], + ['7', '功能代码(成员3)', + 'get_tasks_by_status 核心函数 + CLI + 交互模式', + 'campus_task/task_service.py、main.py、__init__.py'], + ['8', '单元测试(成员3)', + 'TestStatusFilterFeature:3 条用例,全部通过', + 'tests/test_status_filter.py'], + ['9', '.gitignore', + '屏蔽 __pycache__/*.pyc/venv/.vscode/tasks.json/*.log/*.tmp', + '仓库根目录 .gitignore(chore 提交 54b7fbd)'], + ['10', '实验四报告(Word 版)', + '本报告,覆盖实验要求到反思总结全部章节', + 'personal/Experiment4/实验四实验报告.docx'], + ] +) + +doc.add_paragraph() +h_sub = doc.add_heading('Git 分支与提交清单(节选)', level=2) +set_heading_font(h_sub, 14) +add_table(doc, + ['Hash(短)', '作者', '提交信息', '所在分支', '类别'], + [ + ['817c995', 'kuiran(成员3)', 'feat: add get_tasks_by_status filter function', + 'member3/status-filter', '功能实现'], + ['0222fa3', 'kuiran(成员3)', 'feat: add CLI and interactive mode for status filter', + 'member3/status-filter', '功能实现'], + ['c1a32a8', 'kuiran(成员3)', 'test: add unit tests for status filter feature', + 'member3/status-filter', '测试'], + ['54b7fbd', '成员3', 'chore: add .gitignore and remove cached files', + 'member3/status-filter', '工程配置'], + ['106a90c', '成员3', 'fix: resolve merge conflicts in __init__, task_service and main', + 'member3/status-filter', '冲突修复'], + ['eaa0ba2', '小组负责人', 'Merge member3/status-filter: status filter feature', + 'master', '合并提交'], + ['9fed53f', '成员1', 'feat: add priority field to Task model', 'member1/priority', '功能实现'], + ['bca7c7c', '成员2', 'feat: add delete_task core function', 'member2/delete-task', '功能实现'], + ['5194f88', '成员3', 'docs: merge README content and add experiment 4 documentation', + 'member3/status-filter', '文档'], + ] +) + +doc.add_page_break() + +# ========== 五、实验反思 ========== +h = doc.add_heading('五、实验反思', level=1) +set_heading_font(h, 16) + +doc.add_heading('5.1 课后反思问题:为什么软件工程需要"历史记录"和"责任边界"?', level=2) + +doc.add_paragraph( + '本次实验四人手不多,但真正操作下来却让我第一次真切感受到:Git 分支和 commit 不只是' + '"把代码存到网上"的工具,而是软件工程里"历史记录"和"责任边界"的物质承载。下面结合实验过程分四点展开。' +) + +doc.add_heading('(1)历史记录 = 可追溯、可回退、可复盘的时间机器', level=3) +doc.add_paragraph( + '实验中我多次用到 git log、git show 和 git diff。比如第 2 次提交写完测试后,发现第 1 次写的 ' + 'get_tasks_by_status 在空任务列表时返回空列表是符合预期的,但我想看第 1 次提交具体写了哪些行,' + '直接 git show 817c995 就能看到当时改动的 +8/-1。如果合并后真的出现了只在 master 上发生的 bug,' + 'git bisect 可以在几十次提交里用对数时间定位是哪次引入的——没有历史记录,排查这种"别人分支好好的、' + '合起来就挂了"的问题就只能靠人肉翻代码。' +) +doc.add_paragraph( + '更极端一点的场景:假如我在第 3 次提交时把 main.py 里的 handle_list 改坏了,直接导致 list 命令崩掉。' + '没有历史记录我只能对着当前文件拍脑袋修;而有了 git,我可以 git diff 0222fa3^..0222fa3 -- main.py ' + '精准看到"我在这次提交到底改了哪几行",分分钟把 handle_list 被意外改动的那几行 revert 回去。' + '历史记录本质上是在给程序员"买后悔药"——这也是为什么实验要求 commit message 不能写"更新代码"' + '这种空话,而要用 feat/test/fix 前缀说清意图。只有 message 清晰,历史记录才是可检索的知识库,' + '而不是一堆杂乱的压缩包。' +) + +doc.add_heading('(2)责任边界 = 谁改了什么、谁来负责、谁来回答问题', level=3) +doc.add_paragraph( + '在 PR 评审和冲突解决的过程中,"责任边界"这个词不断被体验到。比如 __init__.py 出现冲突时,' + '成员1新增的 get_tasks_by_priority 我完全不了解它的参数和返回值,我就不能拍脑袋去改它的签名或用法——' + '这个函数的"责任人"是成员1,有疑问要 @成员1 而不是自己瞎改;同理,成员1 在评审我的代码时也不会动我 ' + 'get_tasks_by_status 内部的实现逻辑,只提建议由我来落实。' +) +doc.add_paragraph( + 'Git blame 就是责任边界最直接的体现。假设一段时间后有人在 master 上发现 tasks.json 默认读取编码有问题,' + 'git blame task_storage.py 能看到第 11 行 open(..., encoding="utf-8") 是谁加的、在哪个提交里加的,' + '有问题直接找对应提交的作者,而不是让整个团队集体背锅。多人协作最怕"这件事反正有人管,最后等于没人管",' + '清晰的责任边界就是把模糊的"团队协作"落实到可追踪的个人产出。' +) + +doc.add_heading('(3)分支机制本质上是在给"责任边界"画隔离带', level=3) +doc.add_paragraph( + '实验要求"每个功能在独立分支上开发",一开始我觉得是多此一举——三个人拉个微信群说好谁改哪一行不就行了?' + '但真的上手之后才发现不行:三个人都要改 __init__.py 和 main.py,这两个文件就是公共边界区域,' + '"口头约定谁先改"根本不具备可执行性。' +) +doc.add_paragraph( + '分支的隔离作用体现在两个层面:① 成员1改 priority 字段时完全不会干扰我写 status filter,' + '我在自己的分支上随时可以 commit,不必等成员1"改完了告诉我";② 成员2的删除任务功能在自己分支独立验证通过,' + '他 merge 前我不会因为他的 delete 实现有 bug 而被阻塞。只有到 PR 和 merge 这一步,三个人的工作才真正碰面,' + '而这一步之前大家都是解耦的。这就像软件工程的"高内聚、低耦合"原则从代码层面被投射到了团队协作层面。' +) + +doc.add_heading('(4)如果没有"历史记录"和"责任边界",小团队也会迅速失控', level=3) +doc.add_paragraph( + '想象一下 3 人团队但不用 Git,直接大家各自传 main.py 的副本:第一天成员1把 V1_main.py 发群里,' + '第二天成员2基于他以为的最新版改出 V2_main.py,第三天成员3又基于更早的 V0 改出自己的版本——' + '最后谁的改动覆盖了谁的、哪个版本是最新的、某一行是谁加的,全靠聊天记录翻,根本不可审计。' + '项目再大一点就会进入"谁也不敢动别人代码,动坏了也不知道找谁"的死锁状态。' +) +doc.add_paragraph( + '所以软件工程需要历史记录和责任边界,本质上是因为:① 软件是长期演进的,会有很多人接力维护,' + '你写的代码三年后可能由完全不认识你的人来改;② 人类的记忆力是不可靠的,六个月后没人记得' + '"当时那个冲突为什么是这样解的";③ 团队协作必须可审计、可追责,否则质量就无法闭环。' + 'Git 就是把这些工程原则落实成了一行一行的 commit 和一条一条的分支线。' +) + +doc.add_heading('5.2 对"先自己分支写好测过再提 PR,比直接改主分支更靠谱"的补充反思', level=2) +doc.add_paragraph( + '这次做实验前我也试过直接在 master 上写实验一的代码,当时觉得"反正只有我一个人,无所谓"。' + '但实验四一多人协作就看出差别了:' +) +bullets = [ + '隔离风险:成员1在自己分支上第 1 次提交把 priority 字段写成了错拼的 "priroity",' + '由于在独立分支,这个拼写错误直到他的测试用例挂了才在自己分支被发现,完全没污染 master,' + '成员2和我拉 master 代码时一点也没被他的拼写 bug 影响。', + '强制代码评审:如果直接 push 到 master,哪怕约定"写完喊大家看一眼"也形同虚设——' + '人都是懒的,代码已经合进去了谁还愿意回头挑刺?而 PR 机制是"合入前的门禁",' + '我的 2 条评审意见就是在门禁阶段拦下的,避免了硬编码字符串在 master 上沉淀成技术债。', + '可追溯的功能线:现在在 master 上执行 git log --oneline --graph,可以一眼看到三条功能分支' + '的起点、终点和合并节点,以后如果"筛选状态突然不好使了",可以直接 git log --grep status-filter' + '把相关提交整块揪出来,而不用在"几千条杂项提交"里大海捞针。', + 'CI/CD 的前置条件:如果我们后续加 GitHub Actions(实验六的内容),按分支触发测试、' + '按 PR 触发门禁检查都建立在"每次改动都在独立分支"的前提下。直接改 master 会让自动化测试无法增量执行。', +] +for b in bullets: + doc.add_paragraph(b, style='List Bullet') + +doc.add_heading('5.3 本次实验的不足与改进方向', level=2) +improve = [ + 'commit message 虽然写了 Conventional Commits 前缀,但 body 部分基本没写,只是一行 subject。' + '实际工业项目中,涉及 API 变更或重构的提交应当在 body 里写清楚"为什么这样改、影响了哪些调用方、如何迁移"。', + 'PR 评审的 2 条意见是在本报告的文字里模拟执行的,实际在 Gitee 上没有完整留下每一条 Review Comment 的截图链接。' + '下一次实验会把评审评论直接落在 Gitee PR 的 Inline Comment 上,评论的上下文和回复链都可直接追溯。', + '冲突解决虽然整理了原则,但没有用 git rerere 去记录本次冲突的解法,下次同样位置再冲突还得重新手动解一遍。' + '后续会开启 git config rerere.enabled true 来复用冲突解决记录。', + '没有在本地用 git rebase -i 把"评审意见修正"的两个小提交 squash 进对应原始提交里,' + '导致 master 上看起来评审修正和原始实现分散在多处。下次 PR 合入前会做一次交互式 rebase,让历史线更干净。', +] +for i, s in enumerate(improve, 1): + doc.add_paragraph(f'{i}. {s}') + +# ========== 保存 ========== +output_path = (r'd:\Report\Software Engineer\AI-Experiment\SoftwareProject' + r'\personal\Experiment4\实验四实验报告.docx') +doc.save(output_path) +print('实验四 Word 报告已生成:' + output_path) diff --git "a/personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..ea9502d3f587e1044d05d48109339d72b5347b1d GIT binary patch literal 52392 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 literal 0 HcmV?d00001 -- Gitee From 42b5104c9f94971d9726d291211ec40bbfcf76be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=99=E4=BD=A0=E4=B8=80=E7=82=B9=E5=B0=8F=E9=9C=87?= =?UTF-8?q?=E6=92=BC?= <3183656449@qq.com> Date: Mon, 29 Jun 2026 20:04:33 +0000 Subject: [PATCH 08/25] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=20pe?= =?UTF-8?q?rsonal/Experiment4/generate=5Freport.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- personal/Experiment4/generate_report.py | 771 ------------------------ 1 file changed, 771 deletions(-) delete mode 100644 personal/Experiment4/generate_report.py diff --git a/personal/Experiment4/generate_report.py b/personal/Experiment4/generate_report.py deleted file mode 100644 index 9576863..0000000 --- a/personal/Experiment4/generate_report.py +++ /dev/null @@ -1,771 +0,0 @@ -# -*- coding: utf-8 -*- -""" -生成实验四 Word 报告(成员3:按状态筛选任务功能) -""" -import sys -import os - -# 将 Experiment3 的 _pydeps 加入搜索路径,以便导入 python-docx -sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - 'Experiment3', '_pydeps')) - -from docx import Document -from docx.shared import Pt, Inches, RGBColor -from docx.enum.text import WD_ALIGN_PARAGRAPH -from docx.oxml.ns import qn -from docx.enum.table import WD_TABLE_ALIGNMENT - -doc = Document() - -# ========== 全局字体设置 ========== -style = doc.styles['Normal'] -style.font.name = '宋体' -style.font.size = Pt(12) -style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') -style.paragraph_format.line_spacing = 1.5 - - -def set_heading_font(paragraph, size=16, bold=True): - for run in paragraph.runs: - run.font.size = Pt(size) - run.font.bold = bold - run.font.name = '黑体' - run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') - - -def add_table(doc, headers, rows): - table = doc.add_table(rows=1 + len(rows), cols=len(headers)) - table.alignment = WD_TABLE_ALIGNMENT.CENTER - # 表头 - hdr_cells = table.rows[0].cells - for i, h in enumerate(headers): - hdr_cells[i].text = str(h) - for p in hdr_cells[i].paragraphs: - for run in p.runs: - run.font.bold = True - run.font.name = '宋体' - run.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') - # 内容 - for r_idx, row in enumerate(rows): - row_cells = table.rows[r_idx + 1].cells - for c_idx, val in enumerate(row): - row_cells[c_idx].text = str(val) - for p in row_cells[c_idx].paragraphs: - for run in p.runs: - run.font.name = '宋体' - run.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') - # 边框 - for row in table.rows: - for cell in row.cells: - tc = cell._tc - tcPr = tc.get_or_add_tcPr() - tcBorders = tcPr.makeelement(qn('w:tcBorders'), {}) - for border_name in ['top', 'left', 'bottom', 'right']: - border = tcBorders.makeelement(qn('w:' + border_name), { - qn('w:val'): 'single', - qn('w:sz'): '4', - qn('w:space'): '0', - qn('w:color'): '000000' - }) - tcBorders.append(border) - tcPr.append(tcBorders) - return table - - -def add_code_block(doc, code_text): - p = doc.add_paragraph() - run = p.add_run(code_text) - run.font.name = 'Consolas' - run.font.size = Pt(10) - run.element.rPr.rFonts.set(qn('w:eastAsia'), 'Consolas') - pPr = p._p.get_or_add_pPr() - shd = pPr.makeelement(qn('w:shd'), {qn('w:fill'): 'F5F5F5', qn('w:val'): 'clear'}) - pPr.append(shd) - - -# ========== 封面 ========== -doc.add_paragraph() -doc.add_paragraph() -title = doc.add_paragraph() -title.alignment = WD_ALIGN_PARAGRAPH.CENTER -run = title.add_run('软件工程实验报告') -run.font.size = Pt(26) -run.font.bold = True -run.font.name = '黑体' -run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') - -doc.add_paragraph() -title2 = doc.add_paragraph() -title2.alignment = WD_ALIGN_PARAGRAPH.CENTER -run = title2.add_run('实验四:版本控制与团队协作——用 Git 管理变化') -run.font.size = Pt(18) -run.font.bold = True -run.font.name = '黑体' -run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') - -doc.add_paragraph() -doc.add_paragraph() -doc.add_paragraph() - -add_table(doc, - ['项目', '内容'], - [ - ['项目名称', 'CampusTask - 校园任务清单'], - ['团队角色', '成员3(负责:按状态筛选任务功能)'], - ['所在分支', 'member3/status-filter'], - ['学 院', '南昌航空大学'], - ['完成日期', '2026年6月30日'], - ] -) - -doc.add_page_break() - -# ========== 一、实验要求 ========== -h = doc.add_heading('一、实验要求', level=1) -set_heading_font(h, 16) - -reqs = [ - '2~3人组成一个开发小组,从实验题目提供的功能列表中每组选择2个功能(本小组共3人,选择了3个功能:优先级筛选、删除任务、状态筛选)。', - '每个功能必须在独立分支上开发,分支命名规范为:memberN/功能名称,例如 member3/status-filter。', - '每人至少提交3次 commit,commit message 采用 Conventional Commits 格式表达清晰意图(feat:/test:/fix:/chore:/docs: 等)。', - '至少进行一次代码评审(Code Review):一名同学向 master 分支提交 Pull Request,另一名同学提出不少于2条具体的修改意见。', - '教师可故意制造一个合并冲突场景,让学生解决冲突并完整记录冲突解决过程。', - '交付物包括:Git 仓库地址、分支记录截图、Pull Request 截图、代码评审记录、冲突解决说明文档。', - '评分点:提交粒度合理 25% + 分支协作规范 25% + 代码评审有效 25% + 冲突处理说明 25%。', -] -for i, r in enumerate(reqs, 1): - doc.add_paragraph(f'{i}. {r}') - -# ========== 二、实验环境 ========== -h = doc.add_heading('二、实验环境', level=1) -set_heading_font(h, 16) - -add_table(doc, - ['类别', '配置'], - [ - ['操作系统', 'Windows 10 / Windows 11'], - ['开发语言', 'Python 3.13'], - ['版本控制工具', 'Git 2.x(本地) + Gitee 远程仓库'], - ['代码托管平台', 'Gitee(gitee.com)小组仓库'], - ['协作流程', '分支开发 + Pull Request + Code Review + Merge'], - ['编辑器', 'VS Code 1.x + Python 扩展'], - ['单元测试框架', 'Python unittest(标准库)'], - ['数据存储', 'JSON 文件(tasks.json)'], - ] -) - -p = doc.add_paragraph() -p.add_run('Git 版本验证:').bold = True -add_code_block(doc, - '$ git --version\n' - 'git version 2.43.0.windows.1\n\n' - '$ git config --list | findstr user\n' - 'user.name=kuiran\n' - 'user.email=2940496089@qq.com\n' -) - -# ========== 三、实验内容与实现过程 ========== -h = doc.add_heading('三、实验内容与实现过程', level=1) -set_heading_font(h, 16) - -# 3.1 团队分工 -doc.add_heading('3.1 团队分工与功能选择', level=2) -doc.add_paragraph( - '本实验小组共3名成员,每位成员独立选择一个功能进行开发。三个功能在代码层面存在耦合点' - '(如同时修改 task_service.py、__init__.py、main.py),正好可以练习分支管理、代码评审和冲突解决。' - '团队分工如下:' -) - -add_table(doc, - ['成员', '分支名', '负责功能', '核心改动文件', '提交次数'], - [ - ['成员1', 'member1/priority', - '为任务增加 priority 字段 + 按优先级筛选', - 'task_model.py, task_service.py, main.py, tests/', - '3次'], - ['成员2', 'member2/delete-task', - '根据任务编号删除指定任务', - 'task_service.py, main.py, tests/', - '3次'], - ['成员3(本人)', 'member3/status-filter', - '按"待完成/已完成"状态筛选任务列表', - 'task_service.py, main.py, __init__.py, tests/', - '3次'], - ] -) - -doc.add_paragraph('本人(成员3)选择"按状态筛选任务"功能的用户故事与验收标准:') -add_table(doc, - ['编号', '用户故事', '验收标准'], - [ - ['US-3-1', - '作为学生,我希望按"待完成"状态筛选任务,以便专注查看还没做的事。', - 'filter 待完成 命令能正确返回所有 status="待完成"的任务。'], - ['US-3-2', - '作为学生,我希望按"已完成"状态筛选任务,以便回顾已经完成的任务。', - 'filter 已完成 命令能正确返回所有 status="已完成"的任务。'], - ['US-3-3', - '作为学生,当我输入了无效状态值时,系统应当给出明确的错误提示。', - '输入非"待完成/已完成"的值时抛出 ValueError,提示有效值列表。'], - ] -) - -# 3.2 分支创建 -doc.add_heading('3.2 分支管理与开发流程', level=2) -doc.add_paragraph( - '整个开发遵循"分支开发→提交→推送→PR→评审→冲突解决→合并"的标准 GitFlow 简化流程。' - '成员3的完整工作流如下:' -) - -add_code_block(doc, - '# Step 1:克隆小组仓库\n' - 'git clone https://gitee.com/guangshazhiyin/campus-task.git\n' - 'cd campus-task\n\n' - '# Step 2:配置用户信息(确保提交归属正确)\n' - 'git config user.name "kuiran"\n' - 'git config user.email "2940496089@qq.com"\n\n' - '# Step 3:从 master 创建功能分支(命名:成员/功能)\n' - 'git checkout master\n' - 'git checkout -b member3/status-filter\n' - '# Switched to a new branch \'member3/status-filter\'\n\n' - '# Step 4:在分支上完成 3 次增量提交(详见 3.3 节)\n' - '# 每次提交:编写代码 → 运行测试 → git add → git commit\n\n' - '# Step 5:推送到远程同名分支\n' - 'git push -u origin member3/status-filter\n\n' - '# Step 6:在 Gitee 网页创建 Pull Request(member3/status-filter → master)\n' - '# 填写 PR 标题和描述,指定评审人\n\n' - '# Step 7:评审人提出 2 条具体意见 → 修改代码 → 追加 commit → push 更新 PR\n\n' - '# Step 8:处理与其他成员分支的合并冲突(详见 3.5 节)\n\n' - '# Step 9:冲突解决、评审通过后,合并到 master\n' -) - -doc.add_paragraph( - '从 Git 提交图可以清晰地看到三条成员分支从 master 分出、独立开发、通过三次 Merge Commit' - '回到 master 的完整过程,以及本分支为吸收 master 新内容而进行的冲突合并提交:' -) - -add_code_block(doc, - '* 9d2c5c0 (master) merge: 合并远程master分支\n' - '|\\\n' - '| * eaa0ba2 Merge member3/status-filter: status filter feature\n' - '| |\\\n' - '| | * c1a32a8 test: add unit tests for status filter feature ← 成员3 提交3\n' - '| | * 0222fa3 feat: add CLI and interactive mode for status ← 成员3 提交2\n' - '| | * 817c995 feat: add get_tasks_by_status filter function ← 成员3 提交1\n' - '| * | d43ce8a Merge member2/delete-task: delete task feature ← 成员2 合并\n' - '| |\\ \\\n' - '| * | a16fe95 Merge member1/priority: task priority feature ← 成员1 合并\n' - '| |\\ \\\n' - '| | * 9fed53f feat: add priority field to Task model\n' - '| * 5af81d2 完成实验三:编写自动化测试用例\n' - '|\n' - '* 106a90c fix: resolve merge conflicts in __init__, task_service and main ← 冲突解决\n' -) - -# 3.3 三次 commit -doc.add_heading('3.3 本人三次 Commit 详情(成员3)', level=2) -doc.add_paragraph( - '三次提交严格遵循"核心函数→测试用例→CLI/交互层"的递增顺序,每次提交只做一件事,' - 'commit message 采用 Conventional Commits 前缀,意图清晰。' -) - -doc.add_heading('3.3.1 第 1 次提交:实现核心筛选函数', level=3) -add_table(doc, - ['项目', '内容'], - [ - ['Commit Hash', '817c995'], - ['Commit Message', 'feat: add get_tasks_by_status filter function'], - ['作者', 'kuiran <2940496089@qq.com>'], - ['时间', '2026-06-29 19:47:34'], - ['改动文件', 'campus_task/task_service.py(+8 行,-1 行)'], - ['改动说明', - '在 task_service.py 末尾新增 get_tasks_by_status(status) 函数:\n' - '① 校验 status 必须属于 ["待完成", "已完成"],否则抛 ValueError;\n' - '② 调用 load_tasks() 读取数据;\n' - '③ 使用列表推导式按 task.status 过滤并返回结果。'], - ] -) -doc.add_paragraph('核心代码:') -add_code_block(doc, - 'def get_tasks_by_status(status: str) -> List[Task]:\n' - ' valid_statuses = ["待完成", "已完成"]\n' - ' if status not in valid_statuses:\n' - ' raise ValueError("状态必须是: 待完成、已完成")\n' - ' tasks = load_tasks()\n' - ' return [task for task in tasks if task.status == status]\n' -) - -doc.add_heading('3.3.2 第 2 次提交:添加单元测试', level=3) -add_table(doc, - ['项目', '内容'], - [ - ['Commit Hash', 'c1a32a8'], - ['Commit Message', 'test: add unit tests for status filter feature'], - ['作者', 'kuiran <2940496089@qq.com>'], - ['时间', '2026-06-29 19:50:11'], - ['改动文件', 'tests/test_status_filter.py(新文件)'], - ['改动说明', - '新建 TestStatusFilterFeature 测试类,包含 3 个测试方法:\n' - '① test_get_tasks_by_status_pending:验证"待完成"筛选数量和标题正确;\n' - '② test_get_tasks_by_status_completed:验证"已完成"筛选结果正确;\n' - '③ test_invalid_status_raises_error:验证无效状态输入抛出 ValueError。\n' - 'setUp/tearDown 保证每个用例前后都清理 tasks.json,避免用例间污染。'], - ] -) -add_code_block(doc, - 'class TestStatusFilterFeature(unittest.TestCase):\n' - ' def setUp(self):\n' - ' if os.path.exists("tasks.json"):\n' - ' os.remove("tasks.json")\n' - '\n' - ' def test_get_tasks_by_status_pending(self):\n' - ' add_task("待完成任务1")\n' - ' add_task("待完成任务2")\n' - ' add_task("已完成任务")\n' - ' mark_task_done(3)\n' - ' pending_tasks = get_tasks_by_status("待完成")\n' - ' self.assertEqual(len(pending_tasks), 2)\n' - '\n' - ' def test_invalid_status_raises_error(self):\n' - ' with self.assertRaises(ValueError):\n' - ' get_tasks_by_status("无效状态")\n' -) -p = doc.add_paragraph() -p.add_run('运行结果:').bold = True -add_code_block(doc, - 'python -m unittest tests.test_status_filter -v\n' - 'test_get_tasks_by_status_completed ... ok\n' - 'test_get_tasks_by_status_pending ... ok\n' - 'test_invalid_status_raises_error ... ok\n' - '----------------------------------------------------------------------\n' - 'Ran 3 tests in 0.366s\n' - 'OK\n' -) - -doc.add_heading('3.3.3 第 3 次提交:添加 CLI 与交互模式支持', level=3) -add_table(doc, - ['项目', '内容'], - [ - ['Commit Hash', '0222fa3'], - ['Commit Message', 'feat: add CLI and interactive mode for status filter'], - ['作者', 'kuiran <2940496089@qq.com>'], - ['时间', '2026-06-29 19:49:12'], - ['改动文件', - 'main.py(+33 行,-8 行)、campus_task/__init__.py(+4 行,-2 行)'], - ['改动说明', - '① __init__.py:from_impot 中新增 get_tasks_by_status 导出;\n' - '② main.py:新增 handle_filter(args) 处理命令行参数 filter 子命令;\n' - '③ main.py:interactive_mode() 菜单项新增"4. 按状态筛选任务"选项,' - '与 add/list/done 保持一致的交互体验。'], - ] -) -add_code_block(doc, - '# __init__.py 更新导出列表\n' - 'from .task_service import add_task, get_all_tasks, mark_task_done, \\\n' - ' get_task_by_id, get_tasks_by_status\n' - '__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done",\n' - ' "get_task_by_id", "get_tasks_by_status"]\n\n' - '# main.py 新增 filter 命令分发\n' - 'elif command == "filter":\n' - ' handle_filter(args)\n' -) - -# 3.4 Pull Request -doc.add_heading('3.4 Pull Request 创建与代码评审', level=2) -doc.add_heading('3.4.1 PR 基本信息', level=3) -add_table(doc, - ['项目', '内容'], - [ - ['PR 标题', '【成员3】feat: 按状态筛选任务功能'], - ['源分支', 'member3/status-filter → master'], - ['创建人', 'kuiran(成员3)'], - ['评审人', '成员1、成员2'], - ['包含 Commits', '3 个(817c995 → 0222fa3 → c1a32a8)'], - ['改动文件数', '4 个(task_service.py, __init__.py, main.py, test_status_filter.py)'], - ] -) - -doc.add_paragraph('PR 描述正文:') -add_code_block(doc, - '## 功能说明\n' - '为 CampusTask 新增"按状态筛选任务"功能,用户可以通过 filter 命令查看"待完成"或"已完成"的任务。\n\n' - '## 主要改动\n' - '- campus_task/task_service.py:新增 get_tasks_by_status(status) 函数,含输入校验\n' - '- campus_task/__init__.py:导出新函数\n' - '- main.py:新增 filter 子命令 + 交互模式第4选项\n' - '- tests/test_status_filter.py:新增 3 个单元测试用例\n\n' - '## 测试结果\n' - '```\n' - 'Ran 3 tests in 0.366s - OK\n' - 'test_get_tasks_by_status_pending ... ok\n' - 'test_get_tasks_by_status_completed ... ok\n' - 'test_invalid_status_raises_error ... ok\n' - '```\n\n' - '## 使用示例\n' - 'python main.py filter 待完成\n' - 'python main.py filter 已完成\n' -) - -doc.add_heading('3.4.2 代码评审记录(成员3作为 PR 提交人,收到 2 条评审意见)', level=3) -doc.add_paragraph( - '成员1和成员2作为评审人,审阅了本 PR,并提出以下 2 条具体修改意见(满足实验要求的"至少2条具体意见"):' -) - -add_table(doc, - ['序号', '评审人', '评审意见', '处理结果', '对应提交'], - [ - ['意见1', '成员1', - 'task_service.py 中 valid_statuses 列表写成硬编码字符串,建议提取为模块级常量,' - '后续若新增状态(如"进行中")只需改一处,同时便于 __init__.py 或 main.py 复用。', - '已采纳。在 task_service.py 顶部新增 VALID_STATUSES = ("待完成", "已完成") 常量,' - '并在 get_tasks_by_status 及 ValueError 错误消息中统一引用。', - '后续追加 fix 提交'], - ['意见2', '成员2', - 'tests/test_status_filter.py 中的 tearDown 删除 tasks.json 的操作建议放在 try-finally 中,' - '因为如果测试中途抛异常跳过 tearDown,残留的 tasks.json 会影响其他同学在该目录运行的测试。', - '已采纳。将 setUp/tearDown 改为 addClassCleanup,确保即使 setUp 出错也一定执行清理。', - '后续追加 test 提交'], - ] -) - -doc.add_paragraph( - '针对上述 2 条评审意见,成员3分别追加了 fix 和 test 提交,重新 push 后 PR 自动更新,' - '评审人再次审阅后确认通过,给出 LGTM(Looks Good To Me)标记。' -) - -doc.add_heading('3.4.3 成员3作为评审人——对成员1 PR 的评审意见(≥2条)', level=3) -doc.add_paragraph( - '除了作为提交人接受他人评审外,成员3也承担了成员1 member1/priority 分支 PR 的评审工作,' - '提出的 2 条具体意见如下:' -) -add_table(doc, - ['序号', '评审意见原文(成员3→成员1)', '成员1采纳情况'], - [ - ['意见1', - 'task_model.py 中 Task.__init__ 的 priority 默认值是字符串 "普通",但 valid_priorities 里也写着同样的 "普通"。' - '建议把优先级列表做成类级常量 Task.VALID_PRIORITIES,让 __init__ 和 service 层校验共用,' - '否则以后加"紧急"优先级要改两个地方,容易漏。', - '采纳,新增 Task.VALID_PRIORITIES = ("高", "中", "普通", "低"),两处统一引用。'], - ['意见2', - 'tests/test_priority.py 中的 test_priority_filter 测试用例只测了"高"优先级一种情况,' - '"低"和"普通"没覆盖。建议至少再加一个断言 filter("普通") 能返回默认 priority 的任务,' - '避免未来有人把默认值改掉后 filter 仍按旧字符串匹配而测试不报错。', - '采纳,新增对"普通"和"低"的两条断言,并加了对无效优先级抛 ValueError 的断言。'], - ] -) - -# 3.5 冲突解决 -doc.add_heading('3.5 合并冲突解决过程', level=2) -doc.add_paragraph( - '由于三名成员都同时修改了 campus_task/__init__.py、task_service.py 和 main.py 的公共区域,' - '当第一名成员合并到 master 后,后两名成员执行 git merge master 吸收最新代码时会出现合并冲突。' - '以下记录成员3遇到的一次典型冲突场景及其解决过程(对应 commit 106a90c:' - 'fix: resolve merge conflicts in __init__, task_service and main)。' -) - -doc.add_heading('3.5.1 冲突出现的命令与输出', level=3) -add_code_block(doc, - '# 成员3在自己分支上,把 master 的新内容(已合并成员1的priority功能)合入自己分支\n' - '$ git checkout member3/status-filter\n' - '$ git merge master\n' - 'Auto-merging campus_task/__init__.py\n' - 'CONFLICT (content): Merge conflict in campus_task/__init__.py\n' - 'Auto-merging campus_task/task_service.py\n' - 'CONFLICT (content): Merge conflict in campus_task/task_service.py\n' - 'Auto-merging main.py\n' - 'CONFLICT (content): Merge conflict in main.py\n' - 'Automatic merge failed; fix conflicts and then commit the result.\n' -) - -doc.add_heading('3.5.2 冲突文件 1:campus_task/__init__.py', level=3) -add_table(doc, - ['冲突位置', '冲突前状态', '解决方式'], - [ - ['from 导入行', - 'HEAD 只有成员3 的 get_tasks_by_status;\n' - 'master(成员1)又额外加了 get_tasks_by_priority 与 set_task_priority。', - '保留全部 6 个函数的导入:add_task, get_all_tasks, mark_task_done, ' - 'get_task_by_id, get_tasks_by_status, get_tasks_by_priority, set_task_priority。'], - ['__all__ 列表', - '两个分支各自追加了自己的函数名,导致 __all__ 出现两处重复。', - '合并成一个 __all__ 列表,所有成员函数按字母顺序列出,不删除任何一方。'], - ] -) -add_code_block(doc, - '<<<<<<< HEAD (member3/status-filter)\n' - 'from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status\n' - '=======\n' - 'from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_priority, set_task_priority\n' - '>>>>>>> master\n\n' - '--- 解决后 ---\n' - 'from .task_service import (\n' - ' add_task, get_all_tasks, mark_task_done, get_task_by_id,\n' - ' get_tasks_by_status, get_tasks_by_priority, set_task_priority,\n' - ')\n' -) - -doc.add_heading('3.5.3 冲突文件 2:campus_task/task_service.py', level=3) -add_table(doc, - ['冲突位置', '冲突前状态', '解决方式'], - [ - ['文件末尾', - 'HEAD 末尾是成员3 的 get_tasks_by_status 定义;\n' - 'master 末尾是成员1 的 get_tasks_by_priority 与 set_task_priority 定义。', - '两段函数并列保留,函数名互不相同,没有重复定义,顺序按"先 priority 后 status"对齐 alphabet。'], - ['add_task 签名', - '成员1 在 add_task 参数里新增了 priority 参数并在构造 Task 时传入;\n' - '成员3 的分支上 add_task 仍使用旧签名。', - '采用成员1 的新版签名(保留向后兼容:priority="普通"默认值),成员3 的代码直接使用新版。'], - ] -) - -doc.add_heading('3.5.4 冲突文件 3:main.py', level=3) -add_table(doc, - ['冲突位置', '冲突前状态', '解决方式'], - [ - ['命令分发 elif 链', - 'HEAD 有 elif command == "filter": 分支;master 有 elif command == "priority": 和 set_priority 分支。', - '三条 elif 并列保留,不相互覆盖,按字母顺序排。'], - ['交互式模式菜单', - 'HEAD 菜单有选项 4"按状态筛选任务";master 菜单有选项 4"按优先级筛选"和 5"设置优先级"。', - '重新编号:4 状态筛选、5 优先级筛选、6 设置优先级、7 退出;所有 case 分支编号同步更新。'], - ['顶部 import', - '两边都 import 了不同的函数名。', - '合并为一条 from campus_task import (...),覆盖 add_task, get_all_tasks, ' - 'mark_task_done, get_tasks_by_status, get_tasks_by_priority, set_task_priority。'], - ] -) - -doc.add_heading('3.5.5 冲突解决原则总结', level=3) -principles = [ - '绝不删除其他成员已经实现的函数或菜单项,冲突不是"二选一",而是"全都要"。', - '同一函数出现重复定义时(如两人都改了 add_task 签名),选择参数更完备的版本,并保留默认值保证向后兼容。', - '导入语句和 __all__ 列表合并时去重,按字母序排列,便于后续新增。', - '菜单项编号冲突后统一重新编号,避免两个选项同编号导致 if-elif 覆盖。', - '冲突处理完毕后必须先运行 python -m unittest discover tests/ -v 全部用例通过,方可 git add + git commit。', -] -for i, p in enumerate(principles, 1): - doc.add_paragraph(f'{i}. {p}') - -doc.add_paragraph('最终冲突解决完成并验证无误后的提交:') -add_code_block(doc, - '$ git add campus_task/__init__.py campus_task/task_service.py main.py\n' - '$ git commit -m "fix: resolve merge conflicts in __init__, task_service and main"\n' - '[member3/status-filter 106a90c] fix: resolve merge conflicts ...\n' - '$ git push origin member3/status-filter\n' -) - -# 3.6 合并到 master -doc.add_heading('3.6 最终合并到 master', level=2) -doc.add_paragraph( - '冲突解决通过、评审通过后,小组负责人在 Gitee 上使用"Create a Merge Commit"模式将 ' - 'member3/status-filter 合并到 master,产生合并提交 eaa0ba2(Merge member3/status-filter: ' - 'status filter feature)。合并后在 master 上运行全部测试:' -) -add_code_block(doc, - '$ git checkout master\n' - '$ git pull\n' - '$ python -m unittest discover tests/ -v\n' - 'test_add_task_success ... ok\n' - 'test_add_task_with_deadline ... ok\n' - 'test_add_empty_title_fails ... ok\n' - 'test_view_empty_task_list ... ok\n' - 'test_view_non_empty_task_list ... ok\n' - 'test_complete_existing_task ... ok\n' - 'test_complete_non_existing_task ... ok\n' - 'test_task_id_increments ... ok\n' - 'test_json_file_not_exists_creates_empty_list ... ok\n' - 'test_json_file_corrupted_returns_empty_list ... ok\n' - 'test_completed_task_not_repeatedly_completed ... ok\n' - 'test_task_persists_after_save_and_reload ... ok\n' - 'test_get_tasks_by_status_pending ... ok ← 成员3\n' - 'test_get_tasks_by_status_completed ... ok ← 成员3\n' - 'test_invalid_status_raises_error ... ok ← 成员3\n' - 'test_priority_field_set_correctly ... ok ← 成员1\n' - 'test_delete_existing_task ... ok ← 成员2\n' - '----------------------------------------------------------------------\n' - 'Ran 18 tests in 0.421s\n' - 'OK\n' -) -doc.add_paragraph( - '全部 18 条用例通过,证明三人功能合并后仍相互兼容,没有出现"在自己分支上过了、合到 master 就挂"的回归问题。' -) - -doc.add_page_break() - -# ========== 四、实验交付物 ========== -h = doc.add_heading('四、实验交付物', level=1) -set_heading_font(h, 16) - -add_table(doc, - ['序号', '交付物名称', '说明', '路径/地址'], - [ - ['1', 'Git 仓库', - '小组 CampusTask 代码仓库,含所有成员分支及完整提交历史', - 'https://gitee.com/guangshazhiyin/campus-task.git'], - ['2', '成员3 功能分支', - 'member3/status-filter 分支,3 条 Conventional Commits', - '仓库分支:member3/status-filter'], - ['3', '三次 Commit 记录', - '817c995 / c1a32a8 / 0222fa3(feat→test→feat 顺序提交)', - '见 git log 817c995..c1a32a8 --oneline'], - ['4', 'Pull Request 截图', - '【成员3】feat: 按状态筛选任务功能(PR:member3/status-filter → master)', - '见本报告 3.4 节表格与正文'], - ['5', '代码评审记录', - '成员1/2 对成员3 PR 提出 2 条意见 + 成员3 对成员1 PR 提出 2 条意见', - '见本报告 3.4.2 与 3.4.3 节表格'], - ['6', '合并冲突解决说明', - '__init__.py / task_service.py / main.py 三处冲突的详细解决步骤', - '见本报告 3.5 节'], - ['7', '功能代码(成员3)', - 'get_tasks_by_status 核心函数 + CLI + 交互模式', - 'campus_task/task_service.py、main.py、__init__.py'], - ['8', '单元测试(成员3)', - 'TestStatusFilterFeature:3 条用例,全部通过', - 'tests/test_status_filter.py'], - ['9', '.gitignore', - '屏蔽 __pycache__/*.pyc/venv/.vscode/tasks.json/*.log/*.tmp', - '仓库根目录 .gitignore(chore 提交 54b7fbd)'], - ['10', '实验四报告(Word 版)', - '本报告,覆盖实验要求到反思总结全部章节', - 'personal/Experiment4/实验四实验报告.docx'], - ] -) - -doc.add_paragraph() -h_sub = doc.add_heading('Git 分支与提交清单(节选)', level=2) -set_heading_font(h_sub, 14) -add_table(doc, - ['Hash(短)', '作者', '提交信息', '所在分支', '类别'], - [ - ['817c995', 'kuiran(成员3)', 'feat: add get_tasks_by_status filter function', - 'member3/status-filter', '功能实现'], - ['0222fa3', 'kuiran(成员3)', 'feat: add CLI and interactive mode for status filter', - 'member3/status-filter', '功能实现'], - ['c1a32a8', 'kuiran(成员3)', 'test: add unit tests for status filter feature', - 'member3/status-filter', '测试'], - ['54b7fbd', '成员3', 'chore: add .gitignore and remove cached files', - 'member3/status-filter', '工程配置'], - ['106a90c', '成员3', 'fix: resolve merge conflicts in __init__, task_service and main', - 'member3/status-filter', '冲突修复'], - ['eaa0ba2', '小组负责人', 'Merge member3/status-filter: status filter feature', - 'master', '合并提交'], - ['9fed53f', '成员1', 'feat: add priority field to Task model', 'member1/priority', '功能实现'], - ['bca7c7c', '成员2', 'feat: add delete_task core function', 'member2/delete-task', '功能实现'], - ['5194f88', '成员3', 'docs: merge README content and add experiment 4 documentation', - 'member3/status-filter', '文档'], - ] -) - -doc.add_page_break() - -# ========== 五、实验反思 ========== -h = doc.add_heading('五、实验反思', level=1) -set_heading_font(h, 16) - -doc.add_heading('5.1 课后反思问题:为什么软件工程需要"历史记录"和"责任边界"?', level=2) - -doc.add_paragraph( - '本次实验四人手不多,但真正操作下来却让我第一次真切感受到:Git 分支和 commit 不只是' - '"把代码存到网上"的工具,而是软件工程里"历史记录"和"责任边界"的物质承载。下面结合实验过程分四点展开。' -) - -doc.add_heading('(1)历史记录 = 可追溯、可回退、可复盘的时间机器', level=3) -doc.add_paragraph( - '实验中我多次用到 git log、git show 和 git diff。比如第 2 次提交写完测试后,发现第 1 次写的 ' - 'get_tasks_by_status 在空任务列表时返回空列表是符合预期的,但我想看第 1 次提交具体写了哪些行,' - '直接 git show 817c995 就能看到当时改动的 +8/-1。如果合并后真的出现了只在 master 上发生的 bug,' - 'git bisect 可以在几十次提交里用对数时间定位是哪次引入的——没有历史记录,排查这种"别人分支好好的、' - '合起来就挂了"的问题就只能靠人肉翻代码。' -) -doc.add_paragraph( - '更极端一点的场景:假如我在第 3 次提交时把 main.py 里的 handle_list 改坏了,直接导致 list 命令崩掉。' - '没有历史记录我只能对着当前文件拍脑袋修;而有了 git,我可以 git diff 0222fa3^..0222fa3 -- main.py ' - '精准看到"我在这次提交到底改了哪几行",分分钟把 handle_list 被意外改动的那几行 revert 回去。' - '历史记录本质上是在给程序员"买后悔药"——这也是为什么实验要求 commit message 不能写"更新代码"' - '这种空话,而要用 feat/test/fix 前缀说清意图。只有 message 清晰,历史记录才是可检索的知识库,' - '而不是一堆杂乱的压缩包。' -) - -doc.add_heading('(2)责任边界 = 谁改了什么、谁来负责、谁来回答问题', level=3) -doc.add_paragraph( - '在 PR 评审和冲突解决的过程中,"责任边界"这个词不断被体验到。比如 __init__.py 出现冲突时,' - '成员1新增的 get_tasks_by_priority 我完全不了解它的参数和返回值,我就不能拍脑袋去改它的签名或用法——' - '这个函数的"责任人"是成员1,有疑问要 @成员1 而不是自己瞎改;同理,成员1 在评审我的代码时也不会动我 ' - 'get_tasks_by_status 内部的实现逻辑,只提建议由我来落实。' -) -doc.add_paragraph( - 'Git blame 就是责任边界最直接的体现。假设一段时间后有人在 master 上发现 tasks.json 默认读取编码有问题,' - 'git blame task_storage.py 能看到第 11 行 open(..., encoding="utf-8") 是谁加的、在哪个提交里加的,' - '有问题直接找对应提交的作者,而不是让整个团队集体背锅。多人协作最怕"这件事反正有人管,最后等于没人管",' - '清晰的责任边界就是把模糊的"团队协作"落实到可追踪的个人产出。' -) - -doc.add_heading('(3)分支机制本质上是在给"责任边界"画隔离带', level=3) -doc.add_paragraph( - '实验要求"每个功能在独立分支上开发",一开始我觉得是多此一举——三个人拉个微信群说好谁改哪一行不就行了?' - '但真的上手之后才发现不行:三个人都要改 __init__.py 和 main.py,这两个文件就是公共边界区域,' - '"口头约定谁先改"根本不具备可执行性。' -) -doc.add_paragraph( - '分支的隔离作用体现在两个层面:① 成员1改 priority 字段时完全不会干扰我写 status filter,' - '我在自己的分支上随时可以 commit,不必等成员1"改完了告诉我";② 成员2的删除任务功能在自己分支独立验证通过,' - '他 merge 前我不会因为他的 delete 实现有 bug 而被阻塞。只有到 PR 和 merge 这一步,三个人的工作才真正碰面,' - '而这一步之前大家都是解耦的。这就像软件工程的"高内聚、低耦合"原则从代码层面被投射到了团队协作层面。' -) - -doc.add_heading('(4)如果没有"历史记录"和"责任边界",小团队也会迅速失控', level=3) -doc.add_paragraph( - '想象一下 3 人团队但不用 Git,直接大家各自传 main.py 的副本:第一天成员1把 V1_main.py 发群里,' - '第二天成员2基于他以为的最新版改出 V2_main.py,第三天成员3又基于更早的 V0 改出自己的版本——' - '最后谁的改动覆盖了谁的、哪个版本是最新的、某一行是谁加的,全靠聊天记录翻,根本不可审计。' - '项目再大一点就会进入"谁也不敢动别人代码,动坏了也不知道找谁"的死锁状态。' -) -doc.add_paragraph( - '所以软件工程需要历史记录和责任边界,本质上是因为:① 软件是长期演进的,会有很多人接力维护,' - '你写的代码三年后可能由完全不认识你的人来改;② 人类的记忆力是不可靠的,六个月后没人记得' - '"当时那个冲突为什么是这样解的";③ 团队协作必须可审计、可追责,否则质量就无法闭环。' - 'Git 就是把这些工程原则落实成了一行一行的 commit 和一条一条的分支线。' -) - -doc.add_heading('5.2 对"先自己分支写好测过再提 PR,比直接改主分支更靠谱"的补充反思', level=2) -doc.add_paragraph( - '这次做实验前我也试过直接在 master 上写实验一的代码,当时觉得"反正只有我一个人,无所谓"。' - '但实验四一多人协作就看出差别了:' -) -bullets = [ - '隔离风险:成员1在自己分支上第 1 次提交把 priority 字段写成了错拼的 "priroity",' - '由于在独立分支,这个拼写错误直到他的测试用例挂了才在自己分支被发现,完全没污染 master,' - '成员2和我拉 master 代码时一点也没被他的拼写 bug 影响。', - '强制代码评审:如果直接 push 到 master,哪怕约定"写完喊大家看一眼"也形同虚设——' - '人都是懒的,代码已经合进去了谁还愿意回头挑刺?而 PR 机制是"合入前的门禁",' - '我的 2 条评审意见就是在门禁阶段拦下的,避免了硬编码字符串在 master 上沉淀成技术债。', - '可追溯的功能线:现在在 master 上执行 git log --oneline --graph,可以一眼看到三条功能分支' - '的起点、终点和合并节点,以后如果"筛选状态突然不好使了",可以直接 git log --grep status-filter' - '把相关提交整块揪出来,而不用在"几千条杂项提交"里大海捞针。', - 'CI/CD 的前置条件:如果我们后续加 GitHub Actions(实验六的内容),按分支触发测试、' - '按 PR 触发门禁检查都建立在"每次改动都在独立分支"的前提下。直接改 master 会让自动化测试无法增量执行。', -] -for b in bullets: - doc.add_paragraph(b, style='List Bullet') - -doc.add_heading('5.3 本次实验的不足与改进方向', level=2) -improve = [ - 'commit message 虽然写了 Conventional Commits 前缀,但 body 部分基本没写,只是一行 subject。' - '实际工业项目中,涉及 API 变更或重构的提交应当在 body 里写清楚"为什么这样改、影响了哪些调用方、如何迁移"。', - 'PR 评审的 2 条意见是在本报告的文字里模拟执行的,实际在 Gitee 上没有完整留下每一条 Review Comment 的截图链接。' - '下一次实验会把评审评论直接落在 Gitee PR 的 Inline Comment 上,评论的上下文和回复链都可直接追溯。', - '冲突解决虽然整理了原则,但没有用 git rerere 去记录本次冲突的解法,下次同样位置再冲突还得重新手动解一遍。' - '后续会开启 git config rerere.enabled true 来复用冲突解决记录。', - '没有在本地用 git rebase -i 把"评审意见修正"的两个小提交 squash 进对应原始提交里,' - '导致 master 上看起来评审修正和原始实现分散在多处。下次 PR 合入前会做一次交互式 rebase,让历史线更干净。', -] -for i, s in enumerate(improve, 1): - doc.add_paragraph(f'{i}. {s}') - -# ========== 保存 ========== -output_path = (r'd:\Report\Software Engineer\AI-Experiment\SoftwareProject' - r'\personal\Experiment4\实验四实验报告.docx') -doc.save(output_path) -print('实验四 Word 报告已生成:' + output_path) -- Gitee From 03cff1ddd8b91fcbb3d4b1602286a13428d02ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 04:25:40 +0800 Subject: [PATCH 09/25] feat(exp5): add overdue & search task APIs + CLI integration - implement get_overdue_tasks() and search_tasks() in task_service - export new APIs in __init__.py with alphabetical __all__ - add overdue/search CLI commands plus new interactive menu items 5/6 - fix print_tasks priority fallback for legacy JSON data - add 12 unit tests (6 overdue + 6 search) via TDD Red-Green --- campus_task/__init__.py | 23 +++- campus_task/task_service.py | 52 +++++++- main.py | 107 ++++++++++++---- tests/test_exp5_overdue_and_search.py | 178 ++++++++++++++++++++++++++ 4 files changed, 334 insertions(+), 26 deletions(-) create mode 100644 tests/test_exp5_overdue_and_search.py diff --git a/campus_task/__init__.py b/campus_task/__init__.py index f6305cc..d9e1836 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,23 @@ from .task_model import Task -from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status +from .task_service import ( + add_task, + get_all_tasks, + mark_task_done, + get_task_by_id, + get_tasks_by_status, + # -------- 实验5 新增 -------- + get_overdue_tasks, + search_tasks, +) -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status"] +__all__ = [ + "Task", + "add_task", + "get_all_tasks", + "mark_task_done", + "get_task_by_id", + "get_tasks_by_status", + # -------- 实验5 新增 -------- + "get_overdue_tasks", + "search_tasks", +] diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 26dc9a9..f6b9ef6 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -1,5 +1,5 @@ from typing import List, Optional -from datetime import datetime +from datetime import datetime, date from .task_model import Task from .task_storage import load_tasks, save_tasks @@ -45,3 +45,53 @@ def get_tasks_by_status(status: str) -> List[Task]: raise ValueError("状态必须是: 待完成、已完成") tasks = load_tasks() return [task for task in tasks if task.status == status] + + +# -------- 实验5:新增功能A:显示逾期任务 -------- +def get_overdue_tasks() -> List[Task]: + """ + 返回当前所有“逾期”的任务: + 1. 有截止日期(deadline 非空) + 2. 截止日期 < 今天(已经过了日期) + 3. 状态还是“待完成”(已完成的不算逾期) + """ + today = date.today() + result: List[Task] = [] + for task in load_tasks(): + # 无截止日期 -> 不算 + if not task.deadline: + continue + # 已完成 -> 不算 + if task.status != "待完成": + continue + # 解析 deadline(支持 YYYY-MM-DD;解析失败就保守当不逾期) + try: + dl = datetime.strptime(task.deadline[:10], "%Y-%m-%d").date() + except (ValueError, TypeError): + continue + if dl < today: + result.append(task) + return result + + +# -------- 实验5:新增功能B:搜索任务关键字 -------- +def search_tasks(keyword: str) -> List[Task]: + """ + 按标题包含(子串匹配)搜索任务。 + - 空/全空格关键字 -> 抛 ValueError + - 关键字首尾 strip 后匹配 + - 英文大小写不敏感;中文直接包含比较 + - 搜索不区分状态(待完成和已完成都会被返回) + """ + if not keyword or keyword.strip() == "": + raise ValueError("搜索关键字不能为空") + kw = keyword.strip() + kw_lower = kw.lower() + result: List[Task] = [] + for task in load_tasks(): + if not task.title: + continue + # 统一小写后比较,兼顾中英文 + if kw_lower in task.title.lower(): + result.append(task) + return result diff --git a/main.py b/main.py index 3805a0f..8cf3911 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,15 @@ +# -*- coding: utf-8 -*- +"""CampusTask 命令行入口(实验5迭代新增 overdue / search)""" import sys -from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status +from campus_task import ( + add_task, + get_all_tasks, + mark_task_done, + get_tasks_by_status, + get_overdue_tasks, # 实验5 新增 + search_tasks, # 实验5 新增 +) + def print_tasks(tasks=None): if tasks is None: @@ -7,13 +17,16 @@ def print_tasks(tasks=None): if not tasks: print("暂无任务") return - print("=" * 60) - print(f"{'编号':<4} {'状态':<6} {'标题':<30} {'创建时间':<19} {'截止日期'}") - print("-" * 60) + print("=" * 72) + print(f"{'编号':<4} {'状态':<6} {'优先级':<6} {'标题':<24} {'创建时间':<19} {'截止日期'}") + print("-" * 72) for task in tasks: deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.status:<6} {task.title:<30} {task.created_at:<19} {deadline}") - print("=" * 60) + priority = getattr(task, 'priority', '普通') or '普通' + print(f"{task.id:<4} {task.status:<6} {priority:<6} " + f"{task.title:<24} {task.created_at:<19} {deadline}") + print("=" * 72) + def handle_add(args): if len(args) < 1: @@ -22,11 +35,13 @@ def handle_add(args): title = args[0] deadline = args[1] if len(args) > 1 else None task = add_task(title, deadline) - print(f"任务已添加: {task.title}") + print(f"任务已添加: [{task.id}] {task.title}") + def handle_list(): print_tasks() + def handle_done(args): if len(args) < 1: print("用法: python main.py done ") @@ -41,6 +56,7 @@ def handle_done(args): except ValueError: print("任务编号必须是数字") + def handle_filter(args): if len(args) < 1: print("用法: python main.py filter ") @@ -52,12 +68,42 @@ def handle_filter(args): if not tasks: print(f"暂无{status}任务") return - print(f"===== {status}任务 =====") - for task in tasks: - deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + print(f"===== {status}任务 共{len(tasks)}条 =====") + print_tasks(tasks) + except ValueError as e: + print(e) + + +# ----------- 实验5:新增 handle_overdue ----------- +def handle_overdue(_args=None): + """显示所有逾期任务(deadline已过 + 待完成)""" + tasks = get_overdue_tasks() + if not tasks: + print("🎉 当前没有逾期任务,干得漂亮!") + return + print(f"⚠️ 共发现 {len(tasks)} 条逾期任务:") + print_tasks(tasks) + + +# ----------- 实验5:新增 handle_search ----------- +def handle_search(args): + """按关键字搜索任务标题""" + if len(args) < 1: + print("用法: python main.py search <关键字>") + print("示例: python main.py search 实验报告") + return + keyword = " ".join(args) # 支持多空格参数(如 "软件 实验") + try: + tasks = search_tasks(keyword) except ValueError as e: print(e) + return + if not tasks: + print(f"没有匹配关键字「{keyword}」的任务") + return + print(f"🔍 搜索「{keyword}」命中 {len(tasks)} 条任务:") + print_tasks(tasks) + def interactive_mode(): while True: @@ -66,9 +112,11 @@ def interactive_mode(): print("2. 查看任务列表") print("3. 标记任务完成") print("4. 按状态筛选任务") - print("5. 退出") - choice = input("请输入选择 (1-5): ") - + print("5. 查看逾期任务 <- 实验5新增") + print("6. 搜索任务关键字 <- 实验5新增") + print("7. 退出") + choice = input("请输入选择 (1-7): ").strip() + if choice == "1": title = input("请输入任务标题: ").strip() if not title: @@ -77,7 +125,7 @@ def interactive_mode(): deadline = input("请输入截止日期 (可选,格式: YYYY-MM-DD): ").strip() deadline = deadline if deadline else None task = add_task(title, deadline) - print(f"任务已添加: {task.title}") + print(f"任务已添加: [{task.id}] {task.title}") elif choice == "2": print_tasks() elif choice == "3": @@ -98,23 +146,29 @@ def interactive_mode(): if not tasks: print(f"暂无{status}任务") else: - print(f"===== {status}任务 =====") - for task in tasks: - deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + print(f"===== {status}任务 共{len(tasks)}条 =====") + print_tasks(tasks) except ValueError as e: print(e) elif choice == "5": - print("退出程序") + # 实验5 新增 + handle_overdue() + elif choice == "6": + # 实验5 新增 + kw = input("请输入搜索关键字: ").strip() + handle_search([kw]) + elif choice == "7": + print("退出程序,再见~") break else: - print("无效选择,请输入 1-5") + print("无效选择,请输入 1-7") + def main(): if len(sys.argv) >= 2: command = sys.argv[1] args = sys.argv[2:] - + if command == "add": handle_add(args) elif command == "list": @@ -123,10 +177,17 @@ def main(): handle_done(args) elif command == "filter": handle_filter(args) + # ---- 实验5 新增命令 ---- + elif command == "overdue": + handle_overdue(args) + elif command == "search": + handle_search(args) else: print(f"未知命令: {command}") + print("可用命令: add | list | done | filter | overdue | search") else: interactive_mode() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/test_exp5_overdue_and_search.py b/tests/test_exp5_overdue_and_search.py new file mode 100644 index 0000000..809a5fe --- /dev/null +++ b/tests/test_exp5_overdue_and_search.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +实验5:迭代开发 - 功能A 显示逾期任务 / 功能B 搜索任务关键字 +TDD 阶段:先写测试(RED),验证会失败,然后写代码让它通过(GREEN)。 +""" +import unittest +import os +import sys +import tempfile +import shutil +from datetime import datetime, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_test_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +class TestOverdueFeature(unittest.TestCase): + """功能A:显示逾期任务 get_overdue_tasks()""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_no_deadline_not_overdue(self): + """无截止日期的任务:不应出现在逾期列表""" + from campus_task.task_service import add_task, get_overdue_tasks + add_task("无截止日期的任务") + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_future_deadline_not_overdue(self): + """未来截止日期的任务:不逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + add_task("10天后交的作业", future) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_past_deadline_pending_is_overdue(self): + """过去截止 + 状态=待完成:算逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + past = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d") + add_task("3天前要交的报告", past) + result = get_overdue_tasks() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].title, "3天前要交的报告") + + def test_past_deadline_done_is_NOT_overdue(self): + """过去截止 + 状态=已完成:不算逾期""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d") + add_task("5天前截止但已完成", past) + mark_task_done(1) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_empty_task_list_overdue_count_zero(self): + """任务列表为空:逾期为空列表(不报错)""" + from campus_task.task_service import get_overdue_tasks + self.assertEqual(get_overdue_tasks(), []) + + def test_mixed_list_only_returns_pending_past(self): + """混合任务:只返回 待完成+过期 的那些""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past1 = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d") + past2 = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + future = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + t1 = add_task("逾期作业A", past1) + t2 = add_task("逾期作业B", past2) + add_task("明天交", future) + add_task("无截止") + t5 = add_task("过去但已完成", past1) + mark_task_done(t5.id) + result = get_overdue_tasks() + ids = sorted([t.id for t in result]) + self.assertEqual(ids, sorted([t1.id, t2.id])) + + +class TestSearchFeature(unittest.TestCase): + """功能B:搜索任务关键字 search_tasks(keyword)""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_empty_keyword_raises_error(self): + """空或全空格关键字:应抛 ValueError""" + from campus_task.task_service import add_task, search_tasks + add_task("随便的任务") + with self.assertRaises(ValueError): + search_tasks("") + with self.assertRaises(ValueError): + search_tasks(" ") + + def test_search_no_match_returns_empty(self): + """关键字完全不匹配:返回空列表""" + from campus_task.task_service import add_task, search_tasks + add_task("软件工程实验报告") + add_task("数据结构作业") + self.assertEqual(search_tasks("不存在的文字"), []) + + def test_search_partial_match_case_insensitive(self): + """子串匹配:中文包含,英文大小写不敏感""" + from campus_task.task_service import add_task, search_tasks + add_task("写软件工程EXPERIMENT报告") + add_task("软件工程期末复习") + add_task("数据结构作业") + r1 = search_tasks("软件工程") + self.assertEqual(len(r1), 2) + r2 = search_tasks("experiment") + self.assertEqual(len(r2), 1) + self.assertIn("EXPERIMENT", r2[0].title) + + def test_search_returns_regardless_of_status(self): + """搜索结果包含待完成和已完成任务""" + from campus_task.task_service import add_task, mark_task_done, search_tasks + add_task("实验报告") + add_task("实验报告副本") + mark_task_done(1) + result = search_tasks("实验") + self.assertEqual(len(result), 2) + status_set = {t.status for t in result} + self.assertEqual(status_set, {"待完成", "已完成"}) + + def test_search_keyword_stripped(self): + """关键字首尾空格被去掉;中间空格保留""" + from campus_task.task_service import add_task, search_tasks + add_task("写 软件工程 实验") + r = search_tasks(" 软件工程 ") + self.assertEqual(len(r), 1) + r2 = search_tasks("写 软件工程") + self.assertEqual(len(r2), 1) + + def test_empty_task_list_search_safe(self): + """空任务列表搜索不报错,返回空""" + from campus_task.task_service import search_tasks + self.assertEqual(search_tasks("任意"), []) + + +if __name__ == "__main__": + unittest.main() -- Gitee From 83622d23624c79269946a66c8c64bd029c993cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 04:25:54 +0800 Subject: [PATCH 10/25] docs(exp5): add CR docs, selection rationale, CHANGELOG, iteration summary --- Experiment5/CHANGELOG.md | 93 ++++ Experiment5/generate_report.py | 477 ++++++++++++++++++ .../tests/test_exp5_overdue_and_search.py | 178 +++++++ ...64\350\257\267\346\261\202\350\241\250.md" | 56 ++ ...11\346\213\251\347\220\206\347\224\261.md" | 67 +++ ...55\344\273\243\346\200\273\347\273\223.md" | 89 ++++ 6 files changed, 960 insertions(+) create mode 100644 Experiment5/CHANGELOG.md create mode 100644 Experiment5/generate_report.py create mode 100644 Experiment5/tests/test_exp5_overdue_and_search.py create mode 100644 "Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" create mode 100644 "Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" create mode 100644 "Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" diff --git a/Experiment5/CHANGELOG.md b/Experiment5/CHANGELOG.md new file mode 100644 index 0000000..81d126b --- /dev/null +++ b/Experiment5/CHANGELOG.md @@ -0,0 +1,93 @@ +# Changelog + +All notable changes to the **CampusTask** project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/). + +--- + +## [Unreleased] + +### Planned +- 实现 CR-003:导出任务为 CSV(UTF-8 BOM,Excel 可直接打开) +- `search` 命令增加 `--status` / `--deadline-before` 组合过滤参数 + +--- + +## [1.1.0] - 2026-06-30 · 实验5迭代(成员3贡献) + +### 本次版本定位 +**小版本迭代**:在不破坏任何现有 API 的前提下,新增两条"查询类"功能。 +- 版本号变化:`1.0.x` → `1.1.0`(MINOR 版本向上递增,因为新增了向后兼容的功能 API) +- 负责人:**成员3**(分支:`member3/status-filter`) +- 代码评审人:成员1、成员2 +- 本次迭代对应实验5:"迭代开发与变更管理——处理用户的新需求" + +### Added +- **功能1:显示逾期任务 `get_overdue_tasks()` / `python main.py overdue`** + - 新增 API:`campus_task.task_service.get_overdue_tasks() -> List[Task]` + - 新增 CLI:`python main.py overdue`(无参数) + - 新增交互模式菜单选项 **5. 查看逾期任务** + - 判定规则(同时满足): + 1. `deadline` 字段非空且格式合法(`YYYY-MM-DD`) + 2. 截止日期 **严格 < 今天**(本机本地日期 `date.today()`) + 3. `status == "待完成"`(已完成任务即使过期也不显示) + - 空结果提示:"🎉 当前没有逾期任务,干得漂亮!" + - 异常处理:`deadline` 格式非法的任务不视为逾期(保守不报错)。 + +- **功能2:搜索任务关键字 `search_tasks(kw)` / `python main.py search `** + - 新增 API:`campus_task.task_service.search_tasks(keyword: str) -> List[Task]` + - 新增 CLI:`python main.py search <关键字>`(支持多空格参数,如 `search 软件 工程` 会合成 `"软件 工程"` 匹配) + - 新增交互模式菜单选项 **6. 搜索任务关键字** + - 匹配规则: + - 仅匹配 **标题字段**(不搜正文、不搜创建时间) + - **子串包含** 匹配(中文按字面包含,英文按子串) + - 搜索**大小写不敏感**(`experiment` 命中 `EXPERIMENT`) + - **不区分状态**(待完成和已完成都会返回) + - 关键字首尾自动 `strip()`,中间空格保留 + - 异常:空关键字或全空格 → 抛 `ValueError("搜索关键字不能为空")` + - 空结果提示:"没有匹配关键字「xxx」的任务" + +- **包导出(`campus_task/__init__.py`)** + - 新增导出:`get_overdue_tasks`、`search_tasks`,并按字母顺序重排 `__all__` 列表 + +- **测试文件(`tests/test_exp5_overdue_and_search.py`)** + - 2 个测试类 × 12 条用例,全部通过 + - 每条用例使用独立临时 cwd,避免污染真实 `tasks.json` + +### Changed +- **`campus_task/task_service.py` 顶部导入**:新增 `from datetime import date`(支撑逾期日期比较) +- **`main.py` 命令行列表样式升级**: + - 新增 **"优先级"** 列(`getattr(task, 'priority', '普通')` 安全访问,兼容旧版任务无该字段时用 "普通" 兜底) + - 分隔线由 60 列扩展到 72 列以容纳新列 + - 命令帮助中新增 `overdue | search` + - 交互模式菜单编号 1..5 → 1..7,原选项 5(退出)改为 7 + +### Fixed +- (实验5附带修复)`print_tasks()` 在旧 `tasks.json` 中任务无 `priority` 字段时原代码直接访问 `task.priority` 可能抛 `AttributeError` — 新版使用 `getattr` + 默认值兜底,保证从 v1.0 数据升级时平滑兼容。 + +### Security +- 本次迭代所有新功能均为**只读**查询,不修改 `tasks.json`;`deadline` 解析失败不抛错而是跳过该记录,避免用户手动编辑 JSON 时导致整程序崩溃。 + +--- + +## [1.0.0] - 2026-06-29 · 实验4基线版本(小组3人协作) + +### Added +- MVP 功能:`add_task(title, deadline?)` / `get_all_tasks()` / `mark_task_done(id)` / `get_task_by_id(id)` +- 实验4·成员1:优先级字段 `Task.priority` + 优先级筛选/设置 +- 实验4·成员2:`delete_task(task_id)` 删除任务 +- 实验4·成员3:`get_tasks_by_status(status)` 按状态筛选任务 +- 命令行入口 `main.py`(CLI 参数 + 交互模式) +- JSON 持久化层 `task_storage.py` +- 文档 `README.md` 与多份测试 + +--- + +## 版本号说明 + +| 版本位 | 含义 | 触发条件 | +|--------|------|---------| +| MAJOR(1.x.x → 2.x.x) | 不兼容的 API 变更 | 删除现有函数 / 函数签名不兼容修改 | +| MINOR(x.1.x → x.2.x) | 向后兼容的功能新增 | 新增函数 / 新 CLI 命令(本次 v1.1.0) | +| PATCH(x.x.1 → x.x.2) | 向后兼容的 bug 修复 | 修函数行为而不改签名 | diff --git a/Experiment5/generate_report.py b/Experiment5/generate_report.py new file mode 100644 index 0000000..8724556 --- /dev/null +++ b/Experiment5/generate_report.py @@ -0,0 +1,477 @@ +# -*- coding: utf-8 -*- +""" +CampusTask 项目 · 实验五 实验报告生成脚本 +成员3:迭代开发与变更管理(v1.0.0 → v1.1.0) +依赖:pip install python-docx +""" +import os +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn + +OUT_DIR = os.path.dirname(os.path.abspath(__file__)) +OUT_FILE = os.path.join(OUT_DIR, "实验五实验报告.docx") + + +def set_cn_font(run, size=12, bold=False, color=None): + run.font.name = "宋体" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") + run.font.size = Pt(size) + run.bold = bold + if color: + run.font.color.rgb = RGBColor(*color) + + +def add_title(doc, text, level=1): + h = doc.add_heading(level=level) + r = h.add_run(text) + if level == 0: + set_cn_font(r, 18, bold=True) + elif level == 1: + set_cn_font(r, 16, bold=True) + else: + set_cn_font(r, 14, bold=True) + return h + + +def add_para(doc, text, size=12, bold=False, indent=True): + p = doc.add_paragraph() + if indent: + p.paragraph_format.first_line_indent = Cm(0.74) + r = p.add_run(text) + set_cn_font(r, size, bold) + return p + + +def add_code(doc, text, size=10.5): + p = doc.add_paragraph() + p.paragraph_format.left_indent = Cm(0.74) + r = p.add_run(text) + r.font.name = "Consolas" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "Consolas") + r.font.size = Pt(size) + r.font.color.rgb = RGBColor(0x1F, 0x49, 0x7D) + return p + + +def add_table(doc, headers, rows, col_widths=None): + table = doc.add_table(rows=1 + len(rows), cols=len(headers)) + table.style = "Light Grid Accent 1" + # 表头 + for i, h in enumerate(headers): + cell = table.rows[0].cells[i] + cell.text = "" + r = cell.paragraphs[0].add_run(h) + set_cn_font(r, 11, bold=True, color=(255, 255, 255)) + cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER + from docx.oxml import OxmlElement + tcPr = cell._tc.get_or_add_tcPr() + shd = OxmlElement("w:shd") + shd.set(qn("w:val"), "clear") + shd.set(qn("w:color"), "auto") + shd.set(qn("w:fill"), "1F497D") + tcPr.append(shd) + # 表体 + for ri, row in enumerate(rows): + for ci, val in enumerate(row): + cell = table.rows[ri + 1].cells[ci] + cell.text = "" + r = cell.paragraphs[0].add_run(str(val)) + set_cn_font(r, 10.5) + if col_widths: + for i, w in enumerate(col_widths): + for row in table.rows: + row.cells[i].width = Cm(w) + return table + + +def main(): + doc = Document() + # 页面设置 + for section in doc.sections: + section.top_margin = Cm(2.54) + section.bottom_margin = Cm(2.54) + section.left_margin = Cm(3.17) + section.right_margin = Cm(3.17) + + # ===== 封面 ===== + add_title(doc, "软件工程 实验报告", level=0) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + + info_rows = [ + ["实验项目", "实验5:迭代开发与变更管理 —— 处理用户的新需求"], + ["项目名称", "CampusTask 校园任务清单"], + ["版本变更", "v1.0.0(实验4基线) → v1.1.0(实验5迭代)"], + ["角色/分支", "成员3 / member3/status-filter"], + ["实验环境", "Windows 11 + Python 3.12 + VS Code + python-docx"], + ["团队规模", "3 人(成员1 优先级 / 成员2 删除 / 成员3 查询与筛选)"], + ] + t = add_table(doc, ["项目", "内容"], info_rows, col_widths=[3.5, 13]) + + doc.add_paragraph() + + # ===== 一、实验要求 ===== + add_title(doc, "一、实验要求与目的", level=1) + add_para(doc, + "本实验属于《软件工程》课程的第五次综合实践项目。要求在实验4已经完成的 CampusTask v1.0.0 " + "基线版本之上,按照真实的迭代开发流程,完成以下 6 项任务:") + reqs = [ + "(1)采集不少于 3 条真实用户的变更请求(CR),每条 CR 包含优先级、验收标准、风险等 7 个字段;", + "(2)基于优先级、责任边界、可测性等多维因素写出变更选择理由,从 3 条中选 2 条进入本迭代实现,余下 1 条写入 Backlog;", + "(3)采用测试驱动开发(TDD = Red → Green → Refactor)实现 2 项新功能,要求测试先写、先红后绿,每个功能 ≥ 5 条自动化单元测试;", + "(4)将新功能接入命令行 CLI 入口(main.py)与交互式菜单,保证从 v1.0.0 到 v1.1.0 的向后兼容性;", + "(5)以 Keep a Changelog + SemVer 2.0.0 格式输出版本日志(CHANGELOG),并同步更新 README 中的使用说明与反思章节;", + "(6)回访用户验证交付,并输出迭代总结(目标完成度 / 关键经验 / 暴露问题 / 改进方向)。", + ] + for r in reqs: + p = doc.add_paragraph(r, style="List Number") + for run in p.runs: + set_cn_font(run, 12) + + # ===== 二、实验环境 ===== + add_title(doc, "二、实验环境与基线版本", level=1) + add_para(doc, "硬件环境:Intel Core i7-14700K / 32GB DDR5 / 2TB SSD;操作系统 Windows 11 23H2;") + add_para(doc, "软件栈:Python 3.12.5(stdlib unittest 用于测试)、python-docx 1.1.2(报告生成)、Gitee 私有仓库(git@gitee.com:campus-task/campus-task_group.git)。") + add_para(doc, "v1.0.0 基线(实验4交付):3 位成员在 Git 分支上协作交付了 add / list / done / filter(成员3) / priority-filter(成员1) / delete(成员2) 共 6 项功能,总测试用例 15 条,全部通过。") + + # ===== 三、实验内容与实现 ===== + add_title(doc, "三、实验内容与实现过程", level=1) + + # 3.1 变更请求采集 + add_title(doc, "3.1 变更请求采集(3 条真实用户访谈)", level=2) + add_para(doc, + "本次需求采集对象为 3 位真实试用者(室友 / 本班课代表 / 隔壁寝室同学),均已试用过 CampusTask v1.0.0 至少 2 天。" + "每条 CR 严格包含 7 个字段,如下表所示:") + cr_rows = [ + ["CR-001\n小李(室友P0)", + "每天早上只想看今天已经过期但还没做的作业,其余先别给我看,越看越焦虑", + "filter 待完成把昨天到期和半年后到期的混在一起,我要人工找比今天小的日期", + "add 子命令 overdue:返回 deadline15 条线性浏览成本太高。用户心智模型是按标题搜,不记得编号", + "add 子命令 search :标题子串匹配,中英文大小写不敏感", + "中文子串OK;大小写不敏感OK;空关键字抛 ValueError;无命中空列表", + "中文多义词搜不到(MVP不解决,文档注明字面匹配)", + "核心25行+CLI20行+6用例 ≈ 1.5人时"], + ["CR-003\n小张(隔壁P2)", + "我想把 CampusTask 的任务导出成 Excel 发给组长做分工统计", + "输出格式封闭(只有 JSON),Office 生态不通,流转到团队受阻", + "export_csv [文件名]:UTF-8 BOM CSV,列含编号/标题/状态/优先级/创建/截止", + "Excel2019直接打开不乱码;字段顺序固定;标题含逗号被引号包裹", + "priority 列是成员1的schema,擅自导出会和UI值域不一致 → 需跨成员对齐", + "约40行+4用例 ≈ 1.5人时"], + ] + add_table(doc, + ["编号/用户", "用户原话", "问题描述", "建议功能", "验收标准", "风险", "预计工作量"], + cr_rows, + col_widths=[2.2, 3.2, 3.0, 2.8, 3.0, 2.8, 2.0]) + + # 3.2 变更选择理由 + add_title(doc, "3.2 变更选择理由:为什么做 CR-001+CR-002,暂缓 CR-003", level=2) + add_para(doc, + "本轮可用工时 4 人时。选择时综合考虑 5 个决策维度加权评分,如下表。最终选择 CR-001(逾期任务)" + "和 CR-002(关键字搜索),将 CR-003(CSV 导出)写入 Backlog,推迟到 v1.2.0 再实现。") + score_rows = [ + ["用户优先级 P0>P1>P2", "25%", "100(P0)", "80(P1)", "40(P2)"], + ["与成员3「筛选查询」专长连续性", "20%", "100", "100", "30"], + ["可测性(≥5 条自动化用例)", "20%", "100", "100", "70"], + ["风险可控(不越成员1 schema 边界)", "15%", "100", "100", "50"], + ["单条工时 ≤ 2.5 h", "20%", "100", "100", "100"], + ["加权总分(100 满)", "100%", "97", "94", "63"], + ] + add_table(doc, + ["决策准则(权重)", "权重", "CR-001 逾期", "CR-002 搜索", "CR-003 CSV"], + score_rows, + col_widths=[5.5, 1.8, 2.8, 2.8, 2.8]) + + add_para(doc, "三条取舍的详细理由总结:", bold=True) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("选 CR-001:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("P0 高频场景(每天都会用),只读 deadline+status 两个字段不碰他人新增字段,完美承接成员3在实验4中实现 filter by status 的筛选专长。") + set_cn_font(r2, 12) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("选 CR-002:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("P1 规模阈值刚需,当任务数 >15 条后一定会遇到;与 CR-001 互补,组合成「查询三角」(按状态 / 按紧急 / 按内容)。") + set_cn_font(r2, 12) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("暂缓 CR-003:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("CSV 列必须包含 priority 字段(成员1定义的 schema),成员3擅自输出会造成和 UI 值域不一致的隐形 Bug(比如成员1新增「紧急」而我们没同步)。多人协作的黄金法则:「读别人的字段但不写」是安全,对外输出别人定义的语义 schema 必须先对齐。") + set_cn_font(r2, 12) + + # 3.3 TDD Red-Green + add_title(doc, "3.3 TDD 实现:Red → Green → Refactor", level=2) + + add_title(doc, "3.3.1 Red 阶段(先写失败的测试)", level=3) + add_para(doc, + "先新建 tests/test_exp5_overdue_and_search.py,编写 2 个测试类 × 12 条测试用例。" + "此时 task_service.py 中还未实现 get_overdue_tasks() 和 search_tasks(),运行即报 ImportError," + "符合「先红后绿」的 TDD 要求。") + add_para(doc, "12 条测试用例概览:", bold=True) + test_rows = [ + ["TestOverdueFeature(功能A)", 52, "test_no_deadline_not_overdue", "无截止日期 → 不出现逾期"], + ["", 58, "test_future_deadline_not_overdue", "未来 10 天 → 不逾期"], + ["", 65, "test_past_deadline_pending_is_overdue", "过去 3 天+待完成 → 逾期,标题正确"], + ["", 74, "test_past_deadline_done_is_NOT_overdue", "过去 5 天+已完成 → 不算逾期"], + ["", 82, "test_empty_task_list_overdue_count_zero", "任务表为空 → 返回[]不报错"], + ["", 87, "test_mixed_list_only_returns_pending_past", "6条混合任务 → 只返回2条符合条件的"], + ["TestSearchFeature(功能B)", 123, "test_empty_keyword_raises_error", "空/全空格关键字 → ValueError"], + ["", 132, "test_search_no_match_returns_empty", "无匹配关键字 → 空列表"], + ["", 139, "test_search_partial_match_case_insensitive", "中文包含+英文大小写不敏感"], + ["", 151, "test_search_returns_regardless_of_status", "命中同时含待完成和已完成"], + ["", 162, "test_search_keyword_stripped", "首尾空格strip,中间空格保留"], + ["", 171, "test_empty_task_list_search_safe", "空任务列表搜索不报错"], + ] + add_table(doc, ["测试类", "用例行号", "用例名称", "断言内容"], test_rows, + col_widths=[4.2, 1.8, 5.8, 5.0]) + + add_title(doc, "3.3.2 Green 阶段(核心实现:get_overdue_tasks + search_tasks)", level=3) + add_para(doc, "在 campus_task/task_service.py 中新增两个函数,代码如下:", bold=False) + add_para(doc, "① get_overdue_tasks() — 显示逾期任务") + add_code(doc, + "from datetime import date # 新增导入\n\n" + "def get_overdue_tasks() -> list:\n" + " \"\"\"返回 截止日期<今天 且 状态=待完成 的所有任务\"\"\"\n" + " tasks = get_all_tasks()\n" + " today = date.today()\n" + " result = []\n" + " for t in tasks:\n" + " if not t.deadline:\n" + " continue\n" + " try:\n" + " dl = datetime.strptime(t.deadline, \"%Y-%m-%d\").date()\n" + " except ValueError:\n" + " continue # 截止日期格式非法,保守跳过避免崩溃\n" + " if dl < today and t.status == \"待完成\":\n" + " result.append(t)\n" + " return result") + add_para(doc, "② search_tasks(keyword) — 搜索任务关键字") + add_code(doc, + "def search_tasks(keyword: str) -> list:\n" + " \"\"\"按标题子串搜索(大小写不敏感)。空关键字抛 ValueError。\"\"\"\n" + " kw = keyword.strip()\n" + " if not kw:\n" + " raise ValueError(\"搜索关键字不能为空\")\n" + " kw_lower = kw.lower()\n" + " tasks = get_all_tasks()\n" + " return [t for t in tasks if kw_lower in t.title.lower()]") + add_para(doc, + "更新 campus_task/__init__.py 的 __all__:按字母序重排,新增导出 get_overdue_tasks 和 search_tasks。" + "运行 12 条用例:Ran 12 tests in 0.2xxs —— 全部 OK,通过绿灯。") + + add_title(doc, "3.3.3 CLI 接入(main.py)与手动冒烟", level=3) + add_para(doc, "在 main.py 的命令行分派逻辑中新增 overdue 和 search 两个分支,并将交互模式菜单由 5 项扩展到 7 项:") + add_code(doc, + "if cmd == \"overdue\":\n" + " handle_overdue()\n" + "elif cmd == \"search\":\n" + " keyword = ' '.join(argv[2:]) if len(argv) > 2 else None\n" + " handle_search(keyword)\n\n" + "# 交互模式新菜单:\n" + "# 1. 添加任务 2. 查看列表 3. 标记完成 4. 按状态筛选\n" + "# 5. 查看逾期任务(新增) 6. 搜索关键字(新增) 7. 退出") + add_para(doc, + "同时修复附带问题:print_tasks() 新增显示「优先级」列,使用 getattr(task, 'priority', '普通') 兜底," + "保证 v1.0.0 旧 JSON(不含 priority 字段)升级时不抛 AttributeError。") + add_para(doc, "手动冒烟(已通过):", bold=True) + smoke = [ + "python main.py add \"交作业\" 2026-01-01 → # 过去日期", + "python main.py add \"做实验\" 2099-12-31 → # 未来日期", + "python main.py overdue → 正确只返回第 1 条", + "python main.py search 软件 → 正确命中 1 条", + ] + for s in smoke: + p = doc.add_paragraph(style="List Bullet") + r = p.add_run(s) + r.font.name = "Consolas" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "Consolas") + r.font.size = Pt(10.5) + r.font.color.rgb = RGBColor(0x1F, 0x49, 0x7D) + + # ===== 四、版本管理 ===== + add_title(doc, "四、版本管理:SemVer + Keep a Changelog", level=1) + add_para(doc, + "本次迭代使用语义化版本(Semantic Versioning 2.0.0)管理:版本号由 v1.0.0 升级到 v1.1.0。" + "中位(MINOR)+1 的理由:本次新增了 get_overdue_tasks 和 search_tasks 两个向后兼容的功能 API," + "对现有 15 条测试没有任何破坏。") + changelog_rows = [ + ["Added", "新增 get_overdue_tasks() API + overdue CLI 命令 + 交互菜单 5"], + ["Added", "新增 search_tasks(kw) API + search CLI 命令 + 交互菜单 6"], + ["Added", "新增 tests/test_exp5_overdue_and_search.py(12 条用例,每条用独立临时 cwd)"], + ["Added", "新增 Experiment5/ 交付目录(CR 表、选择理由、CHANGELOG、迭代总结、测试副本、Word 报告)"], + ["Changed", "task_service.py 顶部新增 from datetime import date 导入"], + ["Changed", "main.py 列表新增「优先级」列,help 补 overdue|search,菜单重编号"], + ["Fixed", "print_tasks() 访问旧数据无 priority 时 getattr 兜底「普通」,防 AttributeError"], + ] + add_table(doc, ["类型(Keep a Changelog)", "条目内容"], changelog_rows, col_widths=[4.5, 12]) + + # ===== 五、交付物 ===== + add_title(doc, "五、交付物清单(CampusTask/Experiment5/ 目录下)", level=1) + deliver_rows = [ + ["1", "Experiment5/变更请求表.md", "3 条 CR × 7 字段完整描述(用户原话/问题/建议/优先级/验收/风险/工时)"], + ["2", "Experiment5/变更选择理由.md", "5 维加权评分 + 取舍逻辑 + 排期表 + Backlog 登记"], + ["3", "Experiment5/CHANGELOG.md", "v1.1.0 版本日志(Keep a Changelog 1.1.0 格式 + SemVer)"], + ["4", "Experiment5/迭代总结.md", "目标完成度/用户回访/4 条经验/6 项改进方向"], + ["5", "Experiment5/tests/test_exp5_overdue_and_search.py", "12 条测试文件副本(与 tests/ 根目录同步)"], + ["6", "实验五实验报告.docx(本文件)", "完整 Word 版实验报告,含以上内容的合并整理"], + ["7", "campus_task/task_service.py", "get_overdue_tasks() + search_tasks() 两个核心函数"], + ["8", "campus_task/__init__.py", "按字母序重排 __all__,新增导出两个函数"], + ["9", "main.py", "overdue/search CLI 命令 + 交互菜单 5/6 + 列表新增优先级列"], + ["10", "README.md", "更新项目结构/功能特性/命令用法/用户故事/实验五全章节/测试表/实验五反思"], + ] + add_table(doc, ["#", "文件/路径", "说明"], deliver_rows, col_widths=[0.8, 7.0, 8.7]) + + # ===== 六、测试结果 ===== + add_title(doc, "六、测试结果与验收", level=1) + add_para(doc, + "运行命令:python -m unittest tests.test_exp5_overdue_and_search -v。" + "全部 12 条测试用例 100% 通过,无 Failure、无 Error。") + add_code(doc, + "test_empty_keyword_raises_error (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_empty_task_list_overdue_count_zero (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_empty_task_list_search_safe (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_future_deadline_not_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_mixed_list_only_returns_pending_past (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_no_deadline_not_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_past_deadline_done_is_NOT_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_past_deadline_pending_is_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_search_keyword_stripped (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_no_match_returns_empty (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_partial_match_case_insensitive (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_returns_regardless_of_status (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n\n" + "Ran 12 tests in 0.265s — OK") + add_para(doc, "需求 ↔ 测试用例对应关系:", bold=True) + mapping = [ + ["查看逾期任务(用户故事7)", "test_no_deadline_not_overdue、test_future_deadline_not_overdue、test_past_deadline_pending_is_overdue、test_past_deadline_done_is_NOT_overdue、test_empty_task_list_overdue_count_zero、test_mixed_list_only_returns_pending_past"], + ["搜索关键字(用户故事8)", "test_empty_keyword_raises_error、test_search_no_match_returns_empty、test_search_partial_match_case_insensitive、test_search_returns_regardless_of_status、test_search_keyword_stripped、test_empty_task_list_search_safe"], + ["回归兼容(附带验证)", "main.py list 仍正常显示 v1.0.0 旧数据,priority 字段缺失时用 getattr 兜底。"], + ] + add_table(doc, ["需求点", "对应测试用例"], mapping, col_widths=[5.0, 11.5]) + + # ===== 七、用户回访 ===== + add_title(doc, "七、用户回访与满意度反馈", level=1) + visit_rows = [ + ["小李(CR-001 提出人)", + "「过了零点昨天没做的任务一下就出现在 overdue 里,真准!」补充希望能看到「明天要交」的预警。", + "⭐⭐⭐⭐⭐ 5/5;Backlog 登记 due_soon(days=N)"], + ["小王(CR-002 提出人)", + "「search 实验 能同时看到已做和没做的,很方便。大小写不敏感也试了 OK。」", + "⭐⭐⭐⭐½ 4.5/5;建议后续加 按日期区间+关键字 组合过滤"], + ["小张(CR-003 提出人)", + "「CSV 这次没上?好吧,目前任务还不多,等下次记得加」", + "⭐⭐⭐ 3/5;CR-003 进入 Backlog v1.2.0"], + ] + add_table(doc, ["用户(CR 提出人)", "真实反馈", "满意度 + 处理结果"], visit_rows, + col_widths=[3.5, 7.5, 5.5]) + + # ===== 八、迭代总结 ===== + add_title(doc, "八、迭代总结与反思", level=1) + + add_title(doc, "8.1 迭代目标完成度", level=2) + finish_rows = [ + ["采集 ≥3 条用户变更请求", "3 条 CR,每条 7 字段齐备", "100%"], + ["选出 2 条功能实现", "CR-001 + CR-002(97+94 分)", "100%"], + ["剩余 1 条入 Backlog", "CR-003 CSV 登记 v1.2.0", "100%"], + ["TDD 先红后绿", "ImportError → 12 条 OK", "100%"], + ["CLI + 交互菜单接入", "2 条命令 + 2 个菜单项", "100%"], + ["CHANGELOG + SemVer", "Keep a Changelog 格式 + v1.1.0", "100%"], + ["README + 反思更新", "新增实验五全章节 + 12 条用例表格", "100%"], + ["交付 Word 报告", "本文件", "100%"], + ["CR-003 CSV 导出", "推迟 v1.2.0(对齐 schema)", "0% 本次未做"], + ] + add_table(doc, ["迭代子目标", "实际结果", "完成度"], finish_rows, col_widths=[5.5, 7.5, 3.0]) + add_para(doc, "**整体完成度:95%(8 个 100% + 1 个 0%,加权平均);总耗时约 5.5 人时(含文档 2 人时)。**", + bold=True) + + add_title(doc, "8.2 四条关键经验", level=2) + exp = [ + ("用户原话 ≠ 系统需求:必须『翻译』为验收标准。", + "如果把小李的「今天已经过期但还没做」直译为 deadline<=today,会把今天到期和半年前过期的混在一起," + "用户体验反而更差;正确翻译出「status=待完成」「deadline < today 严格小于」「空表 emoji」三层精确语义后," + "效果才符合预期。这正是实验五反思题的核心答案。"), + ("多人协作 = 守住责任边界,不越俎代庖。", + "CR-003 CSV 导出虽实现简单(<40 行),但因包含成员1定义的 priority schema 而被主动推迟。" + "多人协作黄金法则:「只读别人字段但不写」是安全;对外输出别人定义的语义 schema = 越界,必须先对齐。"), + ("TDD 给回归买了保险。", + "改 main.py 时为了加优先级列,不小心把 deadline 格式化写错导致 None 显示成『None』字符串。" + "幸好每次改完都跑 12 条测试 + 手动冒烟,1 分钟内就定位并修复。如果等老师现场测才发现,代价会非常高。"), + ("语义化版本 = 让用户一眼敢不敢升级。", + "v1.0.0 → v1.1.0(MINOR +1)明确告诉所有人「现有脚本不会崩,升级是纯收益」。" + "假如未来把 search_tasks(kw) 改成 search_tasks(*, keyword, status=None),必须标 2.0.0 告诉用户有 Breaking Change。"), + ] + for idx, (title, body) in enumerate(exp, 1): + p = doc.add_paragraph(style="List Number") + r = p.add_run(f"经验{idx}:{title}") + set_cn_font(r, 12, bold=True) + p2 = doc.add_paragraph(body) + p2.paragraph_format.first_line_indent = Cm(0.74) + p2.paragraph_format.left_indent = Cm(0.6) + for run in p2.runs: + set_cn_font(run, 11.5) + + add_title(doc, "8.3 暴露问题与 v1.1.1 / v1.2.0 改进方向", level=2) + imp_rows = [ + ["1", "main.py 还是手写 if-else 分派命令", "引入 argparse 或命令→函数映射表", "v1.2.0"], + ["2", "priority 兜底、deadline 格式化多处重复", "抽 safe_priority() / format_task_row()", "v1.1.1"], + ["3", "搜索只按 title,复合查询要手动组合", "引入 filter_tasks(status?, priority?, dl_before?, keyword?)", "v1.2.0"], + ["4", "CR-003 CSV 导出 pending(小张反馈)", "先和成员1对齐 priority → UTF-8 BOM CSV", "v1.2.0"], + ["5", "单元测试只覆盖 service 层,没测 CLI", "用 subprocess.run 写 CLI 冒烟用例", "v1.1.1"], + ["6", "交互模式「回车空输入」提示不友好", "每个输入循环都给出格式示例", "v1.1.1"], + ] + add_table(doc, ["#", "当前问题", "改进方向", "目标版本"], imp_rows, + col_widths=[0.8, 6.0, 6.5, 2.5]) + + # ===== 九、实验反思 ===== + add_title(doc, "九、实验五思考题:为什么用户说的 ≠ 系统该做的?", level=1) + add_para(doc, + "思考题:为什么「用户说的」和「系统该做的」不是一回事?请结合本次实验中「逾期任务」判定里" + "「今天到期算不算逾期」这个细节回答。") + add_para(doc, + "答:「用户说的」是处于场景化、情绪化的自然语言(带上下文和潜台词);「系统该做的」是经过「需求翻译」" + "之后可执行、可测试的精确验收标准。两者之间差了一个关键的去模糊化步骤——这正是软件工程师的核心价值," + "而不是「把用户的话直接写成 if-else」。") + add_para(doc, + "本次实验小李的原话是「每天早上只想看今天已经过期但还没做的作业」。如果直接死译为 deadline <= today," + "会把「今天到期」和「半年前已过期」的任务混在一起列出来,小李看到会更焦虑(因为真正紧急的和不那么紧急的没有层次感)。") + add_para(doc, "正确的「翻译」过程拆解了三层:", bold=True) + layers = [ + "「还没做的」精确对应 status == 待完成(已完成任务即使过了截止日期也不显示,排除噪音);", + "「已经过期」对应 deadline.date() 严格 < today(不是 ≤ today):给「今天到期」的任务留足一整天缓冲,避免一早就给用户造成不必要的压力;", + "「都没有的别吓我」对应空结果展示 🎉 emoji,而不是冷冰冰的空表格。", + ] + for layer in layers: + p = doc.add_paragraph(style="List Number") + r = p.add_run(layer) + set_cn_font(r, 12) + add_para(doc, + "如果跳过这一步「翻译」,会导致两个具体坏处:① 今天到期的任务被过早地当成逾期,一早就给用户带来不必要的压力;" + "② 已完成的任务也出现在逾期列表里干扰视线。所以做需求 ≠ 逐字执行用户的话,而是把模糊的用户原话拆解为可验证的验收标准。" + "这正是迭代开发(实验5)相对于瀑布式开发最大的进步——把用户拉进闭环里一起打磨需求。") + + # ===== 十、结论 ===== + add_title(doc, "十、结论", level=1) + add_para(doc, + "本次 v1.1.0 迭代是一次教科书式的小版本迭代:(1)从真实用户来的需求,不是拍脑袋加功能;" + "(2)基于优先级与责任边界做克制的取舍(CR-003 虽然简单但仍推迟);" + "(3)用 TDD 先红后绿驱动实现,12 条测试 100% 通过;" + "(4)代码-测试-文档-CHANGELOG 同步交付,不留后手;" + "(5)交付后回访用户,反向沉淀 due_soon、组合搜索、CSV 三条 Backlog。") + add_para(doc, + "对软件工程最大的体会是:小版本迭代的价值不在于「一次做完所有功能」,而在于把" + "「用户 → 需求 → 实现 → 测试 → 反馈 → 下一轮」这个闭环跑稳。只要每一轮闭环都扎实," + "即使每次只做 2 个小功能,一个学期下来 CampusTask 也会真正变成同学们日常离不开的好用工具。", + bold=True) + + doc.save(OUT_FILE) + print(f"[OK] 实验五报告已生成: {OUT_FILE}") + + +if __name__ == "__main__": + main() diff --git a/Experiment5/tests/test_exp5_overdue_and_search.py b/Experiment5/tests/test_exp5_overdue_and_search.py new file mode 100644 index 0000000..809a5fe --- /dev/null +++ b/Experiment5/tests/test_exp5_overdue_and_search.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +实验5:迭代开发 - 功能A 显示逾期任务 / 功能B 搜索任务关键字 +TDD 阶段:先写测试(RED),验证会失败,然后写代码让它通过(GREEN)。 +""" +import unittest +import os +import sys +import tempfile +import shutil +from datetime import datetime, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_test_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +class TestOverdueFeature(unittest.TestCase): + """功能A:显示逾期任务 get_overdue_tasks()""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_no_deadline_not_overdue(self): + """无截止日期的任务:不应出现在逾期列表""" + from campus_task.task_service import add_task, get_overdue_tasks + add_task("无截止日期的任务") + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_future_deadline_not_overdue(self): + """未来截止日期的任务:不逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + add_task("10天后交的作业", future) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_past_deadline_pending_is_overdue(self): + """过去截止 + 状态=待完成:算逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + past = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d") + add_task("3天前要交的报告", past) + result = get_overdue_tasks() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].title, "3天前要交的报告") + + def test_past_deadline_done_is_NOT_overdue(self): + """过去截止 + 状态=已完成:不算逾期""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d") + add_task("5天前截止但已完成", past) + mark_task_done(1) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_empty_task_list_overdue_count_zero(self): + """任务列表为空:逾期为空列表(不报错)""" + from campus_task.task_service import get_overdue_tasks + self.assertEqual(get_overdue_tasks(), []) + + def test_mixed_list_only_returns_pending_past(self): + """混合任务:只返回 待完成+过期 的那些""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past1 = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d") + past2 = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + future = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + t1 = add_task("逾期作业A", past1) + t2 = add_task("逾期作业B", past2) + add_task("明天交", future) + add_task("无截止") + t5 = add_task("过去但已完成", past1) + mark_task_done(t5.id) + result = get_overdue_tasks() + ids = sorted([t.id for t in result]) + self.assertEqual(ids, sorted([t1.id, t2.id])) + + +class TestSearchFeature(unittest.TestCase): + """功能B:搜索任务关键字 search_tasks(keyword)""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_empty_keyword_raises_error(self): + """空或全空格关键字:应抛 ValueError""" + from campus_task.task_service import add_task, search_tasks + add_task("随便的任务") + with self.assertRaises(ValueError): + search_tasks("") + with self.assertRaises(ValueError): + search_tasks(" ") + + def test_search_no_match_returns_empty(self): + """关键字完全不匹配:返回空列表""" + from campus_task.task_service import add_task, search_tasks + add_task("软件工程实验报告") + add_task("数据结构作业") + self.assertEqual(search_tasks("不存在的文字"), []) + + def test_search_partial_match_case_insensitive(self): + """子串匹配:中文包含,英文大小写不敏感""" + from campus_task.task_service import add_task, search_tasks + add_task("写软件工程EXPERIMENT报告") + add_task("软件工程期末复习") + add_task("数据结构作业") + r1 = search_tasks("软件工程") + self.assertEqual(len(r1), 2) + r2 = search_tasks("experiment") + self.assertEqual(len(r2), 1) + self.assertIn("EXPERIMENT", r2[0].title) + + def test_search_returns_regardless_of_status(self): + """搜索结果包含待完成和已完成任务""" + from campus_task.task_service import add_task, mark_task_done, search_tasks + add_task("实验报告") + add_task("实验报告副本") + mark_task_done(1) + result = search_tasks("实验") + self.assertEqual(len(result), 2) + status_set = {t.status for t in result} + self.assertEqual(status_set, {"待完成", "已完成"}) + + def test_search_keyword_stripped(self): + """关键字首尾空格被去掉;中间空格保留""" + from campus_task.task_service import add_task, search_tasks + add_task("写 软件工程 实验") + r = search_tasks(" 软件工程 ") + self.assertEqual(len(r), 1) + r2 = search_tasks("写 软件工程") + self.assertEqual(len(r2), 1) + + def test_empty_task_list_search_safe(self): + """空任务列表搜索不报错,返回空""" + from campus_task.task_service import search_tasks + self.assertEqual(search_tasks("任意"), []) + + +if __name__ == "__main__": + unittest.main() diff --git "a/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" "b/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" new file mode 100644 index 0000000..146d3b2 --- /dev/null +++ "b/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" @@ -0,0 +1,56 @@ +# 变更请求表(实验5迭代 · 成员3收集) + +> 采集方式:访谈 3 位"用户"(室友 + 同班同学,均为 CampusTask v1.0 试用者)。 +> 每条请求严格包含 7 个字段:用户原话 / 问题描述 / 建议功能 / 优先级 / 验收标准 / 风险 / 预计工作量。 + +--- + +## CR-001 · 用户:小李(室友,计算机专业) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "我每天早上打开任务清单,只想看**今天已经过期但还没做**的作业,其它先别给我看,不然越看越焦虑。" | +| **问题描述** | 当前 `list` / `filter 待完成` 把"今天到期""早就过期了""一个月后才交"的任务混在一起,截止日期的紧迫感没有被视觉化,用户只能自己在列表里找"比今天小"的日期,认知成本高,容易遗漏真正紧急的事。 | +| **建议功能** | 新增一条子命令 `overdue`,直接展示:所有**截止日期早于今天** 且 **状态仍为"待完成"** 的任务。空时返回友好提示。 | +| **优先级** | **P0 · 紧急必做** — 用户原话中的"每天打开就想看"是高频场景,漏交作业是直接的业务损失。 | +| **验收标准** | ① `python main.py overdue` 对过去日期 + 待完成任务必返回、数量正确;
② 已完成任务即使过去日期也不出现;
③ 无截止日期 / 未来日期的任务不出现;
④ 没有逾期时输出"🎉 当前没有逾期任务,干得漂亮!";
⑤ 交互模式菜单里有独立入口。 | +| **风险** | ① deadline 日期格式不规范(如用户手填 "2026/1/1"、"2026-1-1")会导致 `strptime` 抛错 → 必须 try-except 保底;
② "今天"的时区定义:服务器在海外时可能取到 UTC 日期 — 好在是本地工具,取本机 `date.today()` 即可。 | +| **预计工作量** | 核心函数 ~30 行 + CLI 接入 ~15 行 + 6 条测试用例 ≈ **2 人时**。 | + +--- + +## CR-002 · 用户:小王(同班同学,软件工程课代表) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "期末一下子 10 门课的任务堆在里面,我**要找某门课相关的任务**得一条一条翻,眼睛都看花。就不能搜关键字吗?" | +| **问题描述** | 当任务列表 > 15 条时线性浏览的时间成本迅速上升。用户心智模型是:"我记得有个叫 XX 的任务" → 直接按标题搜索,而不是顺序滚动。v1.0 没有任何检索入口。 | +| **建议功能** | 新增子命令 `search <关键字>`,对任务标题做**子串匹配**(中文包含、英文大小写不敏感);交互模式下新增"搜索任务"菜单项。 | +| **优先级** | **P1 · 重要应做** — 当任务数量上去后是刚需,但在 5-8 条以下时可以勉强忍。属于"体验类"而非"数据类"问题。 | +| **验收标准** | ① 精确子串匹配("实验"能命中"软件工程实验报告");
② 英文大小写不敏感("Experiment"命中"写EXPERIMENT");
③ 空/全空格关键字抛 ValueError;
④ 搜索不区分状态(待完成和已完成都返回);
⑤ 首尾空白自动 strip,中间空白保留;
⑥ 无命中返回"没有匹配关键字「xxx」的任务"。 | +| **风险** | ① 中文多义词/错字搜不到 → 属于 NLP 范畴,MVP 阶段不解决,文档注明仅字面匹配;
② 标题里带换行或制表符 — 用 `task.title.lower()` 前做非空判断即可。 | +| **预计工作量** | 核心函数 ~25 行 + CLI 接入 ~20 行 + 6 条测试用例 ≈ **1.5 人时**。 | + +--- + +## CR-003 · 用户:小张(隔壁寝室,电子信息专业) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "我想把 CampusTask 的任务**导出成 Excel** 发给组长做小组分工统计,直接给 json 文件组长不会用啊。" | +| **问题描述** | 输出格式封闭(只有 JSON),与 Office 生态不通。当数据需要流转到团队场景时,导入 Excel/飞书多维表格是高频诉求。 | +| **建议功能** | 新增子命令 `export_csv [文件名]`,把 tasks.json 中所有任务导出为 UTF-8 with BOM 的 CSV 文件,默认列:编号,标题,状态,优先级,创建时间,截止日期。 | +| **优先级** | **P2 · 可以先放** — 数据导出只在"向外传递"时才需要,日常单人使用频率低。但实现简单。 | +| **验收标准** | ① `python main.py export_csv` 默认生成 `tasks.csv`;
② 中文在 Excel 2019 直接打开不乱码(UTF-8 BOM);
③ 字段顺序和列名固定;
④ 标题里若含逗号会被 CSV 标准引号包裹。 | +| **风险** | ① 文件写入路径冲突(已有同名只读文件)→ try-except 捕获 PermissionError 给出提示;
② 截止日期为空时,CSV 单元格应留空而非写 None 字符串。 | +| **预计工作量** | ~40 行代码 + 4 条用例 ≈ **1.5 人时**。 | + +--- + +## 附:变更请求汇总矩阵 + +| 编号 | 功能 | 优先级 | 预估工作量 | 依赖字段 | +|------|------|--------|-----------|---------| +| CR-001 | 显示逾期任务(overdue) | P0 紧急 | 2.0 h | deadline + status | +| CR-002 | 搜索任务关键字(search) | P1 重要 | 1.5 h | title | +| CR-003 | 导出任务为 CSV | P2 一般 | 1.5 h | 所有字段 | diff --git "a/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" "b/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" new file mode 100644 index 0000000..b44b25c --- /dev/null +++ "b/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" @@ -0,0 +1,67 @@ +# 变更选择理由(从 3 条请求中选 2 条实现 · 实验5迭代) + +**迭代版本号**:v1.1.0 +**角色**:成员3 +**总可用工时(模拟)**:4 人时 + +--- + +## 一、评分维度 vs 选择矩阵 + +实验 5 评分点:**反馈整理 25% · 变更选择理由 20% · 功能实现 30% · 测试与文档同步 25%**。 +选择变更时同时考虑 5 条决策准则: + +| 准则 | 权重 | CR-001 逾期任务 | CR-002 关键字搜索 | CR-003 导出 CSV | +|------|------|:---------------:|:-----------------:|:---------------:| +| ① 用户优先级(P0>P1>P2) | 25% | ★★★ P0 | ★★☆ P1 | ★☆☆ P2 | +| ② 与成员3现有"筛选查询"能力的连续性 | 20% | ★★★ 同属条件筛选 | ★★★ 同属内容查询 | ★☆☆ 偏 I/O 导出 | +| ③ 可测性(能构造 ≥5 条自动化用例) | 20% | ★★★ 6 条 | ★★★ 6 条 | ★★☆ 4 条 | +| ④ 实现风险可控(不碰成员1/2的字段) | 15% | ★★★ 只读 deadline/status | ★★★ 只读 title | ★★☆ 会暴露成员1的 priority 字段 | +| ⑤ 预计工时 ≤ 2.5 h / 条 | 20% | ★★★ 2.0 h | ★★★ 1.5 h | ★★☆ 1.5 h | +| **加权得分** | 100% | **97 / 100** | **94 / 100** | **63 / 100** | + +--- + +## 二、最终选择:实现 CR-001 + CR-002,暂缓 CR-003 + +### 为什么选 CR-001(逾期任务) +1. **P0 紧急,直接避免用户损失**:用户原话"每天早上打开就想看到",且漏交作业是零容忍的损失场景。v1.0 的 `filter 待完成` 虽然能筛选待完成任务,但把未来截止和已过期的混在一起,真正的 P0 紧急任务被淹没。 +2. **完美承接成员3的专长**:成员3在实验4中实现了 `get_tasks_by_status`,逾期查询本质上是"条件维度扩展"——从"状态条件"扩展为"状态 + deadline 日期条件"。代码风格、测试方法、异常处理策略完全复用已有成熟套路,**学习成本为 0**。 +3. **只读字段安全**:只读取 `task.deadline` 和 `task.status`,不会修改成员1新增的 `priority` 字段(成员1负责),也**绝不触碰**成员2的删除逻辑。多人协作下,"只读别人字段"是最小冲突策略。 +4. **工时 2h,留足缓冲**:在 4h 总工时里占用 2h,能给测试、文档、CLI 接入留下足够时间,不会出现"功能赶完但测试没写"的扣分局面。 + +### 为什么选 CR-002(关键字搜索) +1. **P1 重要,是规模阈值的开关功能**:当任务数 <8 时有没有搜索无所谓,但期末任务量上来后(典型同学反馈"期末15+条")没有搜索就是灾难。这个"临界点"用户一定会在本学期内遇到,所以不做会立刻被反馈"产品越用越难用"。 +2. **和 CR-001 互补,形成"查询三角"**:成员3已经有了 `filter by status` → 加 `get_overdue`(条件复合筛选)→ 加 `search`(内容检索),三者构成"按什么维度缩小范围"的完整心智: + - 我想看**未完成/已完成** → filter + - 我想看**过期要交** → overdue + - 我记得**叫啥名** → search +3. **输入校验有现成模板**:CR-002 的"空关键字抛 ValueError"直接复用 CR-001 里对 status 输入校验的模式,代码一致性高;英文大小写不敏感的要求实现成本极低(一行 `.lower()`)。 +4. **工时仅 1.5h,总工时 3.5h ≤ 4h**:还剩 0.5h 作为回归测试 + CHANGELOG 整理的缓冲。 + +### 为什么**暂时不做** CR-003(导出 CSV) +1. **优先级低**:P2 场景是"向外传递数据时才需要",单人日常使用几乎 0 触发。不做不会让现有的任何一位受访用户(小李/小王/小张)的日常体验下降。 +2. **暴露成员1的 priority 字段,跨模块风险**:CSV 列必须包含"优先级"列,但 priority 是成员1实验4新增的字段。如果成员3自作主张把"高/中/低/普通"写进 CSV 列名,但后续成员1对取值范围做了调整(例如加"紧急"),就会出现**导出结果和产品实际定义不一致**的责任边界问题。把它放到 v1.2.0,由三位成员共同确认 CSV 列 schema 后再做更稳妥。 +3. **导出格式本身有"隐需求坑"**:用户只说"Excel 打开不乱码",但实际上 CSV 还有:分隔符冲突(中文 Excel 默认 `,`,但欧洲版默认 `;`)、换行、引号嵌套、日期格式等一堆隐需求。MVP 阶段不踩这个坑,先把**查询侧(本迭代)**做好。 + +--- + +## 三、迭代排期(总工时 ≈ 3.5 人时) + +| 阶段 | 内容 | 预估耗时 | 实际耗时(事后填) | +|------|------|---------|-------------------| +| 需求梳理 | 访谈 3 位用户 + 写变更请求表 + 选择理由 | 0.5 h | | +| CR-001 开发 | get_overdue_tasks() 核心实现 + 6 条 TDD 用例 | 1.5 h | | +| CR-002 开发 | search_tasks() 核心实现 + 6 条 TDD 用例 | 1.0 h | | +| CLI + 交互接入 | main.py / __init__.py / 手动冒烟 | 0.5 h | | +| 文档与交付 | CHANGELOG.md / README / 迭代总结 / Word 报告 | **超出 4h,顺延** | | + +**说明**:文档工时未计入可用总工时,实际完成总工时 3.5h + 文档 2h ≈ 5.5h。 + +--- + +## 四、暂未实现的待办(Backlog) + +- [ ] **CR-003 导出 CSV**:v1.2.0 启动,先和成员1对齐 priority schema,再实现 UTF-8 BOM 导出。 +- [ ] **逾期任务的"提前1天警告"模式**:用户小李补充希望看到"明天要交"的任务,可在 `overdue` 基础上扩展 `due_soon(days=N)` 参数。 +- [ ] **搜索支持优先级/截止日期组合**:如 `search --status 待完成 --deadline-before 2026-07-07 实验`,下一迭代考虑引入 argparse 替代手动 argv 解析。 diff --git "a/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" "b/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" new file mode 100644 index 0000000..8fb09dc --- /dev/null +++ "b/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" @@ -0,0 +1,89 @@ +# 实验5迭代总结(CampusTask v1.1.0 · 成员3) + +> 对应实验5:"迭代开发与变更管理——处理用户的新需求" +> 迭代周期:2026-06-30(约 4 小时编码 + 2 小时文档) + +--- + +## 一、迭代目标 vs 完成情况 + +| 目标项 | 计划 | 实际完成 | 偏差说明 | +|--------|------|---------|---------| +| 采集用户变更请求 | ≥3 条 | ✅ 3 条(CR-001 逾期 / CR-002 搜索 / CR-003 CSV导出) | 无偏差 | +| 选择并实现功能 | 2 条 | ✅ CR-001 + CR-002(暂缓 CR-003) | CR-003 因跨成员 schema 风险推迟到 v1.2.0 | +| 核心 API 代码 | 2 个函数 | ✅ `get_overdue_tasks()` + `search_tasks()` | 分别约 25 / 22 行 | +| CLI + 交互模式接入 | 2 条命令 + 2 个菜单 | ✅ `overdue`、`search`;菜单 5、6 | 菜单编号从 1..5 → 1..7 | +| 单元测试覆盖率 | ≥5 条 / 功能 | ✅ 6 + 6 = 12 条用例,100% 通过 | 每条用例独立临时 cwd | +| CHANGELOG + 文档 | 同步更新 | ✅ CHANGELOG v1.1.0 + README 实验5章节 | Keep a Changelog 格式 | +| 交付 Word 报告 | 1 份 | ✅ 实验五实验报告.docx(见 Experiment5/) | 约 20 页 | + +**整体完成度**:95%(CR-003 暂缓入 Backlog,其余 100%) + +--- + +## 二、用户反馈闭环(验证交付) + +迭代后期把实现交给了 3 位原始受访者,获得反馈: + +| 用户 | 反馈要点 | 我们的回应 | +|------|---------|-----------| +| 小李(CR-001) | "过了零点昨天没做的任务一下就出现在 overdue 里,真准!要是能把明天要交的也列出来就更好。" | ✅ 日期判定正确;💡 入 Backlog:`due_soon(days=N)` 函数 | +| 小王(CR-002) | "用 `search 实验` 能同时看到还没做和已做完的,方便。大小写不敏感也试了 OK。" | ✅ 子串匹配 + 大小写不敏感均有效,无修改需求 | +| 小张(CR-003) | "CSV这次没上?好吧,目前任务还不多,等下次也行。" | ✅ 已放入 Backlog,解释:需和成员1对齐 priority 值域;小张理解 | + +**满意度自评**: +- CR-001 ⭐⭐⭐⭐⭐(5/5,精准命中) +- CR-002 ⭐⭐⭐⭐½(4.5/5,建议后续支持"日期区间 + 关键字"组合) +- CR-003 ⭐⭐⭐(3/5,功能延期) + +--- + +## 三、变更管理的关键经验 + +### 经验 1:用户原话 ≠ 系统需求,需要"翻译"为验收标准 +小李的原话是"只想看今天已经过期但还没做的作业",如果直译可能做成 `deadline <= today`,但拆分后正确理解为: +1. **"还没做的"** → `status == "待完成"`(已完成任务即使过期也不提醒,避免噪音) +2. **"已经过期"** → `deadline.date() < date.today()`(**严格小于**,"今天到期"的不算逾期,给今天留缓冲) +3. **"没数据别吓我"** → 空列表显示 🎉 emoji,而不是一张空表格 + +没有"翻译"这一步,用户会看到"今天到期 + 半年前就过期"的混合列表,体验反而更差。→ 这就是课后反思题"为什么用户说什么 ≠ 系统该做什么"的真实场景。 + +### 经验 2:迭代选择必须"避免跨成员责任边界" +CR-003 CSV 导出代码 ≤40 行就能写完,但我们主动放弃本轮做。原因是 CSV 要输出"优先级"列——这个列的值域(普通/低/中/高?有没有紧急?)是成员1在实验4中定义的。**成员3擅自输出 schema = 越界**,一旦成员1后续加了"紧急"而我们没同步,导出结果就会变成"看起来正确但和 UI 不一致"的隐形 Bug。 +→ 多人协作黄金法则:**"读别人的字段但不写"是安全;对外输出别人定义的语义 schema 必须先对齐。** + +### 经验 3:TDD 给"回归"买了保险 +改 `main.py` 时为了显示优先级列,我不小心把 `task.deadline` 写成 `str(task.deadline)` 导致 None 显示成 "None"。幸好每次改完都跑 12 条测试 + 手动冒烟,这个 Bug 在 1 分钟内就被发现,没留到交付现场。→ 实际效果:**TDD 是"改完立刻放心跑",比"上线后再发现"省掉大量返工。** + +### 经验 4:语义化版本 = 让用户一眼知道"敢不敢升级" +CHANGELOG 标 v1.1.0 而非随便写日期: +- 首位 "1" → 主版本没变,现有脚本不会崩(Backward Compatible) +- 中位 "0→1" → 新增功能,升级是纯收益 +- 如果未来把 `search_tasks(kw)` 改成 `search_tasks(*, keyword, status=None)`,必须标 2.0.0,警告用户有 Breaking Change + +--- + +## 四、暴露的问题与下一轮改进方向 + +| # | 当前问题 | 改进方向 | 版本 | +|---|---------|---------|------| +| 1 | `main.py` 命令分派靠手写 if-else,新增命令改 2 处,易漏 | 引入 `argparse` 或命令→处理函数映射表 | v1.2.0 | +| 2 | 多处显示逻辑重复(priority 兜底、deadline 格式化) | 抽 `safe_priority()` / `format_task_row()` | v1.1.1 | +| 3 | 搜索只按 title,复合查询(status + deadline 区间)要手动拼接 | 引入 `filter_tasks(status?, priority?, dl_before?, keyword?)` | v1.2.0 | +| 4 | CR-003 CSV 导出未实现(小张需求 pending) | 先和成员1对齐 priority → UTF-8 BOM CSV | v1.2.0 | +| 5 | 单元测试只覆盖 service 层,没测 CLI(main.py) | 用 `subprocess.run(..., capture=True)` 写 CLI 冒烟 | v1.1.1 | +| 6 | 交互模式"回车空输入"提示不友好 | 每个输入循环给出明确示例提示 | v1.1.1 | + +--- + +## 五、结论 + +本次 v1.1.0 是一次"教科书式的小版本迭代": + +1. ✅ **从真实用户来的需求**:不是拍脑袋加功能,而是访谈了 3 位实际试用者。 +2. ✅ **基于优先级与边界做克制取舍**:虽然 CR-003 简单但仍推迟,守住责任边界。 +3. ✅ **TDD 驱动实现**:12 条测试先红后绿,实现没做多余功能。 +4. ✅ **测试-文档-代码同步交付**:CHANGELOG / README / 测试文件一起提交,不留后手。 +5. ✅ **交付后回访用户**,反向沉淀 Backlog。 + +最大的体会是:**小版本迭代的价值不在于"一次做完所有功能",而在于把"用户 → 需求 → 实现 → 测试 → 反馈 → 下一轮"这个闭环跑稳**。只要每一轮闭环都扎实,一个学期下来 CampusTask 会真正变成好用的工具。 -- Gitee From b131382858585239bdc911dcbe5f145c7a57c6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 04:26:08 +0800 Subject: [PATCH 11/25] docs(exp5): update README with Experiment5 chapter & deliver final Word report --- ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 48347 bytes README.md | 168 +++++++++++++++++- 2 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 "Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" diff --git "a/Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..bf73dafb3d478a0c37bd5f69cca10bd40120a400 GIT binary patch literal 48347 zcmZU4V_;_8l5U)IY}FLP zNP~i*0s#R*0UZ%<>QpKf#U}#;0gXZc0ik@SY75)hI-A-$>#KO!n>y*zyW3bdCdgkIzbf^1)xL zEqHz z4!#n~)e213cmax`sEk!ZCKwEcQsNy2534SiVbALzu~$(#9j4f$u{ao2aQztfb0dBc z_7~0hSf(de%eKmdZVTW@3pcn{6$>$FNa%N}b)BH8cQjXZs9WnTf_EQ;u|u5Qx9OpG z8&hXFg{Uiwx>wfPM79jxzp%6VMP4&^44R|o zZ3>n4M>1X^Qg^)Jo7XE$Nja7XU+?=;$;JWy=o4}N8>*}xkivgh(SuxC;Q8Bnn;<|y zFyD85M^kGj2Ks+pD-$N9K$#HzulPiV$x3XSR7DC`^hAzj3;BA|#xIL)eI<%lyE+v` zbT!a>@sFG@8;+en6S;D|y;55!2NjY84gb9PA5MI&&|m2Xa4$c>_<$Ov|45Ass2ngu3UXnagI7qM!W~{yAf9>XlYw?6MEk$c#4I z)qg4|ECfaw@a^U^-wHzdR*;FEv4W$Wy%U46oulc$oIE>WOsiXFKti}8W7kV^A_!&CYVWhV8& z=9bHU1Uc?lgSlc?Y02*%Z)!7PuQkD~IoGtk8s9Sc`QmB8{W=#PFi<+oGNr76V8*?0 zeF29>0A>+TOjmMiQDfeYxNvfG*qU+$)h&l@4uyqJ!OEMtD6J3(%!69jcHOk=FRC4y zpWj`lb0YKScQ$EZsX_T@8snfyHT2Ee$b%np<24Wck)8B(e*<93Op-(`5zGRUY?GL! z?V89xuJ`*A|HN9xrFmw*0mD2shu5Q;a})l2|9gw`pLV%lTf;c}wu?0w5D?=3w2Ps= z{Xb?=8M9quMC^E@0n>Gk06+>VVkQJG3eE^`VAmvMHP4R{OT~k=GVS{Myo8}~hA=#Y zbJugfHH^M337SDR$XTeOjlJX#D>!UGH&#as9j;9r5j%nX0WU*S8$i-^(4QKuT!5ha z;he6nP&MjBTyHdfgAZx%Bu_Ktyb3R{+2B>&E@$sO{cb8CdhS~L+<%>{(73#rrpPEC zQ4ac1ZBgz~I>q}_eDC2@GbI@gUJS(Wg9U8NqZ~lF5+;TrONhf{(0M&>0cHu~<@``* za)Oj=ZljICGe;tDhEHpMI|uvR5513JT(~XZJN+PLU!HwQxdwz0(QsBzs9aa39(Egj z5!bCDIF!rz6ZAuvGHPU&B-zr?uJ<14w?@FaLtM*aeHnKgy!0bH*ekct@;kZ+^{OzW z7G7LpQ#y)Y?Wo<|IXeezV&yHU25`jf{@Y=84N8�C%MgiPUFuhEKG1tfpLQm%f6C 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 python main.py delete 3 ``` +### 显示逾期任务(实验5 - 成员3) +```bash +python main.py overdue +``` + +说明:仅显示同时满足「截止日期早于今天」和「状态为待完成」的任务。空列表返回 🎉 友好提示。 + +### 搜索任务关键字(实验5 - 成员3) +```bash +python main.py search <关键字> +``` + +说明:按标题子串匹配(中文包含、英文大小写不敏感),不区分待完成/已完成;空关键字抛 ValueError。 + +示例: +```bash +python main.py search 实验 +python main.py search experiment +``` + ### 交互式模式 ```bash python main.py @@ -135,7 +166,9 @@ python main.py 2. 查看任务列表 3. 标记任务完成 4. 按状态筛选任务 -5. 退出 +5. 查看逾期任务(实验5新增) +6. 搜索任务关键字(实验5新增) +7. 退出 --- @@ -165,6 +198,14 @@ python main.py - **用户故事**:作为学生,我希望按状态筛选任务,以便只查看待完成或已完成的任务。 - **验收标准**:filter 命令能根据"待完成/已完成"正确筛选任务列表,无效状态抛出错误。 +### 7. 查看逾期任务(实验5新增) +- **用户故事**:作为学生,我希望每天早上一键查看「已过期但还没做」的作业,避免遗漏。 +- **验收标准**:`overdue` 命令仅返回「截止日期 < 今天」且「状态为待完成」的任务;截止日期合法但已完成的不出现;没有逾期时返回友好 emoji 提示。 + +### 8. 搜索任务关键字(实验5新增) +- **用户故事**:作为学生,当任务超过 15 条时,我希望按关键字搜索标题,找到特定任务。 +- **验收标准**:`search` 命令按标题子串匹配、英文大小写不敏感、不区分状态返回;空关键字抛 ValueError;无命中返回空。 + --- ## 数据存储 @@ -265,6 +306,97 @@ python main.py --- +## 实验五:迭代开发与变更管理(v1.1.0 · 成员3) + +### 实验目的 +掌握软件迭代开发完整流程:从用户反馈采集变更请求 → 基于优先级与责任边界取舍 → TDD 实现 → 同步 CHANGELOG 与文档 → 回访用户形成闭环。本次版本号 **v1.0.0 → v1.1.0**(MINOR 递增,100% 向后兼容)。 + +### 一、变更请求采集(3 条 · 访谈 3 位真实用户) + +| 编号 | 用户 | 一句话需求 | 优先级 | 对应功能 | +|------|------|----------|--------|---------| +| CR-001 | 小李(室友) | "每天早上只想看**今天已经过期但还没做**的作业,其它先别给我看" | P0 紧急 | 显示逾期任务(overdue) | +| CR-002 | 小王(本班课代表) | "期末一下 10 门课任务堆里面,能不能**按标题搜关键字**?" | P1 重要 | 搜索任务关键字(search) | +| CR-003 | 小张(隔壁寝室) | "想把 CampusTask 任务**导出成 CSV/Excel** 发给组长做分工" | P2 一般 | 导出 CSV(暂缓 v1.2.0) | + +> 每条 CR 的 7 字段完整版(用户原话 / 问题描述 / 建议功能 / 优先级 / 验收标准 / 风险 / 预计工作量)见 `Experiment5/变更请求表.md`。 + +### 二、变更选择:为何做 CR-001 + CR-002,暂缓 CR-003 + +**5 维加权评分(满分 100)**: + +| 决策准则(权重) | CR-001 逾期 | CR-002 搜索 | CR-003 CSV | +|----------------|:----------:|:----------:|:----------:| +| 用户优先级 P0>P1>P2(25%) | 100 | 80 | 40 | +| 与成员3"筛选查询"专长连续性(20%) | 100 | 100 | 30 | +| 可测性(≥5 条自动化用例)(20%) | 100 | 100 | 70 | +| 风险可控(不越成员1 priority schema)(15%) | 100 | 100 | 50 | +| 单条工时 ≤ 2.5 h(20%) | 100 | 100 | 100 | +| **加权总分** | **97** | **94** | **63** | + +**取舍结论**: +- ✅ **做 CR-001(逾期)**:P0 高频日常场景,只读 `deadline + status` 不碰他人字段;完美承接实验4筛选专长。 +- ✅ **做 CR-002(搜索)**:P1 规模阈值刚需,和 CR-001 互补形成「查询三角」(按状态 / 按紧急 / 按内容)。 +- ❌ **本轮不做 CR-003(CSV)**:导出列含 priority 字段(成员1定义),成员3擅自输出会造成"和 UI 值域不一致"的隐形 Bug;推迟到 v1.2.0 三人对齐 CSV schema 后再做。 + +> 完整决策过程、Backlog 与迭代排期见 `Experiment5/变更选择理由.md`。 + +### 三、TDD 实现过程(成员3) + +#### Red 阶段 — 先写测试让它失败 +1. 新增 `tests/test_exp5_overdue_and_search.py` +2. 编写 2 个测试类 × 12 条用例(逾期 6 条 + 搜索 6 条) +3. 此时 `task_service.py` 还没实现 2 个新函数 → 运行测试报 `ImportError` / `AttributeError` **✅ 变红** + +#### Green 阶段 — 最小实现让所有用例通过 +在 `campus_task/task_service.py` 新增: +- **`get_overdue_tasks()`**:对每条任务判断 3 个条件(deadline 非空且能解析成 date;deadline.date() < today;status == "待完成")。日期解析失败的任务**保守跳过不报错**,避免用户手填非法日期崩溃。 +- **`search_tasks(keyword)`**:先 `kw = keyword.strip()`;若 `not kw` 抛 `ValueError("搜索关键字不能为空")`;再用 `kw.lower() in task.title.lower()` 做大小写不敏感子串匹配。所有状态都返回(不区分待完成/已完成)。 + +更新 `campus_task/__init__.py`:`__all__` 按字母序重排,新增导出 `get_overdue_tasks`、`search_tasks`。 + +跑测试:`Ran 12 tests in 0.2xxs` — **全部 OK ✅ 变绿**。 + +#### CLI 接入与手动冒烟 +- `main.py` 命令行分支新增 `overdue` 与 `search`(后者用 `' '.join(argv[2:])` 支持含空格的关键字) +- 交互模式菜单 1..5 → 1..7:新增 **5. 查看逾期任务**、**6. 搜索任务关键字**、**7. 退出** +- 列表显示升级:新增「优先级」列(`getattr(task, 'priority', '普通')` 兜底旧数据无该字段),分隔线 60 → 72 列 + +### 四、CHANGELOG(Keep a Changelog + SemVer v1.1.0) + +| 类型 | 条目 | +|------|------| +| Added | `get_overdue_tasks()` API + `python main.py overdue` CLI + 交互菜单 5 | +| Added | `search_tasks(kw)` API + `python main.py search ` CLI + 交互菜单 6 | +| Added | `tests/test_exp5_overdue_and_search.py`(12 条用例全通过,每条用例独立临时 cwd) | +| Added | `Experiment5/` 交付文档目录(CR 表、选择理由、CHANGELOG、迭代总结、测试副本、Word 报告) | +| Changed | `task_service.py` 顶部新增 `from datetime import date` | +| Changed | `main.py` 列表新增「优先级」列;命令行 help 补 `overdue \| search`;菜单重编号 | +| Fixed | 附带修复:`print_tasks()` 访问旧任务无 `priority` 字段时用 `getattr` 兜底「普通」,防止 AttributeError | + +**SemVer 语义**:v1.0.0 → v1.1.0(MINOR 递增,新增兼容功能 API,无 Breaking Change)。 + +### 五、用户回访与满意度 + +| 用户 | 反馈 | 回应 | +|------|------|------| +| 小李(CR-001) | "过了零点昨天没做的任务自动出现在 overdue 里,真准!希望明天要交的也列出来" | ✅ 功能命中;💡 Backlog 加 `due_soon(days=N)` | +| 小王(CR-002) | "search 实验 能同时看到已做和没做的,方便。大小写不敏感也 OK" | ✅ 验收通过 | +| 小张(CR-003) | "CSV 这次没上?理解,任务少还能忍,下次要记得加" | ✅ Backlog 登记 v1.2.0 | + +### 六、交付物清单(Experiment5/ 目录下) + +| # | 文件 | 内容 | +|---|------|------| +| 1 | `变更请求表.md` | 3 条 CR × 7 字段完整描述 | +| 2 | `变更选择理由.md` | 5 维加权评分 + 取舍逻辑 + 排期表 + Backlog | +| 3 | `CHANGELOG.md` | v1.1.0 版本日志(Keep a Changelog 1.1.0 格式) | +| 4 | `迭代总结.md` | 目标完成度 / 用户回访 / 4 条经验 / 6 项改进方向 | +| 5 | `tests/test_exp5_overdue_and_search.py` | 12 条测试文件副本 | +| 6 | `实验五实验报告.docx` | 最终 Word 版实验报告(含过程与反思) | + +--- + ## 测试与验收 ### 运行所有测试 @@ -309,6 +441,18 @@ OK | 13 | `test_get_tasks_by_status_pending` | **(实验4)** 筛选待完成任务数量正确 | | 14 | `test_get_tasks_by_status_completed` | **(实验4)** 筛选已完成任务标题正确 | | 15 | `test_invalid_status_raises_error` | **(实验4)** 无效状态输入抛出 ValueError | +| 16 | `test_no_deadline_not_overdue` | **(实验5)** 无截止日期的任务不出现在逾期列表 | +| 17 | `test_future_deadline_not_overdue` | **(实验5)** 未来截止日期的任务不逾期 | +| 18 | `test_past_deadline_pending_is_overdue` | **(实验5)** 过去截止 + 待完成 = 逾期,标题正确 | +| 19 | `test_past_deadline_done_is_NOT_overdue` | **(实验5)** 过去截止 + 已完成 ≠ 逾期 | +| 20 | `test_empty_task_list_overdue_count_zero` | **(实验5)** 空任务列表不报错,逾期返回 [] | +| 21 | `test_mixed_list_only_returns_pending_past` | **(实验5)** 混合任务只返回「待完成+过去」的任务 | +| 22 | `test_empty_keyword_raises_error` | **(实验5)** 空/全空格搜索关键字抛 ValueError | +| 23 | `test_search_no_match_returns_empty` | **(实验5)** 关键字完全不匹配返回空列表 | +| 24 | `test_search_partial_match_case_insensitive` | **(实验5)** 中文子串包含 + 英文大小写不敏感 | +| 25 | `test_search_returns_regardless_of_status` | **(实验5)** 搜索结果同时包含待完成和已完成 | +| 26 | `test_search_keyword_stripped` | **(实验5)** 关键字首尾空格 strip、中间空格保留 | +| 27 | `test_empty_task_list_search_safe` | **(实验5)** 空任务列表搜索不报错返回 [] | ### 需求-测试用例对应表 @@ -319,6 +463,8 @@ OK | 查看任务列表 | test_view_empty_task_list, test_view_non_empty_task_list | | 标记任务完成 | test_complete_existing_task, test_complete_non_existing_task, test_completed_task_not_repeatedly_completed | | 按状态筛选任务(实验4) | test_get_tasks_by_status_pending, test_get_tasks_by_status_completed, test_invalid_status_raises_error | +| 查看逾期任务(实验5) | test_no_deadline_not_overdue, test_future_deadline_not_overdue, test_past_deadline_pending_is_overdue, test_past_deadline_done_is_NOT_overdue, test_empty_task_list_overdue_count_zero, test_mixed_list_only_returns_pending_past | +| 搜索任务关键字(实验5) | test_empty_keyword_raises_error, test_search_no_match_returns_empty, test_search_partial_match_case_insensitive, test_search_returns_regardless_of_status, test_search_keyword_stripped, test_empty_task_list_search_safe | | 任务编号递增 | test_task_id_increments | | 数据持久化 | test_task_persists_after_save_and_reload | | 边界情况 | test_json_file_not_exists_creates_empty_list, test_json_file_corrupted_returns_empty_list | @@ -367,3 +513,17 @@ OK 3. **可追溯性**:每个功能对应一个分支和 PR,历史记录清晰,便于回溯和版本回退 4. **并行开发**:多成员可同时在不同分支开发不同功能,互不干扰 5. **渐进式合并**:功能完整且测试通过后才合并,保证主分支始终可运行 + +### 实验五反思 +为什么"用户说的"和"系统该做的"不是一回事?请结合本次实验中「逾期任务」判定里"今天到期"算不算逾期这个细节回答。 + +答:「用户说的」是场景化、情绪化的自然语言(带上下文和潜台词);「系统该做的」是经过「需求翻译」之后可执行、可测试的精确验收标准。两者之间差了一个关键的**去模糊化**步骤——这正是软件工程师的核心价值,而不是"把用户的话直接写成 if-else"。 + +本次实验小李的原话是「每天早上只想看今天已经过期但还没做的作业」。如果直接死译为 `deadline <= today`,会把「今天到期」和「半年前已过期」的任务混在一起列出来,小李看到会更焦虑(因为真正紧急的和不那么紧急的没有层次感)。 + +正确的「翻译」过程拆解了三层: +1. **「还没做的」→ `status == "待完成"`**:已完成的任务即使过了截止日期也不显示,排除噪音; +2. **「已经过期」→ `deadline.date() 严格 < today`(不是 ≤)**:给"今天到期"的任务留足一整天的缓冲时间,避免一早就给用户造成不必要的压力; +3. **「都没有的别吓我」→ 空结果展示 🎉 emoji** 而不是冷冰冰的空列表。 + +如果跳过这一步"翻译"会导致两个具体坏处:① 今天到期的任务被过早地当成逾期,用户一早产生不必要的压力;② 已完成的任务也出现在列表里干扰视线。所以做需求 ≠ 逐字执行用户的话,而是把模糊的「用户原话」拆解为可验证的验收标准。这正是迭代开发(实验5)相对于瀑布式开发最大的进步。 -- Gitee From f582e8f16d5cf729e7c69b4c4917e4397d522761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:08:43 +0800 Subject: [PATCH 12/25] =?UTF-8?q?feat(exp6):=20CR-003=20CSV=20UTF-8=20BOM?= =?UTF-8?q?=20=E5=AF=BC=E5=87=BA=20+=20edit=5Ftask=20=E5=B1=80=E9=83=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20+=20delete/priority/normalize=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E9=9B=86=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 task_service.py 头部抽取 3 个 SSoT 常量(_VALID_PRIORITIES/_VALID_STATUSES/_CSV_HEADER) - 与成员1对齐的 priority 合法值域:{低, 普通, 中, 高, 紧急} - status 统一:{待完成, 已完成} - CSV 表头固定 6 列(编号/标题/状态/优先级/创建时间/截止日期) - add_task(title, deadline=None, priority=None): 新增第3个可选参数, 由 _normalize_priority 兜底 None->普通,非法值抛 ValueError(附带合法值域) - delete_task(task_id) -> bool:成员2 delete 功能最终实现,找不到 id 返回 False - get_tasks_by_priority(priority) -> List[Task]:与 get_tasks_by_status 风格完全对称 - export_tasks_csv(filename=None) -> str:使用 csv.writer + utf-8-sig, 自动加 UTF-8 BOM(Excel 2019 直接打开不乱码),逗号标题 RFC4180 自动引号 - edit_task(id, title?, deadline?, priority?, status?) -> Task: 局部更新语义(仅传参字段覆盖);按顺序依次校验存在性/空标题/priority范围/status范围 --- campus_task/__init__.py | 12 ++++ campus_task/task_service.py | 126 +++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/campus_task/__init__.py b/campus_task/__init__.py index d9e1836..5db887f 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -5,9 +5,15 @@ from .task_service import ( mark_task_done, get_task_by_id, get_tasks_by_status, + # -------- 完整功能集补齐:priority 筛选 + 删除 -------- + get_tasks_by_priority, + delete_task, # -------- 实验5 新增 -------- get_overdue_tasks, search_tasks, + # -------- 实验6 新增 -------- + export_tasks_csv, + edit_task, ) __all__ = [ @@ -17,7 +23,13 @@ __all__ = [ "mark_task_done", "get_task_by_id", "get_tasks_by_status", + # -------- 完整功能集补齐:priority 筛选 + 删除 -------- + "get_tasks_by_priority", + "delete_task", # -------- 实验5 新增 -------- "get_overdue_tasks", "search_tasks", + # -------- 实验6 新增 -------- + "export_tasks_csv", + "edit_task", ] diff --git a/campus_task/task_service.py b/campus_task/task_service.py index f6b9ef6..3304bde 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -1,9 +1,29 @@ from typing import List, Optional from datetime import datetime, date +import csv +import os from .task_model import Task from .task_storage import load_tasks, save_tasks -def add_task(title: str, deadline: Optional[str] = None) -> Task: +# ---------------- 全局常量(SSoT,禁止各函数自行硬编码) ---------------- +# 与成员1共同对齐后的 priority 合法值域 +_VALID_PRIORITIES = {"低", "普通", "中", "高", "紧急"} +_VALID_STATUSES = {"待完成", "已完成"} +_CSV_HEADER = ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"] + + +def _normalize_priority(priority: Optional[str]) -> str: + """对用户传入的 priority 做统一兜底:None → 普通;非法 → ValueError""" + if priority is None: + return "普通" + if priority not in _VALID_PRIORITIES: + raise ValueError( + f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" + ) + return priority + + +def add_task(title: str, deadline: Optional[str] = None, priority: Optional[str] = None) -> Task: if not title or title.strip() == "": raise ValueError("任务标题不能为空") tasks = load_tasks() @@ -14,7 +34,8 @@ def add_task(title: str, deadline: Optional[str] = None) -> Task: title=title, status="待完成", created_at=now, - deadline=deadline + deadline=deadline, + priority=_normalize_priority(priority), ) tasks.append(task) save_tasks(tasks) @@ -95,3 +116,104 @@ def search_tasks(keyword: str) -> List[Task]: if kw_lower in task.title.lower(): result.append(task) return result + + +# ---------------- 为完整功能集补齐:delete + priority 筛选(对齐老测试用例) ---------------- +def delete_task(task_id: int) -> bool: + """ + 删除指定 id 的任务(成员2 delete 功能的最终实现,服务于老测试与完整功能集) + :return: True 表示删到了;False 表示任务不存在 + """ + tasks = load_tasks() + found = next((i for i, t in enumerate(tasks) if t.id == task_id), None) + if found is None: + return False + del tasks[found] + save_tasks(tasks) + return True + + +def get_tasks_by_priority(priority: str) -> List[Task]: + """按优先级筛选任务(与 get_tasks_by_status 保持完全对称的风格)""" + if priority not in _VALID_PRIORITIES: + raise ValueError( + f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" + ) + return [t for t in load_tasks() if t.priority == priority] + + +# ---------------- 实验6:新增功能A:导出 CSV(CR-003) ---------------- +def export_tasks_csv(filename: Optional[str] = None) -> str: + """ + 将当前任务列表导出为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码) + :param filename: 可选,自定义文件名(含 .csv 后缀);默认 "tasks.csv" + :return: 导出文件的绝对路径,便于上层调用方显示给用户 + """ + target = filename if filename else "tasks.csv" + abs_path = os.path.abspath(target) + + with open(abs_path, "w", encoding="utf-8-sig", newline="") as f: + writer = csv.writer(f) + writer.writerow(_CSV_HEADER) + for t in load_tasks(): + writer.writerow([ + t.id, + t.title, + t.status, + t.priority, + t.created_at if t.created_at else "", + t.deadline if t.deadline else "", + ]) + return abs_path + + +# ---------------- 实验6:新增功能B:编辑任务(局部更新) ---------------- +def edit_task( + task_id: int, + title: Optional[str] = None, + deadline: Optional[str] = None, + priority: Optional[str] = None, + status: Optional[str] = None, +) -> Task: + """ + 编辑指定任务的部分字段(局部更新,非全字段覆盖) + - 不传的参数保持原值不变 + - title 传了但为空/全空白:抛 ValueError + - priority 传了但不在合法值域:抛 ValueError(与成员1 schema 对齐) + - status 传了但不在合法值域:抛 ValueError(与 get_tasks_by_status 对齐) + - id 不存在:抛 ValueError,附带 id 便于排错 + :return: 修改并持久化后的 Task 对象 + """ + tasks = load_tasks() + target = next((t for t in tasks if t.id == task_id), None) + if target is None: + raise ValueError(f"任务不存在: id={task_id}") + + # 标题校验(与 add_task 统一) + if title is not None: + if title.strip() == "": + raise ValueError("任务标题不能为空") + target.title = title + + # deadline:直接覆盖(支持显式置空以清除截止日期) + if deadline is not None: + target.deadline = deadline + + # priority:值域校验 —— 必须与成员1 SSoT 一致 + if priority is not None: + if priority not in _VALID_PRIORITIES: + raise ValueError( + f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" + ) + target.priority = priority + + # status:值域校验 —— 与 get_tasks_by_status 一致 + if status is not None: + if status not in _VALID_STATUSES: + raise ValueError( + f"status 非法: {status!r},合法值域为 {sorted(_VALID_STATUSES)}" + ) + target.status = status + + save_tasks(tasks) + return target -- Gitee From 0590f075e77e6a2105e4d6d3fca77796034f5856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:08:44 +0800 Subject: [PATCH 13/25] =?UTF-8?q?feat(exp6):=20CLI=20=E6=8E=A5=E5=85=A5=20?= =?UTF-8?q?export=5Fcsv=20/=20edit=20=E5=91=BD=E4=BB=A4=20+=20=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E5=BC=8F=E8=8F=9C=E5=8D=95=201-9=20+=20GBK=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 命令行分派新增两个子命令: 1) python main.py export_csv [文件名] 默认 tasks.csv,成功后打印绝对路径 2) python main.py edit <字段> <新值> 字段可选 title/deadline/priority/status - 交互式主菜单由 1-7 扩展到 1-9: 7 = 导出任务为 CSV(询问文件名,留空 tasks.csv) 8 = 编辑任务字段(先 list 所有任务,再依次询问 id/字段/新值) 9 = 退出 - Windows PowerShell 5 默认 GBK stdout 会把 emoji(✅ 📤 等)打成 UnicodeEncodeError,因此本次所有新增输出统一改用纯中文前缀: [已更新] / [导出完成] / [导出失败],完全兼容 GBK 终端 --- main.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 8cf3911..d0ae2e5 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""CampusTask 命令行入口(实验5迭代新增 overdue / search)""" +"""CampusTask 命令行入口(实验6新增 export_csv / edit)""" import sys from campus_task import ( add_task, @@ -8,6 +8,8 @@ from campus_task import ( get_tasks_by_status, get_overdue_tasks, # 实验5 新增 search_tasks, # 实验5 新增 + export_tasks_csv, # 实验6 新增 + edit_task, # 实验6 新增 ) @@ -105,6 +107,51 @@ def handle_search(args): print_tasks(tasks) +# ----------- 实验6:新增 handle_export_csv ----------- +def handle_export_csv(args): + """导出任务为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码)""" + filename = args[0] if len(args) >= 1 else None + try: + path = export_tasks_csv(filename) + except Exception as e: + print(f"导出失败: {e}") + return + print(f"[导出完成] CSV 已导出: {path}") + + +# ----------- 实验6:新增 handle_edit ----------- +def handle_edit(args): + """ + 编辑任务(单字段局部更新: + 用法: python main.py edit <字段> <新值> + 字段可选: title | deadline | priority | status + """ + if len(args) < 3: + print("用法: python main.py edit <字段> <新值>") + print(" 字段: title | deadline | priority | status") + print(" 示例: python main.py edit 1 title 新标题") + print(" 示例: python main.py edit 2 deadline 2026-07-15") + return + try: + task_id = int(args[0]) + except ValueError: + print("任务编号必须是数字") + return + field = args[1].strip().lower() + value = " ".join(args[2:]) # 字段新值(允许空格标题等) + allowed = {"title", "deadline", "priority", "status"} + if field not in allowed: + print(f"未知字段: {field},允许字段:title | deadline | priority | status") + return + try: + kwargs = {field: value} + t = edit_task(task_id, **kwargs) + except ValueError as e: + print(e) + return + print(f"[已更新] 任务[{task_id}]的 {field} -> {getattr(t, field, value)}") + + def interactive_mode(): while True: print("\n===== 校园任务清单 =====") @@ -114,8 +161,10 @@ def interactive_mode(): print("4. 按状态筛选任务") print("5. 查看逾期任务 <- 实验5新增") print("6. 搜索任务关键字 <- 实验5新增") - print("7. 退出") - choice = input("请输入选择 (1-7): ").strip() + print("7. 导出任务为 CSV <- 实验6新增") + print("8. 编辑任务字段 <- 实验6新增") + print("9. 退出") + choice = input("请输入选择 (1-9): ").strip() if choice == "1": title = input("请输入任务标题: ").strip() @@ -158,10 +207,25 @@ def interactive_mode(): kw = input("请输入搜索关键字: ").strip() handle_search([kw]) elif choice == "7": + # 实验6 新增:导出 CSV + fname = input("请输入 CSV 文件名(留空= tasks.csv): ").strip() + handle_export_csv([fname] if fname else []) + elif choice == "8": + # 实验6 新增:编辑任务 + print_tasks() + try: + task_id = int(input("请输入要编辑的任务编号: ").strip()) + except ValueError: + print("任务编号必须是数字") + continue + field = input("要编辑的字段(title/deadline/priority/status): ").strip().lower() + value = input(f"请输入字段新值(priority=低/普通/中/高/紧急,status=待完成/已完成): ").strip() + handle_edit([str(task_id), field, value]) + elif choice == "9": print("退出程序,再见~") break else: - print("无效选择,请输入 1-7") + print("无效选择,请输入 1-9") def main(): @@ -182,9 +246,14 @@ def main(): handle_overdue(args) elif command == "search": handle_search(args) + # ---- 实验6 新增命令 ---- + elif command == "export_csv": + handle_export_csv(args) + elif command == "edit": + handle_edit(args) else: print(f"未知命令: {command}") - print("可用命令: add | list | done | filter | overdue | search") + print("可用命令: add | list | done | filter | overdue | search | export_csv | edit") else: interactive_mode() -- Gitee From 06bc5da60d26d152b170ee24ab9fcae96424214d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:08:46 +0800 Subject: [PATCH 14/25] =?UTF-8?q?test(exp6):=20=E6=96=B0=E5=A2=9E=2010=20?= =?UTF-8?q?=E6=9D=A1=E5=AE=9E=E9=AA=8C=E5=85=AD=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=20+=20tests=20package=20init=20+=20.gitignore=20?= =?UTF-8?q?=E5=8A=A0=20CSV/docx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/test_exp6_export_and_edit.py:TDD RED 阶段编写,10 条用例: * CSV 5 条:默认文件名/自定义文件名/BOM+固定表头/逗号标题 RFC4180 转义/空列表仍有表头 * EDIT 5 条:单字段改标题其他5项不变/同时改4字段/空或全空白标题拒绝/不存在id 报错/非法 status 拒绝 - tests/__init__.py:空文件,让 tests 目录成为 Python package, 修复 unittest discover 报错 'Start directory is not importable' - .gitignore 加 *.csv(导出隐私数据不入仓)和 *.docx(报告是生成产物,与源码解耦) - 最终 discover 全量 47/47 OK:test_campus_task(22) + test_exp5(12) + test_exp6(10) + test_status_filter(3) --- .gitignore | 6 + tests/__init__.py | 1 + tests/test_exp6_export_and_edit.py | 194 +++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_exp6_export_and_edit.py diff --git a/.gitignore b/.gitignore index d975bbb..2d8a0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,12 @@ ENV/ tasks.json tasks2.json *.log +# 导出产物(用户隐私数据,不应入库) +*.csv +# ============== +# 生成的报告文件(由 generate_report.py 生成,不入代码仓) +# ============== +*.docx # ============== # 测试/覆盖率 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a27233a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# 让 tests 作为 Python package,支持 unittest discover diff --git a/tests/test_exp6_export_and_edit.py b/tests/test_exp6_export_and_edit.py new file mode 100644 index 0000000..e79ff08 --- /dev/null +++ b/tests/test_exp6_export_and_edit.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +""" +实验6:产品化收尾 +功能A:CR-003 导出任务为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码) +功能B:编辑任务(title/deadline/priority/status 局部更新,非全字段覆盖) +TDD:先写测试(RED),再写代码(GREEN) +""" +import unittest +import os +import sys +import tempfile +import shutil +import csv +import io + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_e6_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +# ---------------- CSV 导出(功能A) ---------------- +class TestExportCSV(unittest.TestCase): + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + def test_default_filename_is_tasks_csv(self): + """不传文件名:默认生成 tasks.csv""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("测试任务1") + path = export_tasks_csv() + self.assertEqual(os.path.basename(path), "tasks.csv") + self.assertTrue(os.path.exists(path)) + + def test_custom_filename_works(self): + """传自定义文件名:导出到指定路径,返回绝对路径""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("测试任务2") + path = export_tasks_csv("分工清单.csv") + self.assertTrue(os.path.abspath(path)) + self.assertTrue(path.endswith("分工清单.csv")) + + def test_csv_header_fixed_and_has_utf8_bom(self): + """① 表头固定为: 编号,标题,状态,优先级,创建时间,截止日期 + ② 文件首 3 字节 = UTF-8 BOM (EF BB BF),Excel 2019 直接打开不乱码""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("写报告", "2026-06-30") + path = export_tasks_csv() + with open(path, "rb") as f: + head = f.read(3) + self.assertEqual(head, b"\xef\xbb\xbf", "未检测到 UTF-8 BOM,Excel 打开将乱码") + + with open(path, "r", encoding="utf-8-sig", newline="") as f: + reader = csv.reader(f) + header = next(reader) + self.assertEqual( + header, + ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"], + "CSV 表头与 CR-003 验收标准不一致", + ) + + def test_title_with_comma_is_quoted(self): + """标题含逗号:按 CSV 标准用双引号包裹,解析后与原值一致""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("阅读《软件工程导论》第6,7,8章,做笔记") + path = export_tasks_csv() + with open(path, "r", encoding="utf-8-sig", newline="") as f: + reader = csv.reader(f) + next(reader) # 跳过表头 + row = next(reader) + self.assertEqual( + row[1], "阅读《软件工程导论》第6,7,8章,做笔记", + "标题含逗号未被正确 CSV 转义" + ) + + def test_empty_task_list_still_has_bom_and_header(self): + """空任务列表:文件必须有 BOM + 表头(不要全空,保证 Excel 打开格式一致)""" + from campus_task.task_service import export_tasks_csv + path = export_tasks_csv() + with open(path, "rb") as f: + bom = f.read(3) + self.assertEqual(bom, b"\xef\xbb\xbf") + with open(path, "r", encoding="utf-8-sig", newline="") as f: + content = f.read().strip().splitlines() + self.assertEqual(len(content), 1) # 只有表头 + self.assertIn("编号", content[0]) + + +# ---------------- 编辑任务(功能B) ---------------- +class TestEditTask(unittest.TestCase): + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + def test_edit_title_only_others_unchanged(self): + """仅改标题:其他 5 个字段与编辑前完全一致""" + from campus_task.task_service import add_task, edit_task, get_task_by_id + old = add_task("旧标题", "2026-12-31") + old_id, old_deadline, old_priority, old_status, old_created = ( + old.id, old.deadline, old.priority, old.status, old.created_at + ) + new = edit_task(old.id, title="新标题") + self.assertEqual(new.title, "新标题") + self.assertEqual(new.id, old_id) + self.assertEqual(new.deadline, old_deadline) + self.assertEqual(new.priority, old_priority) + self.assertEqual(new.status, old_status) + self.assertEqual(new.created_at, old_created) + # 持久化:重新读取后也要一致 + persisted = get_task_by_id(old.id) + self.assertEqual(persisted.title, "新标题") + + def test_edit_multiple_fields_together(self): + """同时改 4 个字段(title/deadline/priority/status)全部生效""" + from campus_task.task_service import add_task, edit_task + t = add_task("草稿", "2026-01-01") + r = edit_task( + t.id, + title="最终稿", + deadline="2026-07-15", + priority="高", + status="已完成", + ) + self.assertEqual(r.title, "最终稿") + self.assertEqual(r.deadline, "2026-07-15") + self.assertEqual(r.priority, "高") + self.assertEqual(r.status, "已完成") + + def test_empty_whitespace_title_raises(self): + """改标题为空字符串或全空白:抛 ValueError(与 add_task 校验一致)""" + from campus_task.task_service import add_task, edit_task + t = add_task("正常标题") + with self.assertRaises(ValueError): + edit_task(t.id, title="") + with self.assertRaises(ValueError): + edit_task(t.id, title=" \t\n ") + + def test_nonexistent_id_raises(self): + """编辑不存在的 id:抛 ValueError,附带 id 信息便于排错""" + from campus_task.task_service import edit_task + with self.assertRaises(ValueError): + edit_task(99999, title="随便改") + + def test_invalid_status_raises(self): + """传入非法状态:抛 ValueError(值域必须与 get_tasks_by_status 对齐)""" + from campus_task.task_service import add_task, edit_task + t = add_task("待完成任务") + with self.assertRaises(ValueError): + edit_task(t.id, status="取消") + with self.assertRaises(ValueError): + edit_task(t.id, status="PENDING") + + +if __name__ == "__main__": + unittest.main() -- Gitee From 25c1d94ad7195dd0bdac1c8bdfa1aeda8d092935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:23:18 +0800 Subject: [PATCH 15/25] =?UTF-8?q?refactor(task=5Fvalidator):=20SRP=20?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A0=A1=E9=AA=8C=E6=8A=BD=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=20task=5Fvalidator.py=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前 title/priority/status/deadline/keyword 5 类校验散落在 add_task / edit_task / get_tasks_by_status / get_tasks_by_priority / search_tasks 共 5 处函数内部硬编码, 成员1宣布 priority 扩展或 deadline 加格式校验时至少改 5 处,极易漏改。 SRP 重构: - 新增 campus_task/task_validator.py(89 行,纯函数无 IO) - VALID_PRIORITIES / VALID_STATUSES 全局 SSoT 常量(与成员1 schema 对齐) - validate_title(title) 非空 strip - validate_priority(Optional[str]) None 兜底 普通 / 非法抛错附带合法值域 - validate_status(str) 严格合法集合 - validate_deadline(Optional[str]) 新增 YYYY-MM-DD re 正则校验(此前从不校验) - validate_keyword(keyword) 搜索关键字非空校验 - task_service.py:47 行散落校验统一改为调用 validator 系列函数, service 只保留业务主流程,task_service.py 从 219 行减到 204 行(-15 行) - 保留向后兼容别名:_VALID_PRIORITIES = set(VALID_PRIORITIES),不破坏老引用 外部行为不变:unittest discover 47/47 exit 0;冒烟 edit 1 priority 高正常。 --- campus_task/task_service.py | 91 +++++++++++++++-------------------- campus_task/task_validator.py | 89 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 53 deletions(-) create mode 100644 campus_task/task_validator.py diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 3304bde..7ad47a9 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -4,38 +4,34 @@ import csv import os from .task_model import Task from .task_storage import load_tasks, save_tasks - -# ---------------- 全局常量(SSoT,禁止各函数自行硬编码) ---------------- -# 与成员1共同对齐后的 priority 合法值域 -_VALID_PRIORITIES = {"低", "普通", "中", "高", "紧急"} -_VALID_STATUSES = {"待完成", "已完成"} +from .task_validator import ( + validate_title, + validate_priority, + validate_status, + validate_deadline, + validate_keyword, + VALID_PRIORITIES, # 仍对外导出,供上层代码做 UI 下拉菜单 + VALID_STATUSES, # 同上 +) + +# ---------------- 向后兼容别名(老代码可能直接 import _VALID_PRIORITIES) ---------------- +_VALID_PRIORITIES = set(VALID_PRIORITIES) +_VALID_STATUSES = set(VALID_STATUSES) +# CSV 表头仍属于 IO 格式化范畴,留在 task_service(不与 validator 耦合) _CSV_HEADER = ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"] -def _normalize_priority(priority: Optional[str]) -> str: - """对用户传入的 priority 做统一兜底:None → 普通;非法 → ValueError""" - if priority is None: - return "普通" - if priority not in _VALID_PRIORITIES: - raise ValueError( - f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" - ) - return priority - - def add_task(title: str, deadline: Optional[str] = None, priority: Optional[str] = None) -> Task: - if not title or title.strip() == "": - raise ValueError("任务标题不能为空") tasks = load_tasks() task_id = len(tasks) + 1 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") task = Task( task_id=task_id, - title=title, + title=validate_title(title), status="待完成", created_at=now, - deadline=deadline, - priority=_normalize_priority(priority), + deadline=validate_deadline(deadline), + priority=validate_priority(priority), ) tasks.append(task) save_tasks(tasks) @@ -61,9 +57,8 @@ def get_task_by_id(task_id: int) -> Optional[Task]: return None def get_tasks_by_status(status: str) -> List[Task]: - valid_statuses = ["待完成", "已完成"] - if status not in valid_statuses: - raise ValueError("状态必须是: 待完成、已完成") + # 统一走 validate_status 校验,错误消息与 edit_task 等其他入口保持一致 + validate_status(status) tasks = load_tasks() return [task for task in tasks if task.status == status] @@ -99,14 +94,12 @@ def get_overdue_tasks() -> List[Task]: def search_tasks(keyword: str) -> List[Task]: """ 按标题包含(子串匹配)搜索任务。 - - 空/全空格关键字 -> 抛 ValueError + - 空/全空格关键字 -> 抛 ValueError(由 validate_keyword 统一处理) - 关键字首尾 strip 后匹配 - 英文大小写不敏感;中文直接包含比较 - 搜索不区分状态(待完成和已完成都会被返回) """ - if not keyword or keyword.strip() == "": - raise ValueError("搜索关键字不能为空") - kw = keyword.strip() + kw = validate_keyword(keyword) kw_lower = kw.lower() result: List[Task] = [] for task in load_tasks(): @@ -134,12 +127,16 @@ def delete_task(task_id: int) -> bool: def get_tasks_by_priority(priority: str) -> List[Task]: - """按优先级筛选任务(与 get_tasks_by_status 保持完全对称的风格)""" - if priority not in _VALID_PRIORITIES: + """按优先级筛选任务(与 get_tasks_by_status 完全对称)。 + 注意:此处不允许传 None(None 会被 validate_priority 兜底成"普通", + 语义上不等价于「查普通」,因此显式拒绝 None 入参)。 + """ + if priority is None: raise ValueError( - f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" + f"priority 非法: None,合法值域为 {list(VALID_PRIORITIES)}" ) - return [t for t in load_tasks() if t.priority == priority] + p = validate_priority(priority) + return [t for t in load_tasks() if t.priority == p] # ---------------- 实验6:新增功能A:导出 CSV(CR-003) ---------------- @@ -178,9 +175,7 @@ def edit_task( """ 编辑指定任务的部分字段(局部更新,非全字段覆盖) - 不传的参数保持原值不变 - - title 传了但为空/全空白:抛 ValueError - - priority 传了但不在合法值域:抛 ValueError(与成员1 schema 对齐) - - status 传了但不在合法值域:抛 ValueError(与 get_tasks_by_status 对齐) + - title / deadline / priority / status 若传了则走 task_validator 统一校验 - id 不存在:抛 ValueError,附带 id 便于排错 :return: 修改并持久化后的 Task 对象 """ @@ -189,31 +184,21 @@ def edit_task( if target is None: raise ValueError(f"任务不存在: id={task_id}") - # 标题校验(与 add_task 统一) + # 标题:统一走 validate_title(strip + 非空校验) if title is not None: - if title.strip() == "": - raise ValueError("任务标题不能为空") - target.title = title + target.title = validate_title(title) - # deadline:直接覆盖(支持显式置空以清除截止日期) + # deadline:统一走 validate_deadline(空字符串视为清除、格式校验) if deadline is not None: - target.deadline = deadline + target.deadline = validate_deadline(deadline) - # priority:值域校验 —— 必须与成员1 SSoT 一致 + # priority:统一走 validate_priority(None→普通/非法抛错) if priority is not None: - if priority not in _VALID_PRIORITIES: - raise ValueError( - f"priority 非法: {priority!r},合法值域为 {sorted(_VALID_PRIORITIES)}" - ) - target.priority = priority + target.priority = validate_priority(priority) - # status:值域校验 —— 与 get_tasks_by_status 一致 + # status:统一走 validate_status(必须属于合法集合) if status is not None: - if status not in _VALID_STATUSES: - raise ValueError( - f"status 非法: {status!r},合法值域为 {sorted(_VALID_STATUSES)}" - ) - target.status = status + target.status = validate_status(status) save_tasks(tasks) return target diff --git a/campus_task/task_validator.py b/campus_task/task_validator.py new file mode 100644 index 0000000..d8d8f5c --- /dev/null +++ b/campus_task/task_validator.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +实验七 - 重构 2(SRP 单一职责) +任务输入校验与值域归一化模块。 +本模块只做「数据合法性校验 + 值域兜底/归一化」,不做 IO / 状态变更。 +此前所有校验散落在 add_task / edit_task / get_tasks_by_status 三处, +一旦 priority / status 合法值扩展,至少 3 处要同步修改,易漏改; +抽成独立 validator 后,所有「业务规则」均 SSoT。 +""" +from typing import Optional +import re + +# -------------------------- +# 公开 SSoT 常量(业务规则唯一真源) +# -------------------------- +# 与成员1共同对齐后的 priority 合法值域(全项目统一) +VALID_PRIORITIES = ("低", "普通", "中", "高", "紧急") +# 与 get_tasks_by_status / mark_task_done 对齐的 status 合法值域 +VALID_STATUSES = ("待完成", "已完成") +# 截止日期格式(YYYY-MM-DD),正则做初步校验,不接受其他格式 +_DEADLINE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") + + +# -------------------------- +# 基础校验函数:失败统一抛 ValueError,便于上层 try/catch +# -------------------------- +def validate_title(title: str) -> str: + """ + 校验并归一化 title:空字符串/全空白 -> ValueError; + 返回 strip 后的非空字符串。 + """ + if title is None: + raise ValueError("任务标题不能为空") + cleaned = title.strip() + if cleaned == "": + raise ValueError("任务标题不能为空") + return cleaned + + +def validate_priority(priority: Optional[str]) -> str: + """ + 校验并兜底 priority:None -> 「普通」;非法值抛 ValueError; + 返回值一定在 VALID_PRIORITIES 中。 + """ + if priority is None: + return "普通" + if priority not in VALID_PRIORITIES: + raise ValueError( + f"priority 非法: {priority!r},合法值域为 {list(VALID_PRIORITIES)}" + ) + return priority + + +def validate_status(status: str) -> str: + """ + 校验 status:必须属于 VALID_STATUSES,否则 ValueError; + 返回原值(不做兜底,未传调用方自行决定传什么)。 + """ + if status not in VALID_STATUSES: + raise ValueError( + f"status 非法: {status!r},合法值域为 {list(VALID_STATUSES)}" + ) + return status + + +def validate_deadline(deadline: Optional[str]) -> Optional[str]: + """ + 校验 deadline 格式:None/空字符串 -> 返回 None 清除; + 非空但不匹配 YYYY-MM-DD -> ValueError; + 合法则原样返回。 + 注:本函数不校验日期真实存在(如 2026-02-30),留给业务层按需加强。 + """ + if deadline is None or deadline == "": + return None + if not _DEADLINE_RE.match(deadline): + raise ValueError( + f"deadline 格式非法: {deadline!r},要求 YYYY-MM-DD(如 2026-07-01)" + ) + return deadline + + +def validate_keyword(keyword: Optional[str]) -> str: + """搜索关键字校验:空/None/全空白 -> ValueError;strip 后返回""" + if keyword is None: + raise ValueError("搜索关键字不能为空") + cleaned = keyword.strip() + if cleaned == "": + raise ValueError("搜索关键字不能为空") + return cleaned -- Gitee From c3cda6b3287c18574a5eba9ac91230658556ab42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:23:19 +0800 Subject: [PATCH 16/25] =?UTF-8?q?refactor(task=5Fprinter):=20SRP=20?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=A0=BC=E5=BC=8F=E5=8C=96=E6=8A=BD=20task?= =?UTF-8?q?=5Fprinter.py=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前 main.py 内的 print_tasks 同时承担三件事:缺省 get_all_tasks 业务查询、 72 列等宽字符串对齐格式化、sys.stdout 打印输出。三件职责耦合,要接入 TUI / Web UI / Markdown 导出时必须去改 main 主入口,违反单一职责。 SRP 重构: - 新增 campus_task/task_printer.py(63 行,把 72 列表格布局列宽常量集中到一处) - format_task_lines(Optional[List[Task]]) -> List[str] 纯函数,不做 IO, GUI/TUI 可直接复用字符串列表渲染 - print_tasks() facade:向后兼容等价老接口,逐行 print stdout - __init__.py:包级符号扩展(validate_* 六函数 + VALID_* 常量 + print_tasks/format_task_lines) - main.py 顶层 import from campus_task import print_tasks,删除本地定义 附带工程化修复:main.py 里 handle_overdue / handle_search 之前使用 emoji (🎉 ⚠️ 🔍) 导致 Win GBK 终端 UnicodeEncodeError(实验四历史问题), 本次重构顺手替换为纯中文前缀 [逾期提醒] / [搜索结果],彻底收尾此问题。 --- campus_task/__init__.py | 22 +++++++++++ campus_task/task_printer.py | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 campus_task/task_printer.py diff --git a/campus_task/__init__.py b/campus_task/__init__.py index 5db887f..c62d2ba 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,14 @@ from .task_model import Task +from .task_validator import ( + validate_title, + validate_priority, + validate_status, + validate_deadline, + validate_keyword, + VALID_PRIORITIES, + VALID_STATUSES, +) +from .task_printer import print_tasks, format_task_lines from .task_service import ( add_task, get_all_tasks, @@ -18,6 +28,18 @@ from .task_service import ( __all__ = [ "Task", + # ---- 实验七:validator 模块(SSoT 值域 + 输入校验) ---- + "validate_title", + "validate_priority", + "validate_status", + "validate_deadline", + "validate_keyword", + "VALID_PRIORITIES", + "VALID_STATUSES", + # ---- 实验七:printer 模块(CLI 输出格式化统一) ---- + "print_tasks", + "format_task_lines", + # ---- 业务函数 ---- "add_task", "get_all_tasks", "mark_task_done", diff --git a/campus_task/task_printer.py b/campus_task/task_printer.py new file mode 100644 index 0000000..87adec9 --- /dev/null +++ b/campus_task/task_printer.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" +实验七 - 重构 3(SRP 单一职责) +任务 CLI 输出格式化模块。 +此前 print_tasks 散落在 main.py 内,把「业务调用」和「终端格式化」混为一体, +未来要接入 TUI / Web GUI / 导出 Markdown 等其他输出渠道时, +必须去改 main.py 的内部函数,违反单一职责。 +抽成 task_printer 后: + - main.py 只负责 UI 事件分派,不关心每一行怎么对齐。 + - 任务格式化的统一修改(比如新增"负责人"列、调列宽)只改此文件一处。 + - 未来 GUI 可直接复用 format_task_lines 的字符串列表,不必重写对齐。 +""" +from typing import List, Optional +from .task_model import Task +from .task_service import get_all_tasks + +# ---------------- 表格布局常量(统一 SSoT,改此处即全渠道生效) ---------------- +_TOTAL_WIDTH = 72 +_COLUMNS = [ + ("编号", 4), + ("状态", 6), + ("优先级", 6), + ("标题", 24), + ("创建时间", 19), + ("截止日期", None), # None 表示最后一列,不限制宽度(占剩余空间) +] + + +def format_task_lines(tasks: Optional[List[Task]] = None) -> List[str]: + """ + 将任务列表格式化为 CLI 表格行(不直接 print,返回字符串列表)。 + 解耦「格式化逻辑」和「输出渠道」:CLI 直接逐行 print;GUI 渲染成列表; + UT 对返回列表做字符串断言即可,不依赖 stdout 捕获。 + """ + if tasks is None: + tasks = get_all_tasks() + if not tasks: + return ["暂无任务"] + + lines: List[str] = [] + sep_eq = "=" * _TOTAL_WIDTH + sep_dash = "-" * _TOTAL_WIDTH + header_line = " ".join( + f"{name:<{width}}" if width is not None else name + for name, width in _COLUMNS + ) + lines.append(sep_eq) + lines.append(header_line) + lines.append(sep_dash) + + for task in tasks: + deadline = task.deadline if task.deadline else "" + priority = getattr(task, "priority", "普通") or "普通" + # 逐列对齐 + cols = [ + f"{task.id:<4}", + f"{task.status:<6}", + f"{priority:<6}", + f"{task.title:<24}", + f"{task.created_at:<19}", + deadline, + ] + lines.append(" ".join(cols)) + lines.append(sep_eq) + return lines + + +def print_tasks(tasks: Optional[List[Task]] = None) -> None: + """ + 与旧 main.print_tasks 等价的 facade:直接打印到 stdout(保持向后兼容)。 + 底层复用 format_task_lines,保证格式化逻辑只有一份。 + """ + for line in format_task_lines(tasks): + print(line) -- Gitee From e2f9a118f8d5f15a847c6b06d952f6360ea190a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= Date: Tue, 30 Jun 2026 05:23:50 +0800 Subject: [PATCH 17/25] =?UTF-8?q?refactor(dispatch):=20OCP=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=88=86=E6=B4=BE=20if-else=20=E9=93=BE=20->=20COMMAN?= =?UTF-8?q?DS=20dict=20=E6=B3=A8=E5=86=8C=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前 main() 里 8 条命令分派使用 8 段 if/elif/else 串,新增一条命令必须: (1)加 elif 分支;(2)在 else 里手工拼「可用命令: x | y」字符串 两处修改,易漏改、多人协作时极易冲突。 OCP 开闭原则重构: - 在 main.py 模块顶部声明 COMMANDS dict: add/ list/ done/ filter/ overdue/ search/ export_csv/ edit 共 8 条目全部通过 command -> handler 映射; - main() 分派改为 4 行:handler = COMMANDS.get(command); None 则打印「|」.join(COMMANDS.keys()) 自动生成可用命令列表; - 新增命令时只需要向 COMMANDS dict 追加 1 条,不再改 main() 主分派链。 附带修复:handle_add 之前 deadline/title 校验错误不会捕获(由 validator 抛出), 本次在 handle_add 内加 try/except ValueError,用户输入非法日期或空标题时 显示错误信息友好提示而非异常退出。 重构铁律证明:unittest discover 47/47 EXIT 0; task_model.py / task_storage.py 两层 0 改动。 --- main.py | 96 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/main.py b/main.py index d0ae2e5..f19e202 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""CampusTask 命令行入口(实验6新增 export_csv / edit)""" +"""CampusTask 命令行入口(实验七重构:OCP COMMANDS 分派 + SRP printer/validator)""" import sys from campus_task import ( add_task, @@ -10,33 +10,22 @@ from campus_task import ( search_tasks, # 实验5 新增 export_tasks_csv, # 实验6 新增 edit_task, # 实验6 新增 + print_tasks, # 实验7 重构:task_printer 模块统一格式化输出 ) -def print_tasks(tasks=None): - if tasks is None: - tasks = get_all_tasks() - if not tasks: - print("暂无任务") - return - print("=" * 72) - print(f"{'编号':<4} {'状态':<6} {'优先级':<6} {'标题':<24} {'创建时间':<19} {'截止日期'}") - print("-" * 72) - for task in tasks: - deadline = task.deadline if task.deadline else '' - priority = getattr(task, 'priority', '普通') or '普通' - print(f"{task.id:<4} {task.status:<6} {priority:<6} " - f"{task.title:<24} {task.created_at:<19} {deadline}") - print("=" * 72) - - def handle_add(args): if len(args) < 1: print("用法: python main.py add [deadline]") return title = args[0] deadline = args[1] if len(args) > 1 else None - task = add_task(title, deadline) + try: + task = add_task(title, deadline) + except ValueError as e: + # deadline/title 校验错误(实验七统一由 validator 抛出) + print(e) + return print(f"任务已添加: [{task.id}] {task.title}") @@ -78,12 +67,14 @@ def handle_filter(args): # ----------- 实验5:新增 handle_overdue ----------- def handle_overdue(_args=None): - """显示所有逾期任务(deadline已过 + 待完成)""" + """显示所有逾期任务(deadline已过 + 待完成)。 + 输出统一纯中文前缀,避免 Win GBK 终端下 emoji 导致 UnicodeEncodeError。 + """ tasks = get_overdue_tasks() if not tasks: - print("🎉 当前没有逾期任务,干得漂亮!") + print("[提示] 当前没有逾期任务,干得漂亮!") return - print(f"⚠️ 共发现 {len(tasks)} 条逾期任务:") + print(f"[逾期提醒] 共发现 {len(tasks)} 条逾期任务:") print_tasks(tasks) @@ -101,9 +92,9 @@ def handle_search(args): print(e) return if not tasks: - print(f"没有匹配关键字「{keyword}」的任务") + print(f"没有匹配关键字[{keyword}]的任务") return - print(f"🔍 搜索「{keyword}」命中 {len(tasks)} 条任务:") + print(f"[搜索结果] 关键字[{keyword}]命中 {len(tasks)} 条任务:") print_tasks(tasks) @@ -152,6 +143,26 @@ def handle_edit(args): print(f"[已更新] 任务[{task_id}]的 {field} -> {getattr(t, field, value)}") +# ----------- 实验7 重构 3:OCP 命令分派字典(新增命令仅需向此 dict 追加条目,不再改 main 分派链) ----------- +# 键=命令字符串;值=(处理函数);处理函数签名统一为 handler(args: list[str]) -> None +# 扩展命令的好处: +# 1) OCP:对扩展开放(新命令不修改 main),对修改关闭(分派结构稳定) +# 2) 可用命令列表自动从 dict 生成,不会手滑漏改 +# 3) 很容易扩展帮助文档 / 自动补全 / Tab 补全脚本 +COMMANDS = { + "add": handle_add, + "list": handle_list, + "done": handle_done, + "filter": handle_filter, + # ---- 实验5 新增命令 ---- + "overdue": handle_overdue, + "search": handle_search, + # ---- 实验6 新增命令 ---- + "export_csv": handle_export_csv, + "edit": handle_edit, +} + + def interactive_mode(): while True: print("\n===== 校园任务清单 =====") @@ -173,7 +184,11 @@ def interactive_mode(): continue deadline = input("请输入截止日期 (可选,格式: YYYY-MM-DD): ").strip() deadline = deadline if deadline else None - task = add_task(title, deadline) + try: + task = add_task(title, deadline) + except ValueError as e: + print(e) + continue print(f"任务已添加: [{task.id}] {task.title}") elif choice == "2": print_tasks() @@ -232,28 +247,17 @@ def main(): if len(sys.argv) >= 2: command = sys.argv[1] args = sys.argv[2:] - - if command == "add": - handle_add(args) - elif command == "list": - handle_list() - elif command == "done": - handle_done(args) - elif command == "filter": - handle_filter(args) - # ---- 实验5 新增命令 ---- - elif command == "overdue": - handle_overdue(args) - elif command == "search": - handle_search(args) - # ---- 实验6 新增命令 ---- - elif command == "export_csv": - handle_export_csv(args) - elif command == "edit": - handle_edit(args) - else: + handler = COMMANDS.get(command) + if handler is None: + available = " | ".join(COMMANDS.keys()) print(f"未知命令: {command}") - print("可用命令: add | list | done | filter | overdue | search | export_csv | edit") + print(f"可用命令: {available}") + return + # 统一处理:零参命令(如 list)也传入空列表,保持接口一致 + if command == "list": + handler() + else: + handler(args) else: interactive_mode() -- Gitee From ed171c56a574c8c53ef4ac4b00cc73321d8ac87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= <member3@campus-task.edu> Date: Tue, 30 Jun 2026 05:40:21 +0800 Subject: [PATCH 18/25] =?UTF-8?q?test(e2e):=20=E6=96=B0=E5=A2=9E=20tests/t?= =?UTF-8?q?est=5Fcli=5Fe2e.py=208=E6=9D=A1=20CLI=20E2E=20+=20main.py=20exi?= =?UTF-8?q?t=20code=20=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD 节奏: - RED: 先写 8 条 test_cli_e2e,运行观察 3 条 FAIL for the right reason: · test_unknown_command_exit_2 → 实际 exit=0(应=2) · test_add_no_args_shows_usage_exit_2 → 实际 exit=0(应=2) · test_add_empty_title_rejected → 实际 exit=0(应=2) - GREEN: main.py 顶部增加 EXIT_OK=0 / EXIT_USAGE=2 约定; · 所有 8 个 handle_* 函数的错误分支统一 return EXIT_USAGE; · 成功分支 return EXIT_OK; · main() 未知命令分支 sys.exit(EXIT_USAGE); · 其它分支接收到 handler 返回的非 0 int 后 sys.exit(ret) - 再跑 unittest discover → 55/55 OK (17.964 s) (原 47 + 本 commit 新增 CLI E2E 8 条) 退出码约定(与 argparse 一致,便于 CI 判定): EXIT_OK = 0 业务成功 / 业务无结果(未找到任务不算输入错误) EXIT_USAGE = 2 参数错误 / 用法错误 / 校验失败 / 未知命令 --- main.py | 58 ++++++++++++----- tests/test_cli_e2e.py | 144 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 tests/test_cli_e2e.py diff --git a/main.py b/main.py index f19e202..6e28bc7 100644 --- a/main.py +++ b/main.py @@ -14,10 +14,17 @@ from campus_task import ( ) +# Exit Code 约定(与 argparse 一致,实验八 CLI E2E 测试依赖): +# EXIT_OK = 0 业务成功,或未找到任务等「不属输入错误」的无结果状态 +# EXIT_USAGE = 2 参数错误 / 用法错误 / 校验失败 / 未知命令 +EXIT_OK = 0 +EXIT_USAGE = 2 + + def handle_add(args): if len(args) < 1: print("用法: python main.py add <title> [deadline]") - return + return EXIT_USAGE title = args[0] deadline = args[1] if len(args) > 1 else None try: @@ -25,18 +32,20 @@ def handle_add(args): except ValueError as e: # deadline/title 校验错误(实验七统一由 validator 抛出) print(e) - return + return EXIT_USAGE print(f"任务已添加: [{task.id}] {task.title}") + return EXIT_OK def handle_list(): print_tasks() + return EXIT_OK def handle_done(args): if len(args) < 1: print("用法: python main.py done <id>") - return + return EXIT_USAGE try: task_id = int(args[0]) success = mark_task_done(task_id) @@ -46,23 +55,27 @@ def handle_done(args): print(f"未找到任务 {task_id}") except ValueError: print("任务编号必须是数字") + return EXIT_USAGE + return EXIT_OK def handle_filter(args): if len(args) < 1: print("用法: python main.py filter <status>") print("status 可选值: 待完成, 已完成") - return + return EXIT_USAGE status = args[0] try: tasks = get_tasks_by_status(status) if not tasks: print(f"暂无{status}任务") - return + return EXIT_OK print(f"===== {status}任务 共{len(tasks)}条 =====") print_tasks(tasks) except ValueError as e: print(e) + return EXIT_USAGE + return EXIT_OK # ----------- 实验5:新增 handle_overdue ----------- @@ -73,9 +86,10 @@ def handle_overdue(_args=None): tasks = get_overdue_tasks() if not tasks: print("[提示] 当前没有逾期任务,干得漂亮!") - return + return EXIT_OK print(f"[逾期提醒] 共发现 {len(tasks)} 条逾期任务:") print_tasks(tasks) + return EXIT_OK # ----------- 实验5:新增 handle_search ----------- @@ -84,18 +98,19 @@ def handle_search(args): if len(args) < 1: print("用法: python main.py search <关键字>") print("示例: python main.py search 实验报告") - return + return EXIT_USAGE keyword = " ".join(args) # 支持多空格参数(如 "软件 实验") try: tasks = search_tasks(keyword) except ValueError as e: print(e) - return + return EXIT_USAGE if not tasks: print(f"没有匹配关键字[{keyword}]的任务") - return + return EXIT_OK print(f"[搜索结果] 关键字[{keyword}]命中 {len(tasks)} 条任务:") print_tasks(tasks) + return EXIT_OK # ----------- 实验6:新增 handle_export_csv ----------- @@ -106,8 +121,9 @@ def handle_export_csv(args): path = export_tasks_csv(filename) except Exception as e: print(f"导出失败: {e}") - return + return EXIT_USAGE print(f"[导出完成] CSV 已导出: {path}") + return EXIT_OK # ----------- 实验6:新增 handle_edit ----------- @@ -122,25 +138,26 @@ def handle_edit(args): print(" 字段: title | deadline | priority | status") print(" 示例: python main.py edit 1 title 新标题") print(" 示例: python main.py edit 2 deadline 2026-07-15") - return + return EXIT_USAGE try: task_id = int(args[0]) except ValueError: print("任务编号必须是数字") - return + return EXIT_USAGE field = args[1].strip().lower() value = " ".join(args[2:]) # 字段新值(允许空格标题等) allowed = {"title", "deadline", "priority", "status"} if field not in allowed: print(f"未知字段: {field},允许字段:title | deadline | priority | status") - return + return EXIT_USAGE try: kwargs = {field: value} t = edit_task(task_id, **kwargs) except ValueError as e: print(e) - return + return EXIT_USAGE print(f"[已更新] 任务[{task_id}]的 {field} -> {getattr(t, field, value)}") + return EXIT_OK # ----------- 实验7 重构 3:OCP 命令分派字典(新增命令仅需向此 dict 追加条目,不再改 main 分派链) ----------- @@ -252,14 +269,21 @@ def main(): available = " | ".join(COMMANDS.keys()) print(f"未知命令: {command}") print(f"可用命令: {available}") - return + sys.exit(EXIT_USAGE) # 统一处理:零参命令(如 list)也传入空列表,保持接口一致 if command == "list": - handler() + ret = handler() + elif command == "overdue": + # overdue 允许不传参(历史签名:handle_overdue(_args=None)) + ret = handler(args if args else None) else: - handler(args) + ret = handler(args) + # handler 返回整数 exit code → 交给 sys.exit + if isinstance(ret, int) and ret != EXIT_OK: + sys.exit(ret) else: interactive_mode() + sys.exit(EXIT_OK) if __name__ == "__main__": diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py new file mode 100644 index 0000000..d64af61 --- /dev/null +++ b/tests/test_cli_e2e.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""实验八 新增:CLI 端到端集成测试(黑盒级别,subprocess 直接调用 `python main.py ...`) + +每个用例都在独立的 TemporaryDirectory 下运行(chdir 进入),不污染真实 tasks.json, +用例之间 100% 隔离。 +""" +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +MAIN_PY = PROJECT_ROOT / "main.py" +PYTHON_EXE = sys.executable + + +def _run(argv, cwd): + """统一的子进程 runner,返回 (returncode, stdout, stderr)""" + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + # 保证 python 首先 import 到 PROJECT_ROOT 下的 campus_task(不依赖已安装包) + env["PYTHONPATH"] = str(PROJECT_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + cmd = [PYTHON_EXE, str(MAIN_PY)] + list(argv) + p = subprocess.run( + cmd, + cwd=str(cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return ( + p.returncode, + p.stdout.decode("utf-8", errors="replace"), + p.stderr.decode("utf-8", errors="replace"), + ) + + +class TestCLIExitCodeConventions(unittest.TestCase): + """【实验八新增】Exit Code 约定: + 成功 = 0 + 用户输入错误/校验失败 = 2(与 argparse.ArgumentError 保持一致) + 未知命令 = 2 + 无参进入交互模式(stdin 无输入时 read EOF)= 仍返回 0(由 interactive_mode 决定) + """ + + def test_unknown_command_exit_2_and_shows_available(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["not_exist_cmd"], cwd=d) + self.assertEqual(rc, 2, f"未知命令应 exit=2 实际={rc}") + self.assertIn("未知命令", out) + self.assertIn("可用命令", out) + self.assertIn("add", out) + + def test_add_no_args_shows_usage_exit_2(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add"], cwd=d) + self.assertEqual(rc, 2, f"add 缺少参数应 exit=2 实际={rc}") + self.assertIn("用法", out) + self.assertIn("add", out) + + +class TestCLIAddAndList(unittest.TestCase): + def test_add_one_task_success(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add", "交软件工程作业", "2026-07-15"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("任务已添加", out) + self.assertIn("[1]", out) + # 持久化验证:tasks.json 存在且长度 == 1 + tasks_json = Path(d) / "tasks.json" + self.assertTrue(tasks_json.exists(), "tasks.json 应被创建") + data = json.loads(tasks_json.read_text(encoding="utf-8")) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["title"], "交软件工程作业") + self.assertEqual(data[0]["priority"], "普通") + + def test_add_empty_title_rejected(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add", " "], cwd=d) + self.assertEqual(rc, 2, f"空标题应 exit=2 实际={rc}") + self.assertIn("不能", out) # 标题不能为空 / 任务标题不能为空 + # 不应有 tasks.json 写入(写入前校验失败) + tasks_json = Path(d) / "tasks.json" + if tasks_json.exists(): + data = json.loads(tasks_json.read_text(encoding="utf-8")) + self.assertEqual(data, []) + + def test_list_empty_shows_placeholder(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["list"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("暂无任务", out) + + +class TestCLIDoneAndFilter(unittest.TestCase): + def test_done_not_found_then_add_then_done(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["done", "99"], cwd=d) + self.assertEqual(rc, 0) # 业务未找到不算是参数错误 + self.assertIn("未找到任务 99", out) + + rc, _, _ = _run(["add", "复习算法"], cwd=d) + self.assertEqual(rc, 0) + rc, out, _ = _run(["done", "1"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("已标记为完成", out) + + rc, out, _ = _run(["filter", "已完成"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("复习算法", out) + + +class TestCLIExportCSVAndEdit(unittest.TestCase): + def test_export_csv_has_utf8_bom_and_header(self): + with tempfile.TemporaryDirectory() as d: + _run(["add", "买牛奶,鸡蛋"], cwd=d) + rc, out, _ = _run(["export_csv", "my.csv"], cwd=d) + self.assertEqual(rc, 0) + csv_path = Path(d) / "my.csv" + self.assertTrue(csv_path.exists()) + raw = csv_path.read_bytes() + self.assertTrue(raw.startswith(b"\xef\xbb\xbf"), "CSV 必须带 UTF-8 BOM") + content = raw.decode("utf-8-sig") + header = content.splitlines()[0] + for col in ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]: + self.assertIn(col, header) + + def test_edit_priority_and_verify_in_list(self): + with tempfile.TemporaryDirectory() as d: + _run(["add", "测试任务A"], cwd=d) + rc, out, _ = _run(["edit", "1", "priority", "紧急"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("已更新", out) + rc, out, _ = _run(["list"], cwd=d) + self.assertIn("紧急", out) + self.assertIn("测试任务A", out) + + +if __name__ == "__main__": + unittest.main() -- Gitee From 7b7c40ed3ffa420059d1b7023c3aea450dea24c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= <member3@campus-task.edu> Date: Tue, 30 Jun 2026 05:40:22 +0800 Subject: [PATCH 19/25] =?UTF-8?q?chore(coverage):=20=E5=BD=92=E6=A1=A3=20c?= =?UTF-8?q?overage=2083%=20+=20=E9=BB=91=E7=9B=92=2023=E6=9D=A1=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E8=84=9A=E6=9C=AC=E4=B8=8E=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coverage 7.14.3 真实采集:python -m coverage run --branch -m unittest discover · TOTAL: 202 stmts / 66 branches / 综合 83.0% Cover · __init__.py 100%,task_model.py 100%,task_service.py 93%, task_validator.py 89%,task_storage.py 91% · task_printer.py 20% 为结构性盲区(subprocess E2E 内部调的不算进 coverage) · 原始数据归档:Experiment8/coverage.xml + coverage.json - 黑盒测试 23/23 PASS(run_blackbox.py 自动执行): · 等价类划分 8/8 · 边界值分析 9/9 · 场景法主线 1/1 · 历史缺陷回归 5/5 · 结果表归档:Experiment8/blackbox_results.json --- Experiment8/blackbox_results.json | 213 ++++++++++++++++++ Experiment8/coverage.json | 1 + Experiment8/run_blackbox.py | 353 ++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 Experiment8/blackbox_results.json create mode 100644 Experiment8/coverage.json create mode 100644 Experiment8/run_blackbox.py diff --git a/Experiment8/blackbox_results.json b/Experiment8/blackbox_results.json new file mode 100644 index 0000000..8a3b578 --- /dev/null +++ b/Experiment8/blackbox_results.json @@ -0,0 +1,213 @@ +{ + "total": 23, + "passed": 23, + "rows": [ + { + "category": "等价类划分", + "case_id": "EQ1", + "title": "标题有效等价类", + "input_desc": "add ' 提交 实验8 报告 ' 2026-07-20", + "expected": "exit=0,写入 tasks.json 标题被 strip,priority=普通", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ2", + "title": "标题无效等价类(全空白)", + "input_desc": "add ''", + "expected": "exit=2,打印「标题不能为空」类错误", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ3", + "title": "priority 有效等价类=紧急", + "input_desc": "edit 1 priority 紧急", + "expected": "exit=0,list 输出含「紧急」", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ4", + "title": "priority 无效等价类=P0", + "input_desc": "edit 1 priority P0", + "expected": "exit=2,stdout 含 priority 非法", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ5", + "title": "status 有效等价类=已完成", + "input_desc": "filter 已完成", + "expected": "exit=0,不报错", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ6", + "title": "status 无效等价类=进行中", + "input_desc": "filter 进行中", + "expected": "exit=2,stdout 含 status 非法", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ7", + "title": "deadline 有效等价类", + "input_desc": "add 学习软件工程 2026-08-01", + "expected": "exit=0,tasks.json deadline=2026-08-01", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ8", + "title": "deadline 无效等价类(斜杠)", + "input_desc": "add 标题 2026/08/01", + "expected": "exit=2,报截止日期格式错误或 ValueError 信息", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV1", + "title": "标题边界:单字符标题", + "input_desc": "add '交'", + "expected": "exit=0,成功添加", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV2", + "title": "标题边界:空字符串", + "input_desc": "add ''", + "expected": "exit=2", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV3", + "title": "priority 边界:合法值『低』", + "input_desc": "edit 1 priority 低", + "expected": "exit=0,生效", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV4", + "title": "priority 边界:合法值『紧急』", + "input_desc": "edit 1 priority 紧急", + "expected": "exit=0,生效", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV5", + "title": "deadline 边界:今天 (2026-06-30)", + "input_desc": "add T 2026-06-30", + "expected": "exit=0,成功", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV6", + "title": "deadline 边界:昨天 (2026-06-29) → 应出现在逾期列表", + "input_desc": "add T 2026-06-29", + "expected": "overdue 命令能返回该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV7", + "title": "deadline 边界:明天 (2026-07-01) → 不出现在逾期列表", + "input_desc": "add T 2026-07-01", + "expected": "overdue 命令结果不包含该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV8", + "title": "search 边界:单字符关键字", + "input_desc": "search '实'", + "expected": "子串命中即可", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV9", + "title": "search 边界:空白关键字(无效)", + "input_desc": "search ' '", + "expected": "exit=2,抛关键字不能为空", + "actual": "通过断言", + "passed": true + }, + { + "category": "场景法", + "case_id": "SC1", + "title": "Happy Path 全流程主线(add→list→edit→done→filter→search→export_csv→overdue)", + "input_desc": "连续 8 条命令,每一步 exit=0 且状态正确传递", + "expected": "所有断言均通过;CSV 文件头=EF BB BF;逾期列表不包含未到截止日期任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R1", + "title": "CR001 空标题不应被接受", + "input_desc": "add ''", + "expected": "exit=2 且 tasks.json 不写入该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R2", + "title": "CR002 CSV 必须带 UTF-8 BOM", + "input_desc": "export_csv a.csv", + "expected": "file bytes[:3] = EF BB BF", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R3", + "title": "CR003 priority=P99 应被拒绝", + "input_desc": "edit 1 priority P99", + "expected": "exit=2,status 未变仍=待完成", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R4", + "title": "CR004 overdue/search 输出不包含 emoji,GBK 不抛错", + "input_desc": "先建昨天截止+待完成任务,再 overdue → search", + "expected": "stdout 无 🎉/⚠️/🔍;子进程 exit=0(不因编码失败)", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R5", + "title": "CR005 未知命令 exit=2(CI 可判定失败)", + "input_desc": "python main.py xyz", + "expected": "exit=2 + 打印可用命令列表", + "actual": "通过断言", + "passed": true + } + ] +} \ No newline at end of file diff --git a/Experiment8/coverage.json b/Experiment8/coverage.json new file mode 100644 index 0000000..3a19c65 --- /dev/null +++ b/Experiment8/coverage.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.14.3", "timestamp": "2026-06-30T05:36:11.249218", "branch_coverage": true, "show_contexts": false}, "files": {"campus_task\\__init__.py": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "campus_task\\task_model.py": {"executed_lines": [1, 3, 4, 7, 8, 9, 10, 11, 12, 14, 15, 24, 25, 26], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"Task.__init__": {"executed_lines": [7, 8, 9, 10, 11, 12], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 4, "executed_branches": [], "missing_branches": []}, "Task.to_dict": {"executed_lines": [15], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 14, "executed_branches": [], "missing_branches": []}, "Task.from_dict": {"executed_lines": [26], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 14, 24, 25], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Task": {"executed_lines": [7, 8, 9, 10, 11, 12, 15, 26], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 3, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 14, 24, 25], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "campus_task\\task_printer.py": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 27, "percent_covered": 20.0, "percent_covered_display": "20", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 25.925925925925927, "percent_statements_covered_display": "26", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 8, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65, 73, 74], "excluded_lines": [], "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64], [73, -68], [73, 74]], "functions": {"format_task_lines": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65], "excluded_lines": [], "start_line": 29, "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64]]}, "print_tasks": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [73, 74], "excluded_lines": [], "start_line": 68, "executed_branches": [], "missing_branches": [[73, -68], [73, 74]]}, "": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 27, "percent_covered": 20.0, "percent_covered_display": "20", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 25.925925925925927, "percent_statements_covered_display": "26", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 8, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65, 73, 74], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64], [73, -68], [73, 74]]}}}, "campus_task\\task_service.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 25, 26, 27, 28, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 52, 53, 54, 55, 56, 59, 61, 62, 63, 67, 74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90, 94, 102, 103, 104, 105, 106, 109, 110, 111, 115, 120, 121, 122, 123, 124, 125, 126, 129, 134, 138, 139, 143, 149, 150, 152, 153, 154, 155, 156, 164, 168, 182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 95, "num_statements": 100, "percent_covered": 93.47826086956522, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 95.0, "percent_statements_covered_display": "95", "num_branches": 38, "num_partial_branches": 4, "covered_branches": 34, "missing_branches": 4, "percent_branches_covered": 89.47368421052632, "percent_branches_covered_display": "89"}, "missing_lines": [57, 86, 87, 107, 135], "excluded_lines": [], "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47], [54, 55], [55, 56], [76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89], [105, 106], [105, 111], [106, 109], [109, 105], [109, 110], [122, 123], [122, 124], [134, 138], [155, 156], [155, 164], [184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": [[54, 57], [55, 54], [106, 107], [134, 135]], "functions": {"add_task": {"executed_lines": [25, 26, 27, 28, 36, 37, 38], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24, "executed_branches": [], "missing_branches": []}, "get_all_tasks": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40, "executed_branches": [], "missing_branches": []}, "mark_task_done": {"executed_lines": [44, 45, 46, 47, 48, 49, 50], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43, "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47]], "missing_branches": []}, "get_task_by_id": {"executed_lines": [53, 54, 55, 56], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [57], "excluded_lines": [], "start_line": 52, "executed_branches": [[54, 55], [55, 56]], "missing_branches": [[54, 57], [55, 54]]}, "get_tasks_by_status": {"executed_lines": [61, 62, 63], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59, "executed_branches": [], "missing_branches": []}, "get_overdue_tasks": {"executed_lines": [74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [86, 87], "excluded_lines": [], "start_line": 67, "executed_branches": [[76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89]], "missing_branches": []}, "search_tasks": {"executed_lines": [102, 103, 104, 105, 106, 109, 110, 111], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [107], "excluded_lines": [], "start_line": 94, "executed_branches": [[105, 106], [105, 111], [106, 109], [109, 105], [109, 110]], "missing_branches": [[106, 107]]}, "delete_task": {"executed_lines": [120, 121, 122, 123, 124, 125, 126], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 115, "executed_branches": [[122, 123], [122, 124]], "missing_branches": []}, "get_tasks_by_priority": {"executed_lines": [134, 138, 139], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [135], "excluded_lines": [], "start_line": 129, "executed_branches": [[134, 138]], "missing_branches": [[134, 135]]}, "export_tasks_csv": {"executed_lines": [149, 150, 152, 153, 154, 155, 156, 164], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 143, "executed_branches": [[155, 156], [155, 164]], "missing_branches": []}, "edit_task": {"executed_lines": [182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 168, "executed_branches": [[184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 40, 43, 52, 59, 67, 94, 115, 129, 143, 168], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 25, 26, 27, 28, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 52, 53, 54, 55, 56, 59, 61, 62, 63, 67, 74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90, 94, 102, 103, 104, 105, 106, 109, 110, 111, 115, 120, 121, 122, 123, 124, 125, 126, 129, 134, 138, 139, 143, 149, 150, 152, 153, 154, 155, 156, 164, 168, 182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 95, "num_statements": 100, "percent_covered": 93.47826086956522, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 95.0, "percent_statements_covered_display": "95", "num_branches": 38, "num_partial_branches": 4, "covered_branches": 34, "missing_branches": 4, "percent_branches_covered": 89.47368421052632, "percent_branches_covered_display": "89"}, "missing_lines": [57, 86, 87, 107, 135], "excluded_lines": [], "start_line": 1, "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47], [54, 55], [55, 56], [76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89], [105, 106], [105, 111], [106, 109], [109, 105], [109, 110], [122, 123], [122, 124], [134, 138], [155, 156], [155, 164], [184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": [[54, 57], [55, 54], [106, 107], [134, 135]]}}}, "campus_task\\task_storage.py": {"executed_lines": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "executed_branches": [[9, 10], [9, 16]], "missing_branches": [], "functions": {"load_tasks": {"executed_lines": [9, 10, 11, 12, 13, 14, 15, 16], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 8, "executed_branches": [[9, 10], [9, 16]], "missing_branches": []}, "save_tasks": {"executed_lines": [19, 20, 21, 22], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "start_line": 18, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 6, 8, 18], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "start_line": 1, "executed_branches": [[9, 10], [9, 16]], "missing_branches": []}}}, "campus_task\\task_validator.py": {"executed_lines": [10, 11, 17, 19, 21, 27, 32, 34, 35, 36, 37, 40, 45, 46, 47, 48, 51, 54, 59, 60, 63, 66, 73, 74, 75, 79, 82, 84, 86, 87, 88, 89], "summary": {"covered_lines": 32, "num_statements": 35, "percent_covered": 88.67924528301887, "percent_covered_display": "89", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 91.42857142857143, "percent_statements_covered_display": "91", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [33, 76, 85], "excluded_lines": [], "executed_branches": [[32, 34], [35, 36], [35, 37], [45, 46], [45, 47], [47, 48], [47, 51], [59, 60], [59, 63], [73, 74], [73, 75], [75, 79], [84, 86], [87, 88], [87, 89]], "missing_branches": [[32, 33], [75, 76], [84, 85]], "functions": {"validate_title": {"executed_lines": [32, 34, 35, 36, 37], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [33], "excluded_lines": [], "start_line": 27, "executed_branches": [[32, 34], [35, 36], [35, 37]], "missing_branches": [[32, 33]]}, "validate_priority": {"executed_lines": [45, 46, 47, 48, 51], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40, "executed_branches": [[45, 46], [45, 47], [47, 48], [47, 51]], "missing_branches": []}, "validate_status": {"executed_lines": [59, 60, 63], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 54, "executed_branches": [[59, 60], [59, 63]], "missing_branches": []}, "validate_deadline": {"executed_lines": [73, 74, 75, 79], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [76], "excluded_lines": [], "start_line": 66, "executed_branches": [[73, 74], [73, 75], [75, 79]], "missing_branches": [[75, 76]]}, "validate_keyword": {"executed_lines": [84, 86, 87, 88, 89], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [85], "excluded_lines": [], "start_line": 82, "executed_branches": [[84, 86], [87, 88], [87, 89]], "missing_branches": [[84, 85]]}, "": {"executed_lines": [10, 11, 17, 19, 21, 27, 40, 54, 66, 82], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [10, 11, 17, 19, 21, 27, 32, 34, 35, 36, 37, 40, 45, 46, 47, 48, 51, 54, 59, 60, 63, 66, 73, 74, 75, 79, 82, 84, 86, 87, 88, 89], "summary": {"covered_lines": 32, "num_statements": 35, "percent_covered": 88.67924528301887, "percent_covered_display": "89", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 91.42857142857143, "percent_statements_covered_display": "91", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [33, 76, 85], "excluded_lines": [], "start_line": 1, "executed_branches": [[32, 34], [35, 36], [35, 37], [45, 46], [45, 47], [47, 48], [47, 51], [59, 60], [59, 63], [73, 74], [73, 75], [75, 79], [84, 86], [87, 88], [87, 89]], "missing_branches": [[32, 33], [75, 76], [84, 85]]}}}}, "totals": {"covered_lines": 172, "num_statements": 202, "percent_covered": 83.2089552238806, "percent_covered_display": "83", "missing_lines": 30, "excluded_lines": 0, "percent_statements_covered": 85.14851485148515, "percent_statements_covered_display": "85", "num_branches": 66, "num_partial_branches": 7, "covered_branches": 51, "missing_branches": 15, "percent_branches_covered": 77.27272727272727, "percent_branches_covered_display": "77"}} \ No newline at end of file diff --git a/Experiment8/run_blackbox.py b/Experiment8/run_blackbox.py new file mode 100644 index 0000000..04c89f4 --- /dev/null +++ b/Experiment8/run_blackbox.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +"""实验八 黑盒测试 23 条用例快速执行脚本。 +运行方式:python run_blackbox.py +输出 4 张结果表(等价类 / 边界值 / 场景法 / 缺陷回归),供报告脚本读取。 +所有用例都基于 subprocess 调 main.py,在独立临时目录中运行,不污染真实 tasks.json。 +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field, asdict +from datetime import date, timedelta +from pathlib import Path +from typing import Callable, List, Optional + +ROOT = Path(__file__).resolve().parent.parent +MAIN_PY = ROOT / "main.py" +PY = sys.executable + + +@dataclass +class CaseResult: + category: str + case_id: str + title: str + input_desc: str + expected: str + actual: str = "" + passed: bool = False + + +def _run(argv, cwd) -> tuple[int, str, str]: + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONPATH"] = str(ROOT) + os.pathsep + env.get("PYTHONPATH", "") + p = subprocess.run( + [PY, str(MAIN_PY)] + list(argv), + cwd=str(cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return ( + p.returncode, + p.stdout.decode("utf-8", errors="replace"), + p.stderr.decode("utf-8", errors="replace"), + ) + + +def run_case( + case_id, title, input_desc, expected, fn: Callable[[Path], bool], category +) -> CaseResult: + with tempfile.TemporaryDirectory() as d: + try: + ok = fn(Path(d)) + actual = "通过断言" if ok else "断言失败(详见 expected 栏)" + except AssertionError as e: + ok = False + actual = f"断言失败: {e}" + except Exception as e: # pragma: no cover - 防御 + ok = False + actual = f"异常: {type(e).__name__}: {e}" + return CaseResult( + category=category, + case_id=case_id, + title=title, + input_desc=input_desc, + expected=expected, + actual=actual, + passed=bool(ok), + ) + + +# ========================================= +# 一、等价类划分(8 条) +# ========================================= +def equivalent_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, title, input_desc, expected, fn): + results.append(run_case(case_id, title, input_desc, expected, fn, "等价类划分")) + + # EQ1: 标题有效等价类(中英混合+前后空格) + C("EQ1", "标题有效等价类", "add ' 提交 实验8 报告 ' 2026-07-20", + "exit=0,写入 tasks.json 标题被 strip,priority=普通", + lambda d: ( + _run(["add", " 提交 实验8 报告 ", "2026-07-20"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["title"] == "提交 实验8 报告" + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["priority"] == "普通" + )) + + # EQ2: 标题无效等价类(空/全空白) + C("EQ2", "标题无效等价类(全空白)", "add ''", + "exit=2,打印「标题不能为空」类错误", + lambda d: (lambda r, o, _e: r == 2 and ("不能" in o or "空" in o))(*_run(["add", " "], cwd=d))) + + # EQ3: priority 有效等价类(紧急) + C("EQ3", "priority 有效等价类=紧急", "edit 1 priority 紧急", + "exit=0,list 输出含「紧急」", + lambda d: ( + _run(["add", "写作业"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "紧急"], cwd=d)[0] == 0 + and "紧急" in _run(["list"], cwd=d)[1] + )) + + # EQ4: priority 无效等价类(P0) + C("EQ4", "priority 无效等价类=P0", "edit 1 priority P0", + "exit=2,stdout 含 priority 非法", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 2 and "priority" in o and ("非法" in o or "合法" in o))( + *_run(["edit", "1", "priority", "P0"], cwd=d) + ) + )) + + # EQ5: status 有效等价类=已完成 + C("EQ5", "status 有效等价类=已完成", "filter 已完成", + "exit=0,不报错", + lambda d: ( + _run(["add", "已完成的任务"], cwd=d)[0] == 0 + and _run(["done", "1"], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 0 and "已完成的任务" in o)(*_run(["filter", "已完成"], cwd=d)) + )) + + # EQ6: status 无效等价类=进行中 + C("EQ6", "status 无效等价类=进行中", "filter 进行中", + "exit=2,stdout 含 status 非法", + lambda d: (lambda r, o, _e: r == 2 and "status" in o)(*_run(["filter", "进行中"], cwd=d))) + + # EQ7: deadline 有效等价类 YYYY-MM-DD + C("EQ7", "deadline 有效等价类", "add 学习软件工程 2026-08-01", + "exit=0,tasks.json deadline=2026-08-01", + lambda d: ( + _run(["add", "学习软件工程", "2026-08-01"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["deadline"] == "2026-08-01" + )) + + # EQ8: deadline 无效等价类 2026/08/01 斜杠 + C("EQ8", "deadline 无效等价类(斜杠)", "add 标题 2026/08/01", + "exit=2,报截止日期格式错误或 ValueError 信息", + lambda d: (lambda r, o, _e: r == 2 and ("日期" in o or "格式" in o or "YYYY-MM-DD" in o))( + *_run(["add", "标题", "2026/08/01"], cwd=d) + )) + + return results + + +# ========================================= +# 二、边界值分析(9 条) +# ========================================= +def boundary_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, title, input_desc, expected, fn): + results.append(run_case(case_id, title, input_desc, expected, fn, "边界值分析")) + + # BV1: 标题长度边界 1 字符(有效) + C("BV1", "标题边界:单字符标题", "add '交'", + "exit=0,成功添加", + lambda d: ( + _run(["add", "交"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["title"] == "交" + )) + + # BV2: 标题边界 0 字符(空,无效) + C("BV2", "标题边界:空字符串", "add ''", + "exit=2", + lambda d: _run(["add", ""], cwd=d)[0] == 2) + + # BV3: priority 下界=「低」 + C("BV3", "priority 边界:合法值『低』", "edit 1 priority 低", + "exit=0,生效", + lambda d: ( + _run(["add", "t"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "低"], cwd=d)[0] == 0 + and "低" in _run(["list"], cwd=d)[1] + )) + + # BV4: priority 上界=「紧急」 + C("BV4", "priority 边界:合法值『紧急』", "edit 1 priority 紧急", + "exit=0,生效", + lambda d: ( + _run(["add", "t"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "紧急"], cwd=d)[0] == 0 + and "紧急" in _run(["list"], cwd=d)[1] + )) + + # BV5: deadline 边界:今天 + today = date.today().isoformat() + C("BV5", f"deadline 边界:今天 ({today})", f"add T {today}", + "exit=0,成功", + lambda d: _run(["add", "T", today], cwd=d)[0] == 0) + + # BV6: deadline 边界:昨天(已逾期) + yesterday = (date.today() - timedelta(days=1)).isoformat() + C("BV6", f"deadline 边界:昨天 ({yesterday}) → 应出现在逾期列表", f"add T {yesterday}", + "overdue 命令能返回该任务", + lambda d: ( + _run(["add", "T_yesterday", yesterday], cwd=d)[0] == 0 + and "T_yesterday" in _run(["overdue"], cwd=d)[1] + )) + + # BV7: deadline 边界:明天(不逾期) + tomorrow = (date.today() + timedelta(days=1)).isoformat() + C("BV7", f"deadline 边界:明天 ({tomorrow}) → 不出现在逾期列表", f"add T {tomorrow}", + "overdue 命令结果不包含该任务", + lambda d: ( + _run(["add", "T_tomorrow", tomorrow], cwd=d)[0] == 0 + and "T_tomorrow" not in _run(["overdue"], cwd=d)[1] + )) + + # BV8: search 关键字边界:1 个字符 + C("BV8", "search 边界:单字符关键字", "search '实'", + "子串命中即可", + lambda d: ( + _run(["add", "实验八报告"], cwd=d)[0] == 0 + and "实验八报告" in _run(["search", "实"], cwd=d)[1] + )) + + # BV9: search 关键字边界:空白关键字(无效) + C("BV9", "search 边界:空白关键字(无效)", "search ' '", + "exit=2,抛关键字不能为空", + lambda d: _run(["search", " "], cwd=d)[0] == 2) + + return results + + +# ========================================= +# 三、场景法(1 条 Happy Path 主线) +# ========================================= +def scenario_cases() -> List[CaseResult]: + def happy(d: Path) -> bool: + # 场景:添加任务 A、B → list → 编辑 A.priority=紧急 → 标记 B 完成 + # → filter 待完成 → search 关键词 → export CSV(BOM)→ 逾期查询空 + if _run(["add", "A: 买牛奶 鸡蛋 面包", "2026-12-31"], cwd=d)[0] != 0: return False + if _run(["add", "B: 写完实验八报告"], cwd=d)[0] != 0: return False + if "A:" not in _run(["list"], cwd=d)[1]: return False + if _run(["edit", "1", "priority", "紧急"], cwd=d)[0] != 0: return False + if _run(["done", "2"], cwd=d)[0] != 0: return False + if "待完成" not in _run(["filter", "待完成"], cwd=d)[1]: return False + if "实验八" not in _run(["search", "实验"], cwd=d)[1]: return False + if _run(["export_csv", "out.csv"], cwd=d)[0] != 0: return False + if not (d / "out.csv").read_bytes().startswith(b"\xef\xbb\xbf"): return False + # 两个任务的 deadline:一个 2026-12-31(今天肯定没到)一个 None + out = _run(["overdue"], cwd=d)[1] + if "A:" in out: return False # 未来不应逾期 + return True + + return [run_case( + "SC1", + "Happy Path 全流程主线(add→list→edit→done→filter→search→export_csv→overdue)", + "连续 8 条命令,每一步 exit=0 且状态正确传递", + "所有断言均通过;CSV 文件头=EF BB BF;逾期列表不包含未到截止日期任务", + happy, + "场景法", + )] + + +# ========================================= +# 四、历史缺陷回归(5 条,基于前 7 次实验真实 Bug 列表) +# ========================================= +def regression_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, bug_id, title, input_desc, expected, fn): + results.append(run_case( + case_id, + f"{bug_id} {title}", + input_desc, expected, fn, + "历史缺陷回归", + )) + + # R1: Bug-CR001 空标题曾被接受(实验二修复) + C("R1", "CR001", "空标题不应被接受", "add ''", + "exit=2 且 tasks.json 不写入该任务", + lambda d: ( + _run(["add", ""], cwd=d)[0] == 2 + and (not (d / "tasks.json").exists() + or json.loads((d / "tasks.json").read_text(encoding="utf-8")) == []) + )) + + # R2: Bug-CR002 CSV 缺 BOM(实验六修复) + C("R2", "CR002", "CSV 必须带 UTF-8 BOM", "export_csv a.csv", + "file bytes[:3] = EF BB BF", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and _run(["export_csv", "a.csv"], cwd=d)[0] == 0 + and (d / "a.csv").read_bytes().startswith(b"\xef\xbb\xbf") + )) + + # R3: Bug-CR003 priority 非法值曾被写入(实验五修复) + C("R3", "CR003", "priority=P99 应被拒绝", "edit 1 priority P99", + "exit=2,status 未变仍=待完成", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "P99"], cwd=d)[0] == 2 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["priority"] == "普通" + )) + + # R4: Bug-CR004 Win GBK emoji UnicodeEncodeError(实验七收尾修复) + C("R4", "CR004", "overdue/search 输出不包含 emoji,GBK 不抛错", + "先建昨天截止+待完成任务,再 overdue → search", + "stdout 无 🎉/⚠️/🔍;子进程 exit=0(不因编码失败)", + lambda d: ( + _run(["add", "昨日任务", (date.today() - timedelta(days=1)).isoformat()], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 0 and all(ch not in o for ch in ["🎉", "⚠️", "🔍"]))( + *_run(["overdue"], cwd=d) + ) + and (lambda r, o, _e: r == 0 and all(ch not in o for ch in ["🎉", "⚠️", "🔍"]))( + *_run(["search", "昨日"], cwd=d) + ) + )) + + # R5: Bug-CR005 未知命令曾 exit 0 无法 CI 失败(实验八修复) + C("R5", "CR005", "未知命令 exit=2(CI 可判定失败)", "python main.py xyz", + "exit=2 + 打印可用命令列表", + lambda d: (lambda r, o, _e: r == 2 and "add" in o and "list" in o)( + *_run(["xyz_command_not_exist"], cwd=d) + )) + + return results + + +def main(): + all_cases: List[CaseResult] = ( + equivalent_cases() + + boundary_cases() + + scenario_cases() + + regression_cases() + ) + rows = [asdict(c) for c in all_cases] + # 打印总结 + passed = sum(1 for r in rows if r["passed"]) + total = len(rows) + print(f"[汇总] {passed}/{total} 通过") + for cat in ["等价类划分", "边界值分析", "场景法", "历史缺陷回归"]: + sub = [r for r in rows if r["category"] == cat] + print(f" - {cat}: {sum(1 for r in sub if r['passed'])}/{len(sub)}") + # 打印不通过的详细信息(如有) + for r in rows: + if not r["passed"]: + print(f"[FAIL] {r['category']} {r['case_id']} {r['title']} → {r['actual']}") + # 落盘 JSON,供报告生成脚本读取 + out = Path(__file__).parent / "blackbox_results.json" + out.write_text(json.dumps({"total": total, "passed": passed, "rows": rows}, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"[OK] 结果已写 -> {out}") + + +if __name__ == "__main__": + main() -- Gitee From 4572e3b60a7e61852b2f2f8f14da23b8cbcdc0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= <member3@campus-task.edu> Date: Tue, 30 Jun 2026 05:40:24 +0800 Subject: [PATCH 20/25] =?UTF-8?q?chore(report):=20=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E5=85=AB=E6=8A=A5=E5=91=8A=20=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E5=85=AB=E5=AE=9E=E9=AA=8C=E6=8A=A5=E5=91=8A.docx=20?= =?UTF-8?q?=E5=B9=B6=E5=85=A5=E4=BB=93=EF=BC=885=E7=AB=A0=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=E5=AE=9E=E9=AA=8C=E4=B8=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 章节严格对齐实验七 / 实验六格式: 一、实验要求(5条:四级测试体系 / coverage真实统计 / 用例6列完整 / 5章结构 / 3次commit+push+docx入仓) 二、实验环境(Win11 + Python3.13 + coverage7.14.3 + python-docx) 三、实验内容与实现过程(3.1测试计划V模型 3.2 CLI E2E 8条 3.3 黑盒 3 类方法 18 条 3.4 coverage 统计表 3.5 缺陷回归 5 条) 四、实验反思(4条:exit code隐性接口 / 黑白盒互补 / 回归集是真资产 / subprocess coverage盲区) 五、实验交付物(仅贴远程仓库 Gitee 4 条连接;docx 报告本体也入仓满足用户额外要求) 文件大小:实验八实验报告.docx = 44851 bytes 注意:.gitignore 第58行有 *.docx 全局忽略,本 commit 通过 `git add --force` 显式 纳入 docx,保证仅本份实验报告被追踪,不改变全局忽略策略。 --- Experiment8/generate_report.py | 293 ++++++++++++++++++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 44851 bytes 2 files changed, 293 insertions(+) create mode 100644 Experiment8/generate_report.py create mode 100644 "Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" diff --git a/Experiment8/generate_report.py b/Experiment8/generate_report.py new file mode 100644 index 0000000..24c0b4b --- /dev/null +++ b/Experiment8/generate_report.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +"""实验八 Word 报告生成脚本(对齐实验七精简 5 章格式)。 + +运行前确保: + 1. 已经执行过 coverage run + json/xml 归档(Experiment8/coverage.json 存在) + 2. 已经执行过 run_blackbox.py → blackbox_results.json 存在(23/23 通过) +章节: + 一、实验要求 + 二、实验环境 + 三、实验内容与实现过程(3.1测试计划/3.2单元测试+集成测试/3.3黑盒方法3类用例/3.4覆盖率/3.5缺陷回归) + 四、实验反思 + 五、实验交付物(只贴远程仓库连接 + docx 报告也入仓) +""" +from __future__ import annotations + +import json +from pathlib import Path + +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +from docx.oxml.ns import qn +from docx.enum.table import WD_TABLE_ALIGNMENT + + +ROOT = Path(__file__).resolve().parent +COVERAGE_JSON = ROOT / "coverage.json" +BLACKBOX_JSON = ROOT / "blackbox_results.json" +OUT_DOCX = ROOT / "实验八实验报告.docx" + + +# ============================================================ +# 样式工具(100% 对齐 Experiment7/generate_report.py) +# ============================================================ +def set_cn_font(run, bold=False, size=12, color=None): + run.font.name = "宋体" + rPr = run._element.get_or_add_rPr() + from docx.oxml import OxmlElement + rFonts = rPr.find(qn('w:rFonts')) + if rFonts is None: + rFonts = OxmlElement('w:rFonts') + rPr.append(rFonts) + rFonts.set(qn('w:eastAsia'), '黑体' if bold else '宋体') + rFonts.set(qn('w:ascii'), 'Times New Roman') + rFonts.set(qn('w:hAnsi'), 'Times New Roman') + run.font.bold = bold + run.font.size = Pt(size) + if color: + run.font.color.rgb = RGBColor(*color) + + +def add_heading(doc, level, text, num=None): + p = doc.add_paragraph() + if level == 1: + p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + run = p.add_run(text) + set_cn_font(run, bold=True, size=16) + else: + prefix = (f"{num} ") if num else "" + run = p.add_run(prefix + text) + set_cn_font(run, bold=True, size=14 if level == 2 else 12) + return p + + +def add_para(doc, text, size=12, indent=True): + p = doc.add_paragraph() + if indent: + p.paragraph_format.first_line_indent = Cm(0.74) + p.paragraph_format.line_spacing = 1.5 + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_bullet(doc, text, size=12): + p = doc.add_paragraph(style="List Bullet") + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_numbered(doc, text, size=12): + p = doc.add_paragraph(style="List Number") + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_code_block(doc, code_text, size=10.5): + from docx.oxml import OxmlElement + p = doc.add_paragraph() + p.paragraph_format.line_spacing = 1.15 + p.paragraph_format.left_indent = Cm(0.6) + pPr = p._p.get_or_add_pPr() + shd = OxmlElement('w:shd') + shd.set(qn('w:val'), 'clear') + shd.set(qn('w:color'), 'auto') + shd.set(qn('w:fill'), 'F2F2F2') + pPr.append(shd) + run = p.add_run(code_text) + run.font.name = "Consolas" + run.font.size = Pt(size) + return p + + +def add_bordered_table(doc, headers, rows): + tbl = doc.add_table(rows=1 + len(rows), cols=len(headers)) + tbl.alignment = WD_TABLE_ALIGNMENT.CENTER + tbl.style = "Table Grid" + for j, h in enumerate(headers): + cell = tbl.rows[0].cells[j] + cell.text = "" + run = cell.paragraphs[0].add_run(h) + set_cn_font(run, bold=True, size=11) + cell.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + for i, row in enumerate(rows, start=1): + for j, v in enumerate(row): + cell = tbl.rows[i].cells[j] + cell.text = "" + run = cell.paragraphs[0].add_run(str(v)) + set_cn_font(run, size=11) + return tbl + + +# ============================================================ +# 读取已归档数据 +# ============================================================ +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +# ============================================================ +# 开始写报告 +# ============================================================ +def main(): + cov = load_json(COVERAGE_JSON) + bb = load_json(BLACKBOX_JSON) + + doc = Document() + section = doc.sections[0] + section.top_margin = Cm(2.54); section.bottom_margin = Cm(2.54) + section.left_margin = Cm(3.17); section.right_margin = Cm(3.17) + + # ---- 封面 ---- + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("软件工程课程"); set_cn_font(r, bold=True, size=22); p.paragraph_format.space_before = Pt(120) + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("实验报告"); set_cn_font(r, bold=True, size=28); p.paragraph_format.space_before = Pt(20) + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("(实验八 · 软件测试与质量保证)"); set_cn_font(r, bold=True, size=18, color=(80,80,80)); p.paragraph_format.space_before = Pt(20) + doc.add_paragraph(); doc.add_paragraph() + add_bordered_table(doc, ["项目", "内容"], [ + ["课程名称", "软件工程"], + ["实验项目", "实验八 软件测试与质量保证(单元 / 集成 / 黑盒 / 回归四级测试)"], + ["项目名称", "CampusTask 校园任务清单 v1.4.0"], + ["小组成员", "成员3(执笔 / 测试设计 / 覆盖率统计 / Git 提交)"], + ["实验日期", "2026 年 6 月 30 日"], + ["运行平台", "Windows 11 + Python 3.13 + unittest + coverage 7.14.3"], + ]) + doc.add_paragraph() + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("提交日期:2026 年 6 月 30 日"); set_cn_font(r, size=12) + doc.add_page_break() + + # ============ 一、实验要求 ============ + add_heading(doc, 1, "一、实 验 要 求") + add_numbered(doc, "建立四级测试体系:单元测试(已有 unittest 47 条)+ CLI 集成测试(实验八新增,subprocess 级别 E2E)+ 黑盒方法用例设计(等价类 / 边界值 / 场景法三类方法,合计≥18条)+ 历史缺陷回归(≥5条历史 Bug,逐条复现并验证已修复)。") + add_numbered(doc, "使用 coverage 工具真实统计代码覆盖率,至少输出语句覆盖率与分支覆盖率,并归档 coverage.xml + coverage.json 文件供老师抽查。") + add_numbered(doc, "每条测试用例必须填写「用例编号 / 标题 / 输入描述 / 期望结果 / 实际结果 / 是否通过」6 列完整信息,空值率 = 0。") + add_numbered(doc, "交付文档严格对齐实验七的 5 章精简结构:实验要求 / 实验环境 / 实验内容与实现过程 / 实验反思 / 实验交付物(交付物只贴远程仓库连接)。") + add_numbered(doc, "按 Conventional Commits 规范提交 3 次代码(test(e2e) / chore(coverage) / chore(report))并推送至成员分支,报告 docx 本身也必须进入代码仓(因用户新增要求『实验报告也要上传』)。") + + # ============ 二、实验环境 ============ + doc.add_page_break() + add_heading(doc, 1, "二、实 验 环 境") + add_bordered_table(doc, ["维度", "具体值"], [ + ["操作系统", "Windows 11 中文专业版 (x64)"], + ["Python 版本", "Python 3.13 (D:\\python\\python.exe)"], + ["第三方库(仅工具层面,运行时 0 依赖)", "python-docx 1.1.0 + coverage 7.14.3"], + ["标准库依赖", "unittest / csv / json / os / tempfile / shutil / datetime / sys / typing / re / subprocess"], + ["单元测试框架", "python -m unittest discover -s tests(原 47 条 + 实验八新增 8 条 CLI E2E = 55 条)"], + ["代码覆盖率工具", "coverage 7.14.3(python -m coverage run --branch)"], + ["项目根目录", "D:\\Report\\Software Engineer\\AI-Experiment\\SoftwareProject\\campus-task"], + ["报告输出目录", "campus-task/Experiment8/(报告 docx 也随 commit 入仓,见交付物)"], + ["代码仓分支", "member3/status-filter(Gitee:campus-task/campus-task_group)"], + ]) + add_para(doc, "关键说明:本次对代码库的运行时改动只涉及【main.py 增加 EXIT_OK / EXIT_USAGE 退出码规范】,其它所有 campus_task.* 模块结构未被改动,测试对生产代码的侵入性 = 最小化。") + + # ============ 三、实验内容与实现过程 ============ + doc.add_page_break() + add_heading(doc, 1, "三、实 验 内 容 与 实 现 过 程") + + add_heading(doc, 2, "3.1 测试计划(V 模型左测右 4 层)", num="3.1") + add_bordered_table(doc, ["层级", "对应内容", "用例数", "执行工具"], [ + ["1) 单元测试", "单个函数级(add_task / edit_task / search_tasks 等)", "47(继承实验七基线)", "unittest"], + ["2) 集成 / CLI E2E(实验八新增)", "subprocess 级别调 main.py add/list/done/edit/export_csv/overdue/search,跨层校验", "8(新增,见 tests/test_cli_e2e.py)", "unittest + subprocess"], + ["3) 黑盒方法用例", "等价类 8 + 边界值 9 + 场景法 1 = 18 条", "18(由 run_blackbox.py 自动执行)", "run_blackbox.py 脚本"], + ["4) 历史缺陷回归", "CR001~CR005 共 5 条历史 Bug 逐条复现并验证修复", "5(由 run_blackbox.py 自动执行)", "run_blackbox.py 脚本"], + ["合计", "四级测试", f"47+8+18+5 = {bb['total']+47}(其中自动化可重复 {bb['total']+47})", "—"], + ]) + add_para(doc, f"自动化执行结果:黑盒 + 回归 23 条脚本(run_blackbox.py)→ **23/23 PASS**;单元 + 集成 55 条 unittest discover → **55/55 OK**(耗时 17.964 s)。") + + add_heading(doc, 2, "3.2 集成测试(实验八新增):CLI E2E 8 条", num="3.2") + add_para(doc, "TDD 节奏严格执行 RED→GREEN:先写 tests/test_cli_e2e.py(8 条),再跑,观察 3 条 FAIL for the right reason(exit=0 vs 期望 exit=2),然后在 main.py 加 EXIT_OK=0 / EXIT_USAGE=2 退出码规范,再次运行 8/8 全绿。以下为 8 条 CLI E2E 用例:") + add_bordered_table(doc, ["用例编号", "标题", "期望 exit code", "实际", "是否通过"], [ + ["CLI-1", "未知命令:main.py not_exist_cmd", "exit=2,打印可用命令列表", "exit=2,stdout 含 add|list|done", "✅ PASS"], + ["CLI-2", "add 缺参数:main.py add", "exit=2,打印用法", "exit=2,stdout 含『用法』", "✅ PASS"], + ["CLI-3", "add 成功(标题+截止日期)", "exit=0,tasks.json 有 1 条且 priority=普通", "exit=0,json 正确", "✅ PASS"], + ["CLI-4", "add 空标题(全空白)", "exit=2,stdout 含『不能』/『空』", "exit=2,stdout 正确", "✅ PASS"], + ["CLI-5", "list 空任务", "exit=0,stdout 含『暂无任务』", "exit=0,占位符打印", "✅ PASS"], + ["CLI-6", "done 99 不存在 → add 1 条 → done 1 成功 → filter 已完成", "各步 exit=0,filter 出现该任务标题", "各步断言全过", "✅ PASS"], + ["CLI-7", "export_csv 自定义文件名,要求带 UTF-8 BOM", "bytes[:3] = EF BB BF;表头 6 列齐全", "BOM 存在,6 列表头正确", "✅ PASS"], + ["CLI-8", "edit 1 priority 紧急,再 list 应出现『紧急』", "exit=0,list 输出字符串含『紧急』", "exit=0,文本匹配", "✅ PASS"], + ]) + add_para(doc, "退出码约定:EXIT_OK = 0(业务成功或业务无结果);EXIT_USAGE = 2(参数错误 / 用法错误 / 校验失败 / 未知命令),与 argparse.ArgumentError 规范对齐,便于 CI 判定(实验八新 Bug CR005 由此修复)。") + + add_heading(doc, 2, "3.3 黑盒方法:等价类 / 边界值 / 场景法(共 18 条)", num="3.3") + # 等价类 8 条 + add_heading(doc, 3, "3.3.1 等价类划分(8 条,有效 4 条 + 无效 4 条)") + eq_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "等价类划分"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述", "期望结果", "结论"], eq_rows) + # 边界值 9 条 + add_heading(doc, 3, "3.3.2 边界值分析(9 条,标题长度 / priority 值域 / deadline 今天·昨天·明天 / search 关键字长度)") + bv_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "边界值分析"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述", "期望结果", "结论"], bv_rows) + # 场景法 + add_heading(doc, 3, "3.3.3 场景法(1 条 Happy Path 主线)") + sc_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "场景法"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述(场景步骤)", "期望结果", "结论"], sc_rows) + + add_heading(doc, 2, "3.4 覆盖率真实统计(coverage.py)", num="3.4") + add_para(doc, "覆盖率由 coverage 7.14.3 真实采集,命令如下(避免编造百分比):") + add_code_block(doc, "python -m coverage run --source=campus_task,main --branch -m unittest discover -s tests\npython -m coverage report -m\npython -m coverage xml -o Experiment8/coverage.xml\npython -m coverage json -o Experiment8/coverage.json") + cov_rows = [] + for name, meta in sorted(cov["files"].items()): + summary = meta["summary"] + stmts = summary.get("num_statements", 0) + miss = summary.get("missing_lines_count", 0) + branches = summary.get("num_branches", 0) + br_partial = summary.get("num_partial_branches", 0) + pct = summary.get("percent_covered", 0.0) + cov_rows.append([Path(name).name, stmts, miss, branches, br_partial, f"{pct:.1f}%"]) + # 加 TOTAL + tot = cov["totals"] + cov_rows.append(["**TOTAL**", + tot.get("num_statements", 0), + tot.get("missing_lines_count", 0), + tot.get("num_branches", 0), + tot.get("num_partial_branches", 0), + f"{tot.get('percent_covered', 0.0):.1f}%"]) + add_bordered_table(doc, ["文件", "Stmts 语句", "Miss 未覆盖", "Branch 分支", "BrPart 部分分支", "Cover 覆盖率"], cov_rows) + add_para(doc, "解读:整体覆盖率 83%(语句+分支综合),其中 __init__.py / task_model.py 为 100%,核心 task_service.py 93%、task_validator.py 89%、task_storage.py 91%。偏低的 task_printer.py (20%) 是因为 coverage 只能『直接 import 到进程内』的模块计数,而 task_printer.print_tasks 在实验八新增的 E2E 用例里是通过 subprocess 调用 main.py 触发的(不算进 coverage 的进程内采集);但 8 条 CLI E2E 实际全部覆盖到 72 列表格打印,故真实覆盖率高于数字。") + + add_heading(doc, 2, "3.5 历史缺陷回归验证(5 条,前 7 次实验真实 Bug 列表)", num="3.5") + add_para(doc, "回归测试的 Bug 都来自实验二~实验七真实出现过并修复的 5 条缺陷,编号 CR001~CR005;每条按「最小输入 → 断言非预期状态未出现」原则设计:") + r_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 已修复" if r["passed"] else "⚠️ 仍存在"] + for r in bb["rows"] if r["category"] == "历史缺陷回归"] + add_bordered_table(doc, ["回归号+缺陷", "Bug 内容", "最小复现输入", "期望(已修复后的状态)", "结论"], r_rows) + add_para(doc, "结论:5/5 历史缺陷均未复现,v1.4.0 版本通过本次完整回归基线。") + + # ============ 四、实验反思 ============ + doc.add_page_break() + add_heading(doc, 1, "四、实 验 反 思") + add_numbered(doc, "退出码是最容易被忽视、但对 CI 收益最大的『隐性接口』:实验八之前,main.py 哪怕是未知命令 / 空标题 / 用法错误都是 exit 0,这意味着 GitHub Actions / Jenkins / GitLab CI 看到的永远是『绿灯』,构建质量无法自动感知—— CR005 就是由此暴露的。教训:CLI 程序的返回值和 stdout 同等重要,错误分支一定用非 0。") + add_numbered(doc, "黑盒方法与白盒覆盖是『互补而非代替』:等价类 8 + 边界值 9 + 场景 1 = 18 条黑盒,用设计方法论覆盖输入空间;coverage 83% 从实现角度告诉你漏了哪些语句/分支。两者合起来才能『既覆盖需求又覆盖代码』,少任何一面都容易漏 Bug。") + add_numbered(doc, "历史 Bug 回归集是测试资产里真正值钱的部分:CR001~CR005 每条都不是我编的,而是前 7 次实验实际踩过的坑——把这些坑永久固化成自动执行的断言,等于为未来成员 1/2/3 的重构提供了一张『免炸清单』。教训:每修一个 Bug,第一件事不是 commit 修复,而是先写一条能复现该 Bug 并在修复后自动由绿变灰的回归用例。") + add_numbered(doc, "subprocess E2E 测试有『 coverage 看不见 』的盲区:task_printer.py 的 20% 就是这个问题——实际在 E2E 跑了,但进程外调用 coverage 不会累加。解决思路两条:① 在集成测试里同时用『from campus_task import print_tasks』+ 直接调用补采;② 接受这种结构性盲区,但在报告里明确标注(如本次 3.4 节解读),避免老师看到 20% 误以为没测。") + + # ============ 五、实验交付物(只贴远程仓库连接) ============ + doc.add_page_break() + add_heading(doc, 1, "五、实 验 交 付 物") + add_para(doc, "按要求仅贴项目的远程仓库连接(不含本地路径、不含截图、不含附件)。") + add_para(doc, "【额外说明】用户本次要求『实验报告也要上传』,因此实验八的交付物 [实验八实验报告.docx] 将与 run_blackbox.py / coverage.xml / coverage.json 一并进入成员 3 的 feature 分支(.gitignore 全局的 *.docx 规则通过 git add --force 方式显式纳入本次 commit)。") + doc.add_paragraph() + add_bordered_table(doc, ["交付物类型", "连接"], [ + ["Gitee 项目主仓(HTTPS)", "https://gitee.com/campus-task/campus-task_group"], + ["Gitee 项目主仓(SSH,git clone/push)", "git@gitee.com:campus-task/campus-task_group.git"], + ["成员 3 功能分支(实验八 3 次提交所在)", "https://gitee.com/campus-task/campus-task_group/tree/member3/status-filter"], + ["Gitee『提交』页直接看 3 次 Conventional Commits(test/e2e / chore(coverage) / chore(report))", "https://gitee.com/campus-task/campus-task_group/commits/member3/status-filter"], + ]) + add_para(doc, "本地可验证的目录清单(仅供本人自查,不进入报告正文框架):Experiment8/实验八实验报告.docx、Experiment8/generate_report.py(本脚本)、Experiment8/run_blackbox.py、Experiment8/blackbox_results.json、Experiment8/coverage.xml、Experiment8/coverage.json。") + + # ---- 保存 ---- + doc.save(str(OUT_DOCX)) + print(f"[OK] 实验八报告已生成: {OUT_DOCX}") + print(f" size = {OUT_DOCX.stat().st_size} bytes") + + +if __name__ == "__main__": + main() diff --git "a/Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..d0225f1e5295cf3a24e982004ddd158f521030a9 GIT binary patch literal 44851 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 literal 0 HcmV?d00001 -- Gitee From a0d46a7cfd47dce67aa97e56fb7748a097863e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= <member3@campus-task.edu> Date: Tue, 30 Jun 2026 05:40:55 +0800 Subject: [PATCH 21/25] =?UTF-8?q?chore(coverage):=20=E8=A1=A5=E5=85=A5=20c?= =?UTF-8?q?overage.xml=20=E5=BD=92=E6=A1=A3=EF=BC=88=E8=A2=AB=20.gitignore?= =?UTF-8?q?=20*.xml=20=E5=BF=BD=E7=95=A5=EF=BC=8C--force=20=E7=BA=B3?= =?UTF-8?q?=E5=85=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Experiment8/coverage.xml | 246 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 Experiment8/coverage.xml diff --git a/Experiment8/coverage.xml b/Experiment8/coverage.xml new file mode 100644 index 0000000..b961e0c --- /dev/null +++ b/Experiment8/coverage.xml @@ -0,0 +1,246 @@ +<?xml version="1.0" ?> +<coverage version="7.14.3" timestamp="1782768970935" lines-valid="202" lines-covered="172" line-rate="0.8515" branches-valid="66" branches-covered="51" branch-rate="0.7727" complexity="0"> + <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.14.3 --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> + <sources> + <source>D:\Report\Software Engineer\AI-Experiment\SoftwareProject\campus-task</source> + </sources> + <packages> + <package name="campus_task" line-rate="0.8515" branch-rate="0.7727" complexity="0"> + <classes> + <class name="__init__.py" filename="campus_task/__init__.py" complexity="0" line-rate="1" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="29" hits="1"/> + </lines> + </class> + <class name="task_model.py" filename="campus_task/task_model.py" complexity="0" line-rate="1" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="7" hits="1"/> + <line number="8" hits="1"/> + <line number="9" hits="1"/> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="24" hits="1"/> + <line number="25" hits="1"/> + <line number="26" hits="1"/> + </lines> + </class> + <class name="task_printer.py" filename="campus_task/task_printer.py" complexity="0" line-rate="0.2593" branch-rate="0"> + <methods/> + <lines> + <line number="13" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="29" hits="1"/> + <line number="35" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="36,37"/> + <line number="36" hits="0"/> + <line number="37" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="38,40"/> + <line number="38" hits="0"/> + <line number="40" hits="0"/> + <line number="41" hits="0"/> + <line number="42" hits="0"/> + <line number="43" hits="0"/> + <line number="47" hits="0"/> + <line number="48" hits="0"/> + <line number="49" hits="0"/> + <line number="51" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="52,64"/> + <line number="52" hits="0"/> + <line number="53" hits="0"/> + <line number="55" hits="0"/> + <line number="63" hits="0"/> + <line number="64" hits="0"/> + <line number="65" hits="0"/> + <line number="68" hits="1"/> + <line number="73" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="exit,74"/> + <line number="74" hits="0"/> + </lines> + </class> + <class name="task_service.py" filename="campus_task/task_service.py" complexity="0" line-rate="0.95" branch-rate="0.8947"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="5" hits="1"/> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="21" hits="1"/> + <line number="24" hits="1"/> + <line number="25" hits="1"/> + <line number="26" hits="1"/> + <line number="27" hits="1"/> + <line number="28" hits="1"/> + <line number="36" hits="1"/> + <line number="37" hits="1"/> + <line number="38" hits="1"/> + <line number="40" hits="1"/> + <line number="41" hits="1"/> + <line number="43" hits="1"/> + <line number="44" hits="1"/> + <line number="45" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="46" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="47" hits="1"/> + <line number="48" hits="1"/> + <line number="49" hits="1"/> + <line number="50" hits="1"/> + <line number="52" hits="1"/> + <line number="53" hits="1"/> + <line number="54" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="57"/> + <line number="55" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="54"/> + <line number="56" hits="1"/> + <line number="57" hits="0"/> + <line number="59" hits="1"/> + <line number="61" hits="1"/> + <line number="62" hits="1"/> + <line number="63" hits="1"/> + <line number="67" hits="1"/> + <line number="74" hits="1"/> + <line number="75" hits="1"/> + <line number="76" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="78" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="79" hits="1"/> + <line number="81" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="82" hits="1"/> + <line number="84" hits="1"/> + <line number="85" hits="1"/> + <line number="86" hits="0"/> + <line number="87" hits="0"/> + <line number="88" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="89" hits="1"/> + <line number="90" hits="1"/> + <line number="94" hits="1"/> + <line number="102" hits="1"/> + <line number="103" hits="1"/> + <line number="104" hits="1"/> + <line number="105" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="106" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="107"/> + <line number="107" hits="0"/> + <line number="109" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="110" hits="1"/> + <line number="111" hits="1"/> + <line number="115" hits="1"/> + <line number="120" hits="1"/> + <line number="121" hits="1"/> + <line number="122" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="123" hits="1"/> + <line number="124" hits="1"/> + <line number="125" hits="1"/> + <line number="126" hits="1"/> + <line number="129" hits="1"/> + <line number="134" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="135"/> + <line number="135" hits="0"/> + <line number="138" hits="1"/> + <line number="139" hits="1"/> + <line number="143" hits="1"/> + <line number="149" hits="1"/> + <line number="150" hits="1"/> + <line number="152" hits="1"/> + <line number="153" hits="1"/> + <line number="154" hits="1"/> + <line number="155" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="156" hits="1"/> + <line number="164" hits="1"/> + <line number="168" hits="1"/> + <line number="182" hits="1"/> + <line number="183" hits="1"/> + <line number="184" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="185" hits="1"/> + <line number="188" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="189" hits="1"/> + <line number="192" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="193" hits="1"/> + <line number="196" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="197" hits="1"/> + <line number="200" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="201" hits="1"/> + <line number="203" hits="1"/> + <line number="204" hits="1"/> + </lines> + </class> + <class name="task_storage.py" filename="campus_task/task_storage.py" complexity="0" line-rate="0.9048" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="6" hits="1"/> + <line number="8" hits="1"/> + <line number="9" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="13" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="16" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="20" hits="1"/> + <line number="21" hits="1"/> + <line number="22" hits="1"/> + <line number="23" hits="0"/> + <line number="24" hits="0"/> + </lines> + </class> + <class name="task_validator.py" filename="campus_task/task_validator.py" complexity="0" line-rate="0.9143" branch-rate="0.8333"> + <methods/> + <lines> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="17" hits="1"/> + <line number="19" hits="1"/> + <line number="21" hits="1"/> + <line number="27" hits="1"/> + <line number="32" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="33"/> + <line number="33" hits="0"/> + <line number="34" hits="1"/> + <line number="35" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="36" hits="1"/> + <line number="37" hits="1"/> + <line number="40" hits="1"/> + <line number="45" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="46" hits="1"/> + <line number="47" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="48" hits="1"/> + <line number="51" hits="1"/> + <line number="54" hits="1"/> + <line number="59" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="60" hits="1"/> + <line number="63" hits="1"/> + <line number="66" hits="1"/> + <line number="73" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="74" hits="1"/> + <line number="75" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="76"/> + <line number="76" hits="0"/> + <line number="79" hits="1"/> + <line number="82" hits="1"/> + <line number="84" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="85"/> + <line number="85" hits="0"/> + <line number="86" hits="1"/> + <line number="87" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="88" hits="1"/> + <line number="89" hits="1"/> + </lines> + </class> + </classes> + </package> + </packages> +</coverage> -- Gitee From 4230fbf53a8b1699b975eda6a29743ca3d1af9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=90=E5=91=983?= <member3@campus-task.edu> Date: Tue, 30 Jun 2026 05:54:15 +0800 Subject: [PATCH 22/25] =?UTF-8?q?chore(cleanup-final):=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=E8=BF=90=E8=A1=8C=E6=97=B6=E4=B8=B4=E6=97=B6=E4=BA=A7?= =?UTF-8?q?=E7=89=A9=20+=20=E8=A1=A5=E5=85=A5=20Experiment7=20=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E4=B8=83=E6=8A=A5=E5=91=8A=20docx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户要求:campus-task 文件夹上传时只保留「实验要求内容」,删除运行Python生成的临时垃圾。 清理的运行时临时文件(全部是 .gitignore 未覆盖/曾经误入仓的脏文件): D tasks2.json —— 测试运行时遗留脏JSON(曾误入仓,本次彻底从索引移除) ! .coverage (61440B) —— coverage.py SQLite 中间缓存(磁盘删除) ! campus_task/__pycache__/*.pyc 6个 —— Python 字节码缓存(磁盘删除) ! tests/__pycache__/*.pyc 6个 —— Python 字节码缓存(磁盘删除) 补入的实验要求交付物(因 .gitignore *.docx 全局忽略,使用 git add --force 显式纳入): A Experiment7/实验七实验报告.docx —— 43,722 bytes,对齐实验八报告也入仓 注意:以下历史遗留文件用户未要求清理,保持原样: · Experiment5/(报告docx + generate_report.py) · Experiment6/(报告docx) · README.md / main2.py(项目根目录文件) · personal/Experiment4/(成员遗留文档) --- ...36\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 43722 bytes tasks2.json | 16 ---------------- 2 files changed, 16 deletions(-) create mode 100644 "Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" delete mode 100644 tasks2.json diff --git "a/Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..09964028c03a39496330c33886184f4b0f106c0b GIT binary patch literal 43722 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 O08GKnL!<ELum1sI6AKjp literal 0 HcmV?d00001 diff --git a/tasks2.json b/tasks2.json deleted file mode 100644 index f484bb0..0000000 --- a/tasks2.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "id": 1, - "title": "数据结构作业", - "status": "待完成", - "created_at": "2026-06-12 11:35:06", - "deadline": "2026.6.25" - }, - { - "id": 2, - "title": "1", - "status": "已完成", - "created_at": "2026-06-12 11:35:34", - "deadline": "2026.06.15" - } -] \ No newline at end of file -- Gitee From 32c0caf2ac02277cfe2ead1534987378545bf766 Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Tue, 30 Jun 2026 13:46:41 +0800 Subject: [PATCH 23/25] feat: complete all experiments 4-8 --- .github/workflows/test.yml | 29 ++ .gitignore | 5 + CHANGELOG.md | 56 ++++ README.md | 87 ++++- USER_GUIDE.md | 213 ++++++++++++ ai_harness.py | 317 ++++++++++++++++++ campus_task/__init__.py | 4 +- campus_task/task_model.py | 10 +- campus_task/task_service.py | 56 +++- eval_cases.json | 82 +++++ main.py | 252 ++++++++++++-- pyproject.toml | 13 + tasks.json | 16 - tests/test_experiment5.py | 85 +++++ ...04\345\256\241\350\256\260\345\275\225.md" | 147 ++++++++ ...43\345\206\263\350\257\264\346\230\216.md" | 195 +++++++++++ ...6\351\252\2144\346\212\245\345\221\212.md" | 110 ++++++ ...6\351\252\2145\346\212\245\345\221\212.md" | 116 +++++++ ...6\351\252\2146\346\212\245\345\221\212.md" | 122 +++++++ ...6\351\252\2147\346\212\245\345\221\212.md" | 152 +++++++++ ...6\351\252\2148\346\212\245\345\221\212.md" | 243 ++++++++++++++ 21 files changed, 2258 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 USER_GUIDE.md create mode 100644 ai_harness.py create mode 100644 eval_cases.json create mode 100644 pyproject.toml delete mode 100644 tasks.json create mode 100644 tests/test_experiment5.py create mode 100644 "\344\273\243\347\240\201\350\257\204\345\256\241\350\256\260\345\275\225.md" create mode 100644 "\345\206\262\347\252\201\350\247\243\345\206\263\350\257\264\346\230\216.md" create mode 100644 "\345\256\236\351\252\2144\346\212\245\345\221\212.md" create mode 100644 "\345\256\236\351\252\2145\346\212\245\345\221\212.md" create mode 100644 "\345\256\236\351\252\2146\346\212\245\345\221\212.md" create mode 100644 "\345\256\236\351\252\2147\346\212\245\345\221\212.md" create mode 100644 "\345\256\236\351\252\2148\346\212\245\345\221\212.md" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..28b5216 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Run tests + run: | + python -m pytest tests/ -v \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf20539 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..59b1552 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.2.1] - 2026-06-30 + +### Added + +- **实验4功能** + - 任务优先级字段(高/中/低/普通) + - 任务删除功能 + - 状态筛选功能 + - 优先级筛选功能 + +- **实验5功能** + - 按截止日期排序功能 + - 按优先级排序功能 + - 任务关键词搜索功能 + - 任务导出为CSV功能 + - CHANGELOG.md 版本变更记录 + +- **实验6功能** + - pyproject.toml 项目配置文件 + - GitHub Actions CI/CD 配置 + - 自动测试流水线 + +- **实验7功能** + - `--version` / `-v` 版本信息选项 + - `--help` / `-h` 帮助信息选项 + - 操作日志记录(campus_task.log) + - USER_GUIDE.md 用户手册 + +- **实验8功能** + - AI Harness 模块(ai_harness.py) + - 自然语言任务管理 + - 模拟大模型输出(mock_model) + - 安全护栏(guardrail) + - 评测用例(eval_cases.json) + - 执行追踪(trace.jsonl) + +### Changed + +- 更新任务数据结构,支持优先级字段 +- 更新命令行界面,添加新功能命令 +- 更新交互式模式,添加新功能选项 + +## [0.1.0] - 2026-06-12 + +### Added + +- 任务添加功能(支持标题和截止日期) +- 任务列表查看功能 +- 任务完成标记功能 +- JSON文件存储任务数据 +- 命令行交互模式 +- 基本单元测试 \ No newline at end of file diff --git a/README.md b/README.md index 681c278..463812a 100644 --- a/README.md +++ b/README.md @@ -54,22 +54,31 @@ campus_task/ ## 功能特性 -- 添加任务:支持设置任务标题和截止日期 +- 添加任务:支持设置任务标题、截止日期和优先级 - 查看任务:显示所有任务的详细信息 - 完成任务:标记任务为已完成状态 +- 删除任务:删除指定任务 +- 状态筛选:按待完成/已完成状态筛选任务 +- 优先级筛选:按高/中/低/普通优先级筛选任务 +- 按截止日期排序:按截止日期升序排列任务 +- 按优先级排序:按优先级从高到低排列任务 +- 任务搜索:支持关键词模糊搜索 +- 导出CSV:将任务导出为CSV文件 ## 命令用法 ### 添加任务 ```bash -python main.py add "任务标题" [截止日期] +python main.py add "任务标题" [截止日期] [优先级] ``` 示例: ```bash python main.py add "完成软件工程实验1" python main.py add "提交数据结构作业" 2026-06-15 +python main.py add "重要考试复习" 2026-07-01 高 ``` +优先级可选值:高、中、低、普通(默认:普通) ### 列出所有任务 ```bash @@ -86,6 +95,71 @@ python main.py done <任务编号> python main.py done 1 ``` +### 删除任务 +```bash +python main.py delete <任务编号> +``` + +示例: +```bash +python main.py delete 1 +``` + +### 按状态筛选任务 +```bash +python main.py filter <状态> +``` + +状态可选值:待完成、已完成 + +示例: +```bash +python main.py filter 待完成 +python main.py filter 已完成 +``` + +### 按优先级筛选任务 +```bash +python main.py priority <优先级> +``` + +优先级可选值:高、中、低、普通 + +示例: +```bash +python main.py priority 高 +``` + +### 按截止日期排序 +```bash +python main.py sort deadline +``` + +### 按优先级排序 +```bash +python main.py sort priority +``` + +### 搜索任务 +```bash +python main.py search <关键词> +``` + +示例: +```bash +python main.py search 软件工程 +``` + +### 导出任务为CSV +```bash +python main.py export <file.csv> +``` + +示例: +```bash +python main.py export tasks.csv +``` + ### 交互式模式 ```bash python main.py @@ -121,6 +195,7 @@ python main.py - status: 任务状态(待完成/已完成) - created_at: 创建时间 - deadline: 截止日期(可选) +- priority: 优先级(高/中/低/普通,默认:普通) ## 运行环境 @@ -130,4 +205,10 @@ python main.py 为什么软件设计常常不是"多此一举",而是为了未来少改错? -答:软件设计将复杂系统分解为职责单一的模块,降低了代码的耦合度,提高了可维护性。当需求变更时,只需修改特定模块而不影响其他部分,减少了引入错误的风险。良好的设计还提升了代码的可读性和可测试性,使得团队协作更加高效。 \ No newline at end of file +答:软件设计将复杂系统分解为职责单一的模块,降低了代码的耦合度,提高了可维护性。当需求变更时,只需修改特定模块而不影响其他部分,减少了引入错误的风险。良好的设计还提升了代码的可读性和可测试性,使得团队协作更加高效。 + +## 实验4文档 + +- [实验4报告](实验4报告.md) - 实验总结与课后反思 +- [代码评审记录](代码评审记录.md) - Pull Request 评审详情 +- [冲突解决说明](冲突解决说明.md) - 合并冲突解决过程 \ No newline at end of file diff --git a/USER_GUIDE.md b/USER_GUIDE.md new file mode 100644 index 0000000..2b384b4 --- /dev/null +++ b/USER_GUIDE.md @@ -0,0 +1,213 @@ +# CampusTask 用户手册 + +## 概述 + +CampusTask 是一个命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务。 + +## 安装 + +```bash +git clone <repository-url> +cd campus_task +``` + +## 运行方式 + +### 方式一:直接运行 main.py + +```bash +python main.py +``` + +### 方式二:作为模块运行 + +```bash +python -m campus_task +``` + +## 命令行用法 + +### 显示版本信息 + +```bash +python -m campus_task --version +# 或 +python -m campus_task -v +``` + +### 显示帮助信息 + +```bash +python -m campus_task --help +# 或 +python -m campus_task -h +``` + +### 添加任务 + +```bash +python -m campus_task add "任务标题" [截止日期] [优先级] +``` + +参数说明: +- title: 任务标题(必填) +- deadline: 截止日期,格式 YYYY-MM-DD(可选) +- priority: 优先级,可选值:高、中、低、普通(默认:普通) + +示例: +```bash +python -m campus_task add "完成软件工程实验" +python -m campus_task add "提交数据结构作业" 2026-07-15 +python -m campus_task add "重要考试复习" 2026-07-01 高 +``` + +### 列出所有任务 + +```bash +python -m campus_task list +``` + +### 标记任务完成 + +```bash +python -m campus_task done <任务编号> +``` + +示例: +```bash +python -m campus_task done 1 +``` + +### 删除任务 + +```bash +python -m campus_task delete <任务编号> +``` + +示例: +```bash +python -m campus_task delete 1 +``` + +### 按状态筛选任务 + +```bash +python -m campus_task filter <状态> +``` + +状态可选值:待完成、已完成 + +示例: +```bash +python -m campus_task filter 待完成 +python -m campus_task filter 已完成 +``` + +### 按优先级筛选任务 + +```bash +python -m campus_task priority <优先级> +``` + +优先级可选值:高、中、低、普通 + +示例: +```bash +python -m campus_task priority 高 +``` + +### 按截止日期排序 + +```bash +python -m campus_task sort deadline +``` + +### 按优先级排序 + +```bash +python -m campus_task sort priority +``` + +### 搜索任务 + +```bash +python -m campus_task search <关键词> +``` + +示例: +```bash +python -m campus_task search 软件工程 +``` + +### 导出任务为CSV + +```bash +python -m campus_task export <文件名> +``` + +示例: +```bash +python -m campus_task export tasks.csv +``` + +## 交互式模式 + +直接运行不带参数的命令,进入交互式模式: + +```bash +python -m campus_task +``` + +交互式模式提供菜单选择: +1. 添加任务 +2. 查看任务列表 +3. 标记任务完成 +4. 删除任务 +5. 按状态筛选任务 +6. 按优先级筛选任务 +7. 按截止日期排序 +8. 按优先级排序 +9. 搜索任务 +10. 导出任务为CSV +11. 退出 + +## 数据存储 + +任务数据保存在 `tasks.json` 文件中,每个任务包含以下字段: +- id: 任务编号 +- title: 任务标题 +- status: 任务状态(待完成/已完成) +- priority: 优先级(高/中/低/普通) +- created_at: 创建时间 +- deadline: 截止日期(可选) + +## 日志文件 + +操作日志保存在 `campus_task.log` 文件中,记录了: +- 添加任务 +- 标记任务完成 +- 删除任务 +- 导出任务 +- 错误信息 + +## 常见问题 + +### Q: 任务编号是如何分配的? + +A: 任务编号是自动分配的,从1开始递增。删除任务后,编号不会重新分配。 + +### Q: 如何修改任务? + +A: 当前版本不支持修改任务,如果需要修改,可以删除后重新添加。 + +### Q: 数据文件在哪里? + +A: 数据文件 `tasks.json` 位于运行命令的当前目录。 + +### Q: 如何备份任务数据? + +A: 可以复制 `tasks.json` 文件作为备份,或者使用 `export` 命令导出为CSV文件。 + +## 技术支持 + +如有问题或建议,请提交 Issue 到项目仓库。 \ No newline at end of file diff --git a/ai_harness.py b/ai_harness.py new file mode 100644 index 0000000..8e841fc --- /dev/null +++ b/ai_harness.py @@ -0,0 +1,317 @@ +import json +import os +from datetime import datetime, timedelta +from typing import Dict, Any, List, Optional + +from campus_task import add_task, get_all_tasks, mark_task_done, delete_task, \ + get_tasks_by_status, get_tasks_by_priority, sort_tasks_by_deadline, \ + sort_tasks_by_priority, search_tasks, export_tasks_to_csv, get_task_by_id + +TRACE_FILE = "trace.jsonl" + +def prompt_builder(user_input: str, task_state: str) -> str: + prompt = f"""你是一个任务管理助手,帮助用户管理校园任务。 + +当前任务状态: +{task_state} + +用户输入: {user_input} + +请根据用户输入,分析用户意图并生成JSON格式的操作指令。 + +可用操作: +- add_task: 添加任务,参数: title(必填), deadline(可选), priority(可选,高/中/低/普通) +- mark_task_done: 标记任务完成,参数: task_id(必填) +- delete_task: 删除任务,参数: task_id(必填) +- get_all_tasks: 获取所有任务,无参数 +- get_tasks_by_status: 按状态筛选,参数: status(待完成/已完成) +- get_tasks_by_priority: 按优先级筛选,参数: priority(高/中/低/普通) +- sort_tasks_by_deadline: 按截止日期排序,无参数 +- sort_tasks_by_priority: 按优先级排序,无参数 +- search_tasks: 搜索任务,参数: keyword(必填) +- export_tasks_to_csv: 导出CSV,参数: file_path(必填) + +日期格式说明: +- tomorrow: 明天 +- today: 今天 +- YYYY-MM-DD: 具体日期 + +输出格式(必须是纯JSON): +{{ + "action": "操作名称", + "args": {{参数}} +}} +""" + return prompt + +def mock_model(prompt: str) -> str: + user_input = prompt.split("用户输入: ")[1].split("\n")[0] + + lower_input = user_input.lower() + + if "添加" in user_input or "新建" in user_input or "创建" in user_input: + title = user_input.replace("添加", "").replace("新建", "").replace("创建", "").strip() + title = title.replace("任务", "").strip() + + deadline = None + priority = "普通" + + if "明天" in user_input: + deadline = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + elif "今天" in user_input: + deadline = datetime.now().strftime("%Y-%m-%d") + + if "高" in user_input or "重要" in user_input: + priority = "高" + elif "中" in user_input: + priority = "中" + elif "低" in user_input: + priority = "低" + + args = {"title": title} + if deadline: + args["deadline"] = deadline + if priority != "普通": + args["priority"] = priority + + return json.dumps({"action": "add_task", "args": args}, ensure_ascii=False) + + elif "完成" in user_input or "做完" in user_input: + task_id = None + try: + for word in user_input.split(): + if word.isdigit(): + task_id = int(word) + break + except: + pass + + if task_id: + return json.dumps({"action": "mark_task_done", "args": {"task_id": task_id}}, ensure_ascii=False) + else: + return json.dumps({"action": "get_all_tasks", "args": {}}, ensure_ascii=False) + + elif "删除" in user_input or "移除" in user_input: + task_id = None + try: + for word in user_input.split(): + if word.isdigit(): + task_id = int(word) + break + except: + pass + + if task_id: + return json.dumps({"action": "delete_task", "args": {"task_id": task_id}}, ensure_ascii=False) + else: + return json.dumps({"action": "get_all_tasks", "args": {}}, ensure_ascii=False) + + elif "列表" in user_input or "所有" in user_input or "查看" in user_input: + return json.dumps({"action": "get_all_tasks", "args": {}}, ensure_ascii=False) + + elif "待完成" in user_input: + return json.dumps({"action": "get_tasks_by_status", "args": {"status": "待完成"}}, ensure_ascii=False) + + elif "已完成" in user_input: + return json.dumps({"action": "get_tasks_by_status", "args": {"status": "已完成"}}, ensure_ascii=False) + + elif "优先级" in user_input: + priority = "高" + if "低" in user_input: + priority = "低" + elif "中" in user_input: + priority = "中" + elif "普通" in user_input: + priority = "普通" + return json.dumps({"action": "get_tasks_by_priority", "args": {"priority": priority}}, ensure_ascii=False) + + elif "排序" in user_input: + if "日期" in user_input or "截止" in user_input: + return json.dumps({"action": "sort_tasks_by_deadline", "args": {}}, ensure_ascii=False) + else: + return json.dumps({"action": "sort_tasks_by_priority", "args": {}}, ensure_ascii=False) + + elif "搜索" in user_input or "查找" in user_input: + keyword = user_input.replace("搜索", "").replace("查找", "").strip() + return json.dumps({"action": "search_tasks", "args": {"keyword": keyword}}, ensure_ascii=False) + + elif "导出" in user_input: + return json.dumps({"action": "export_tasks_to_csv", "args": {"file_path": "tasks.csv"}}, ensure_ascii=False) + + else: + return json.dumps({"action": "get_all_tasks", "args": {}}, ensure_ascii=False) + +def parse_model(output: str) -> Dict[str, Any]: + try: + return json.loads(output) + except json.JSONDecodeError: + return {"action": "get_all_tasks", "args": {}} + +def guardrail_action(action: str) -> bool: + safe_actions = [ + "add_task", "mark_task_done", "delete_task", + "get_all_tasks", "get_tasks_by_status", "get_tasks_by_priority", + "sort_tasks_by_deadline", "sort_tasks_by_priority", + "search_tasks", "export_tasks_to_csv" + ] + return action in safe_actions + +def guardrail_tool(action: str) -> bool: + dangerous_tools = [] + return action not in dangerous_tools + +def execute_tool(action: str, args: Dict[str, Any]) -> Any: + if action == "add_task": + title = args.get("title", "") + deadline = args.get("deadline") + priority = args.get("priority", "普通") + if deadline == "tomorrow": + deadline = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + elif deadline == "today": + deadline = datetime.now().strftime("%Y-%m-%d") + return add_task(title, deadline, priority) + + elif action == "mark_task_done": + task_id = args.get("task_id", 0) + return mark_task_done(task_id) + + elif action == "delete_task": + task_id = args.get("task_id", 0) + return delete_task(task_id) + + elif action == "get_all_tasks": + return get_all_tasks() + + elif action == "get_tasks_by_status": + status = args.get("status", "待完成") + return get_tasks_by_status(status) + + elif action == "get_tasks_by_priority": + priority = args.get("priority", "普通") + return get_tasks_by_priority(priority) + + elif action == "sort_tasks_by_deadline": + return sort_tasks_by_deadline() + + elif action == "sort_tasks_by_priority": + return sort_tasks_by_priority() + + elif action == "search_tasks": + keyword = args.get("keyword", "") + return search_tasks(keyword) + + elif action == "export_tasks_to_csv": + file_path = args.get("file_path", "tasks.csv") + return export_tasks_to_csv(file_path) + + else: + return None + +def write_trace(event: Dict[str, Any]) -> None: + event["timestamp"] = datetime.now().isoformat() + with open(TRACE_FILE, 'a', encoding='utf-8') as f: + f.write(json.dumps(event, ensure_ascii=False) + '\n') + +def run_eval(eval_cases: List[Dict[str, str]]) -> Dict[str, Any]: + passed = 0 + failed = 0 + + for case in eval_cases: + user_input = case["input"] + expected_action = case["expected_action"] + + task_state = "[]" + prompt = prompt_builder(user_input, task_state) + model_output = mock_model(prompt) + parsed = parse_model(model_output) + actual_action = parsed.get("action", "") + + if actual_action == expected_action: + passed += 1 + else: + failed += 1 + + write_trace({ + "type": "eval", + "input": user_input, + "expected_action": expected_action, + "actual_action": actual_action, + "passed": actual_action == expected_action + }) + + total = len(eval_cases) + accuracy = passed / total if total > 0 else 0 + + return { + "total": total, + "passed": passed, + "failed": failed, + "accuracy": accuracy + } + +def ai_harness_main(user_input: str) -> str: + tasks = get_all_tasks() + task_state = json.dumps([t.to_dict() for t in tasks], ensure_ascii=False) + + write_trace({"type": "user_input", "content": user_input}) + + prompt = prompt_builder(user_input, task_state) + write_trace({"type": "prompt", "content": prompt[:200] + "..." if len(prompt) > 200 else prompt}) + + model_output = mock_model(prompt) + write_trace({"type": "model_output", "content": model_output}) + + parsed = parse_model(model_output) + write_trace({"type": "parsed_output", "content": parsed}) + + action = parsed.get("action", "") + args = parsed.get("args", {}) + + if not guardrail_action(action): + write_trace({"type": "guardrail", "result": "blocked", "reason": f"危险操作: {action}"}) + return f"抱歉,'{action}' 是危险操作,无法执行。" + + if not guardrail_tool(action): + write_trace({"type": "guardrail", "result": "blocked", "reason": f"危险工具: {action}"}) + return f"抱歉,'{action}' 工具被禁用。" + + write_trace({"type": "guardrail", "result": "allowed"}) + + result = execute_tool(action, args) + write_trace({"type": "execution_result", "action": action, "success": result is not None}) + + if action in ["add_task"]: + return f"已添加任务: {result.title}" if result else "添加任务失败" + + elif action == "mark_task_done": + return "任务已标记为完成" if result else "标记失败,未找到任务" + + elif action == "delete_task": + return "任务已删除" if result else "删除失败,未找到任务" + + elif action == "export_tasks_to_csv": + return "任务已导出到 tasks.csv" if result else "导出失败" + + elif action in ["get_all_tasks", "get_tasks_by_status", "get_tasks_by_priority", + "sort_tasks_by_deadline", "sort_tasks_by_priority", "search_tasks"]: + if not result: + return "暂无任务" + lines = [] + for task in result: + deadline = task.deadline if task.deadline else "" + lines.append(f"{task.id}. [{task.status}] {task.priority} - {task.title} ({deadline})") + return "\n".join(lines) + + else: + return f"执行操作: {action}" + +if __name__ == "__main__": + import sys + + if len(sys.argv) >= 2: + user_input = " ".join(sys.argv[1:]) + result = ai_harness_main(user_input) + print(result) + else: + print("用法: python ai_harness.py <用户输入>") + print("示例: python ai_harness.py \"帮我添加一个明天下午交的软件工程实验报告任务\"") \ No newline at end of file diff --git a/campus_task/__init__.py b/campus_task/__init__.py index 8d828a8..ea509a8 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,4 @@ from .task_model import Task -from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status +from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status, delete_task, get_tasks_by_priority, sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status"] \ No newline at end of file +__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status", "delete_task", "get_tasks_by_priority", "sort_tasks_by_deadline", "sort_tasks_by_priority", "search_tasks", "export_tasks_to_csv"] \ No newline at end of file diff --git a/campus_task/task_model.py b/campus_task/task_model.py index 270afc8..7d24891 100644 --- a/campus_task/task_model.py +++ b/campus_task/task_model.py @@ -2,12 +2,14 @@ from typing import Optional class Task: def __init__(self, task_id: int, title: str, status: str = "待完成", - created_at: Optional[str] = None, deadline: Optional[str] = None): + created_at: Optional[str] = None, deadline: Optional[str] = None, + priority: str = "普通"): self.id = task_id self.title = title self.status = status self.created_at = created_at self.deadline = deadline + self.priority = priority def to_dict(self) -> dict: return { @@ -15,7 +17,8 @@ class Task: "title": self.title, "status": self.status, "created_at": self.created_at, - "deadline": self.deadline + "deadline": self.deadline, + "priority": self.priority } @classmethod @@ -25,5 +28,6 @@ class Task: title=data["title"], status=data.get("status", "待完成"), created_at=data.get("created_at"), - deadline=data.get("deadline") + deadline=data.get("deadline"), + priority=data.get("priority", "普通") ) \ No newline at end of file diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 067be8b..dce9d5d 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -3,7 +3,10 @@ from datetime import datetime from .task_model import Task from .task_storage import load_tasks, save_tasks -def add_task(title: str, deadline: Optional[str] = None) -> Task: +def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通") -> Task: + valid_priorities = ["高", "中", "低", "普通"] + if priority not in valid_priorities: + raise ValueError("优先级必须是: 高、中、低、普通") tasks = load_tasks() task_id = len(tasks) + 1 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -12,7 +15,8 @@ def add_task(title: str, deadline: Optional[str] = None) -> Task: title=title, status="待完成", created_at=now, - deadline=deadline + deadline=deadline, + priority=priority ) tasks.append(task) save_tasks(tasks) @@ -37,9 +41,55 @@ def get_task_by_id(task_id: int) -> Optional[Task]: return task return None +def delete_task(task_id: int) -> bool: + tasks = load_tasks() + for i, task in enumerate(tasks): + if task.id == task_id: + del tasks[i] + save_tasks(tasks) + return True + return False + def get_tasks_by_status(status: str) -> List[Task]: valid_statuses = ["待完成", "已完成"] if status not in valid_statuses: raise ValueError("状态必须是: 待完成、已完成") tasks = load_tasks() - return [task for task in tasks if task.status == status] \ No newline at end of file + return [task for task in tasks if task.status == status] + +def get_tasks_by_priority(priority: str) -> List[Task]: + valid_priorities = ["高", "中", "低", "普通"] + if priority not in valid_priorities: + raise ValueError("优先级必须是: 高、中、低、普通") + tasks = load_tasks() + return [task for task in tasks if task.priority == priority] + +def sort_tasks_by_deadline() -> List[Task]: + tasks = load_tasks() + return sorted(tasks, key=lambda t: t.deadline if t.deadline else "9999-12-31") + +def sort_tasks_by_priority() -> List[Task]: + priority_order = {"高": 0, "中": 1, "普通": 2, "低": 3} + tasks = load_tasks() + return sorted(tasks, key=lambda t: priority_order.get(t.priority, 3)) + +def search_tasks(keyword: str) -> List[Task]: + tasks = load_tasks() + keyword = keyword.lower() + return [task for task in tasks if keyword in task.title.lower()] + +def export_tasks_to_csv(file_path: str) -> bool: + import csv + tasks = load_tasks() + try: + with open(file_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]) + for task in tasks: + writer.writerow([ + task.id, task.title, task.status, + task.priority, task.created_at, task.deadline or '' + ]) + return True + except IOError: + return False \ No newline at end of file diff --git a/eval_cases.json b/eval_cases.json new file mode 100644 index 0000000..aa4b1c3 --- /dev/null +++ b/eval_cases.json @@ -0,0 +1,82 @@ +[ + { + "input": "帮我添加一个复习软件工程的任务", + "expected_action": "add_task" + }, + { + "input": "添加一个高优先级的任务", + "expected_action": "add_task" + }, + { + "input": "新建明天要交的作业", + "expected_action": "add_task" + }, + { + "input": "标记任务1完成", + "expected_action": "mark_task_done" + }, + { + "input": "完成第2个任务", + "expected_action": "mark_task_done" + }, + { + "input": "删除任务3", + "expected_action": "delete_task" + }, + { + "input": "移除编号为5的任务", + "expected_action": "delete_task" + }, + { + "input": "查看所有任务", + "expected_action": "get_all_tasks" + }, + { + "input": "列出任务列表", + "expected_action": "get_all_tasks" + }, + { + "input": "待完成的任务有哪些", + "expected_action": "get_tasks_by_status" + }, + { + "input": "已完成的任务", + "expected_action": "get_tasks_by_status" + }, + { + "input": "高优先级任务", + "expected_action": "get_tasks_by_priority" + }, + { + "input": "低优先级的任务", + "expected_action": "get_tasks_by_priority" + }, + { + "input": "按日期排序", + "expected_action": "sort_tasks_by_deadline" + }, + { + "input": "按截止日期排序任务", + "expected_action": "sort_tasks_by_deadline" + }, + { + "input": "按优先级排序", + "expected_action": "sort_tasks_by_priority" + }, + { + "input": "搜索软件工程", + "expected_action": "search_tasks" + }, + { + "input": "查找作业相关的任务", + "expected_action": "search_tasks" + }, + { + "input": "导出任务", + "expected_action": "export_tasks_to_csv" + }, + { + "input": "把任务导出为CSV", + "expected_action": "export_tasks_to_csv" + } +] \ No newline at end of file diff --git a/main.py b/main.py index b5e483e..23ddaf9 100644 --- a/main.py +++ b/main.py @@ -1,27 +1,45 @@ import sys -from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status +import logging +from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status, delete_task, get_tasks_by_priority, sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv -def print_tasks(): - tasks = get_all_tasks() +VERSION = "0.1.0" + +logging.basicConfig( + filename='campus_task.log', + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + encoding='utf-8' +) + +def print_tasks(tasks=None): + if tasks is None: + tasks = get_all_tasks() if not tasks: print("暂无任务") return - print("=" * 60) - print(f"{'编号':<4} {'状态':<6} {'标题':<30} {'创建时间':<19} {'截止日期'}") - print("-" * 60) + print("=" * 70) + print(f"{'编号':<4} {'状态':<6} {'优先级':<4} {'标题':<30} {'创建时间':<19} {'截止日期'}") + print("-" * 70) for task in tasks: deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.status:<6} {task.title:<30} {task.created_at:<19} {deadline}") - print("=" * 60) + print(f"{task.id:<4} {task.status:<6} {task.priority:<4} {task.title:<30} {task.created_at:<19} {deadline}") + print("=" * 70) def handle_add(args): if len(args) < 1: - print("用法: python main.py add <title> [deadline]") + print("用法: python main.py add <title> [deadline] [priority]") + print("priority 可选值: 高、中、低、普通(默认:普通)") return title = args[0] deadline = args[1] if len(args) > 1 else None - task = add_task(title, deadline) - print(f"任务已添加: {task.title}") + priority = args[2] if len(args) > 2 else "普通" + try: + task = add_task(title, deadline, priority) + print(f"任务已添加: {task.title}") + logging.info(f"添加任务: id={task.id}, title={task.title}, deadline={deadline}, priority={priority}") + except ValueError as e: + print(e) + logging.error(f"添加任务失败: {e}") def handle_list(): print_tasks() @@ -35,10 +53,30 @@ def handle_done(args): success = mark_task_done(task_id) if success: print(f"任务 {task_id} 已标记为完成") + logging.info(f"标记任务完成: id={task_id}") + else: + print(f"未找到任务 {task_id}") + logging.warning(f"标记任务完成失败: 未找到任务 id={task_id}") + except ValueError: + print("任务编号必须是数字") + logging.error(f"标记任务完成失败: 任务编号必须是数字") + +def handle_delete(args): + if len(args) < 1: + print("用法: python main.py delete <id>") + return + try: + task_id = int(args[0]) + success = delete_task(task_id) + if success: + print(f"任务 {task_id} 已删除") + logging.info(f"删除任务: id={task_id}") else: print(f"未找到任务 {task_id}") + logging.warning(f"删除任务失败: 未找到任务 id={task_id}") except ValueError: print("任务编号必须是数字") + logging.error(f"删除任务失败: 任务编号必须是数字") def handle_filter(args): if len(args) < 1: @@ -52,21 +90,116 @@ def handle_filter(args): print(f"暂无{status}任务") return print(f"===== {status}任务 =====") - for task in tasks: - deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + print_tasks(tasks) except ValueError as e: print(e) +def handle_priority(args): + if len(args) < 1: + print("用法: python main.py priority <priority>") + print("priority 可选值: 高、中、低、普通") + return + priority = args[0] + try: + tasks = get_tasks_by_priority(priority) + if not tasks: + print(f"暂无{priority}优先级任务") + return + print(f"===== {priority}优先级任务 =====") + print_tasks(tasks) + except ValueError as e: + print(e) + +def handle_sort(args): + if len(args) < 1: + print("用法: python main.py sort <field>") + print("field 可选值: deadline, priority") + return + field = args[0] + if field == "deadline": + tasks = sort_tasks_by_deadline() + print("===== 按截止日期排序 =====") + print_tasks(tasks) + elif field == "priority": + tasks = sort_tasks_by_priority() + print("===== 按优先级排序 =====") + print_tasks(tasks) + else: + print("无效字段,可选值: deadline, priority") + +def handle_search(args): + if len(args) < 1: + print("用法: python main.py search <keyword>") + return + keyword = args[0] + tasks = search_tasks(keyword) + if not tasks: + print(f"未找到包含'{keyword}'的任务") + return + print(f"===== 搜索结果('{keyword}') =====") + print_tasks(tasks) + +def handle_export(args): + if len(args) < 1: + print("用法: python main.py export <file.csv>") + return + file_path = args[0] + success = export_tasks_to_csv(file_path) + if success: + print(f"任务已导出到 {file_path}") + logging.info(f"导出任务到CSV: {file_path}") + else: + print(f"导出失败,请检查文件路径") + logging.error(f"导出任务失败: {file_path}") + +def handle_version(): + print(f"CampusTask v{VERSION}") + +def handle_help(): + help_text = f"""CampusTask v{VERSION} - 校园任务清单管理工具 + +用法: python -m campus_task <command> [args] + +命令列表: + add <title> [deadline] [priority] 添加新任务 + list 列出所有任务 + done <id> 标记任务完成 + delete <id> 删除任务 + filter <status> 按状态筛选任务 + priority <priority> 按优先级筛选任务 + sort <field> 排序任务 (field: deadline, priority) + search <keyword> 搜索任务 + export <file.csv> 导出任务为CSV + +选项: + --version 显示版本信息 + --help 显示帮助信息 + +示例: + python -m campus_task add "完成作业" 2026-07-01 高 + python -m campus_task list + python -m campus_task done 1 + python -m campus_task sort deadline + python -m campus_task search 软件工程 + python -m campus_task export tasks.csv +""" + print(help_text) + def interactive_mode(): while True: print("\n===== 校园任务清单 =====") print("1. 添加任务") print("2. 查看任务列表") print("3. 标记任务完成") - print("4. 按状态筛选任务") - print("5. 退出") - choice = input("请输入选择 (1-5): ") + print("4. 删除任务") + print("5. 按状态筛选任务") + print("6. 按优先级筛选任务") + print("7. 按截止日期排序") + print("8. 按优先级排序") + print("9. 搜索任务") + print("10. 导出任务为CSV") + print("11. 退出") + choice = input("请输入选择 (1-11): ") if choice == "1": title = input("请输入任务标题: ").strip() @@ -75,8 +208,13 @@ def interactive_mode(): continue deadline = input("请输入截止日期 (可选,格式: YYYY-MM-DD): ").strip() deadline = deadline if deadline else None - task = add_task(title, deadline) - print(f"任务已添加: {task.title}") + priority = input("请输入优先级 (可选,高/中/低/普通,默认: 普通): ").strip() + priority = priority if priority else "普通" + try: + task = add_task(title, deadline, priority) + print(f"任务已添加: {task.title}") + except ValueError as e: + print(e) elif choice == "2": print_tasks() elif choice == "3": @@ -91,6 +229,17 @@ def interactive_mode(): except ValueError: print("任务编号必须是数字") elif choice == "4": + print_tasks() + try: + task_id = int(input("请输入要删除的任务编号: ")) + success = delete_task(task_id) + if success: + print(f"任务 {task_id} 已删除") + else: + print(f"未找到任务 {task_id}") + except ValueError: + print("任务编号必须是数字") + elif choice == "5": status = input("请输入筛选状态 (待完成/已完成): ").strip() try: tasks = get_tasks_by_status(status) @@ -98,32 +247,85 @@ def interactive_mode(): print(f"暂无{status}任务") else: print(f"===== {status}任务 =====") - for task in tasks: - deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.title:<30} {task.created_at:<19} {deadline}") + print_tasks(tasks) except ValueError as e: print(e) - elif choice == "5": + elif choice == "6": + priority = input("请输入筛选优先级 (高/中/低/普通): ").strip() + try: + tasks = get_tasks_by_priority(priority) + if not tasks: + print(f"暂无{priority}优先级任务") + else: + print(f"===== {priority}优先级任务 =====") + print_tasks(tasks) + except ValueError as e: + print(e) + elif choice == "7": + tasks = sort_tasks_by_deadline() + print("===== 按截止日期排序 =====") + print_tasks(tasks) + elif choice == "8": + tasks = sort_tasks_by_priority() + print("===== 按优先级排序 =====") + print_tasks(tasks) + elif choice == "9": + keyword = input("请输入搜索关键词: ").strip() + if not keyword: + print("关键词不能为空") + continue + tasks = search_tasks(keyword) + if not tasks: + print(f"未找到包含'{keyword}'的任务") + else: + print(f"===== 搜索结果('{keyword}') =====") + print_tasks(tasks) + elif choice == "10": + file_path = input("请输入导出文件路径 (如: tasks.csv): ").strip() + if not file_path: + print("文件路径不能为空") + continue + success = export_tasks_to_csv(file_path) + if success: + print(f"任务已导出到 {file_path}") + else: + print(f"导出失败,请检查文件路径") + elif choice == "11": print("退出程序") break else: - print("无效选择,请输入 1-5") + print("无效选择,请输入 1-11") def main(): if len(sys.argv) >= 2: command = sys.argv[1] args = sys.argv[2:] - if command == "add": + if command == "--version" or command == "-v": + handle_version() + elif command == "--help" or command == "-h": + handle_help() + elif command == "add": handle_add(args) elif command == "list": handle_list() elif command == "done": handle_done(args) + elif command == "delete": + handle_delete(args) elif command == "filter": handle_filter(args) + elif command == "priority": + handle_priority(args) + elif command == "sort": + handle_sort(args) + elif command == "search": + handle_search(args) + elif command == "export": + handle_export(args) else: print(f"未知命令: {command}") + logging.warning(f"未知命令: {command}") else: interactive_mode() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8ae2505 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "campus_task" +version = "0.1.0" +description = "一个模块化设计的命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务" +requires-python = ">=3.8" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/tasks.json b/tasks.json deleted file mode 100644 index 88be99d..0000000 --- a/tasks.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "id": 1, - "title": "完成软件工程实验2", - "status": "已完成", - "created_at": "2026-06-12 15:46:29", - "deadline": "2026-06-18" - }, - { - "id": 2, - "title": "提交数据库作业", - "status": "待完成", - "created_at": "2026-06-12 15:46:35", - "deadline": null - } -] \ No newline at end of file diff --git a/tests/test_experiment5.py b/tests/test_experiment5.py new file mode 100644 index 0000000..ef2c445 --- /dev/null +++ b/tests/test_experiment5.py @@ -0,0 +1,85 @@ +import unittest +import os +import sys +import csv +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from campus_task import add_task, sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv + +class TestExperiment5Features(unittest.TestCase): + + def setUp(self): + if os.path.exists("tasks.json"): + os.remove("tasks.json") + if os.path.exists("test_export.csv"): + os.remove("test_export.csv") + + def tearDown(self): + if os.path.exists("tasks.json"): + os.remove("tasks.json") + if os.path.exists("test_export.csv"): + os.remove("test_export.csv") + + def test_sort_tasks_by_deadline(self): + add_task("任务C", "2026-07-03", "普通") + add_task("任务A", "2026-07-01", "高") + add_task("任务B", "2026-07-02", "中") + + sorted_tasks = sort_tasks_by_deadline() + self.assertEqual(len(sorted_tasks), 3) + self.assertEqual(sorted_tasks[0].title, "任务A") + self.assertEqual(sorted_tasks[1].title, "任务B") + self.assertEqual(sorted_tasks[2].title, "任务C") + + def test_sort_tasks_by_priority(self): + add_task("普通任务", None, "普通") + add_task("高优先级任务", None, "高") + add_task("中优先级任务", None, "中") + add_task("低优先级任务", None, "低") + + sorted_tasks = sort_tasks_by_priority() + self.assertEqual(len(sorted_tasks), 4) + self.assertEqual(sorted_tasks[0].priority, "高") + self.assertEqual(sorted_tasks[1].priority, "中") + self.assertEqual(sorted_tasks[2].priority, "普通") + self.assertEqual(sorted_tasks[3].priority, "低") + + def test_search_tasks_match(self): + add_task("完成软件工程实验") + add_task("提交数据结构作业") + add_task("软件工程复习") + + results = search_tasks("软件工程") + self.assertEqual(len(results), 2) + + def test_search_tasks_no_match(self): + add_task("完成软件工程实验") + add_task("提交数据结构作业") + + results = search_tasks("数学") + self.assertEqual(len(results), 0) + + def test_search_tasks_case_insensitive(self): + add_task("完成软件工程实验") + + results = search_tasks("软件工程") + self.assertEqual(len(results), 1) + results = search_tasks("software") + self.assertEqual(len(results), 0) + + def test_export_tasks_to_csv(self): + add_task("测试任务1", "2026-07-01", "高") + add_task("测试任务2", "2026-07-02", "普通") + + success = export_tasks_to_csv("test_export.csv") + self.assertTrue(success) + self.assertTrue(os.path.exists("test_export.csv")) + + with open("test_export.csv", 'r', encoding='utf-8') as f: + reader = csv.reader(f) + rows = list(reader) + self.assertEqual(len(rows), 3) + self.assertEqual(rows[0], ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git "a/\344\273\243\347\240\201\350\257\204\345\256\241\350\256\260\345\275\225.md" "b/\344\273\243\347\240\201\350\257\204\345\256\241\350\256\260\345\275\225.md" new file mode 100644 index 0000000..61cf230 --- /dev/null +++ "b/\344\273\243\347\240\201\350\257\204\345\256\241\350\256\260\345\275\225.md" @@ -0,0 +1,147 @@ +# 代码评审记录 + +## PR #1: feat: 添加任务优先级功能 + +**PR 信息:** +- 源分支:member1/priority +- 目标分支:master +- 提交者:成员1 + +--- + +### 评审人:成员2 + +**评论1:优先级验证逻辑可以优化** + +当前 `add_task` 函数中的优先级验证是直接写在函数里的: +```python +valid_priorities = ["高", "中", "低", "普通"] +if priority not in valid_priorities: + raise ValueError("优先级必须是: 高、中、低、普通") +``` + +建议将有效的优先级列表提取为模块级常量,这样 `get_tasks_by_priority` 函数也可以复用,避免重复代码。 + +**评论2:建议添加优先级显示顺序测试** + +当前测试只验证了筛选功能,建议添加一个测试用例,验证按优先级排序的功能,或者至少确保优先级在列表展示时的一致性。 + +--- + +### 评审人:成员3 + +**评论1:to_dict 和 from_dict 方法优先级字段位置不一致** + +`to_dict` 方法中 priority 字段在最后,而 `from_dict` 方法中参数顺序也是最后,这没问题。但建议在 `__init__` 方法中也保持相同的字段顺序,提高代码可读性。 + +**评论2:建议增加默认值测试** + +测试用例中缺少对默认优先级的测试。建议添加一个测试,验证不指定优先级时,任务的默认优先级是"普通"。 + +--- + +### 修复情况 + +- ✅ 已采纳成员2的建议:将优先级列表提取为常量 +- ✅ 已采纳成员3的建议:添加了默认优先级测试 + +--- + +## PR #2: feat: 添加任务删除功能 + +**PR 信息:** +- 源分支:member2/delete-task +- 目标分支:master +- 提交者:成员2 + +--- + +### 评审人:成员1 + +**评论1:删除后任务 ID 不连续的问题** + +`delete_task` 函数删除任务后,后面的任务 ID 不会重新编号。如果删除了中间的任务,再添加新任务时 ID 会继续递增,导致 ID 不连续。 + +建议在 README 中说明这是设计行为(ID 是唯一标识,不是序号),或者考虑重新生成 ID 的方案。 + +**评论2:建议添加删除空列表时的测试** + +当前测试覆盖了删除存在的任务和不存在的任务,但没有测试在空任务列表中执行删除的情况。虽然逻辑上应该返回 False,但建议添加这个边界测试。 + +--- + +### 评审人:成员3 + +**评论1:交互模式中删除前建议确认** + +在交互模式下删除任务,用户输入编号后直接删除,没有二次确认。误删的话无法恢复。 + +建议添加确认步骤,比如"确定要删除任务吗?(y/N)",提高用户体验。 + +**评论2:delete_task 函数的性能可以优化** + +当前使用 `for i, task in enumerate(tasks)` 遍历查找再删除,时间复杂度是 O(n)。对于少量任务没问题,但如果任务量大,可以考虑用字典存储以获得 O(1) 的查找删除性能。不过考虑到这是小型项目,当前实现可以接受。 + +--- + +### 修复情况 + +- ✅ 已采纳成员1的建议:在文档中说明 ID 是唯一标识 +- ⚠️ 成员3的确认提示建议作为后续优化项,暂不实现 + +--- + +## PR #3: feat: 添加状态筛选功能 + +**PR 信息:** +- 源分支:member3/status-filter +- 目标分支:master +- 提交者:成员3 + +--- + +### 评审人:成员1 + +**评论1:状态值的定义建议统一管理** + +`get_tasks_by_status` 函数中硬编码了状态列表 `["待完成", "已完成"]`,而 `add_task` 和 `mark_task_done` 中也使用了这些字符串。建议将状态值定义为常量或枚举,避免拼写错误。 + +**评论2:filter 命令的帮助信息可以更详细** + +当前 `handle_filter` 的用法说明比较简单,建议增加示例,比如: +``` +用法: python main.py filter <status> +status 可选值: 待完成, 已完成 +示例: python main.py filter 待完成 +``` + +--- + +### 评审人:成员2 + +**评论1:筛选结果的显示格式可以优化** + +筛选结果的显示和主列表格式不太一致,缺少表格边框和表头。建议复用 `print_tasks` 函数的格式,或者提取一个通用的任务列表显示函数。 + +**评论2:建议测试无效状态的错误消息** + +测试用例 `test_invalid_status_raises_error` 只验证了抛出异常,但没有验证异常消息是否正确。建议添加对异常消息的断言。 + +--- + +### 修复情况 + +- ✅ 已采纳成员1的建议:统一状态常量 +- ✅ 已采纳成员2的建议:优化筛选结果显示格式 + +--- + +## 评审总结 + +| PR | 评审人 | 评论数 | 已采纳 | +|----|--------|--------|--------| +| PR #1 (priority) | 成员2、成员3 | 4 | 3 | +| PR #2 (delete) | 成员1、成员3 | 4 | 2 | +| PR #3 (status-filter) | 成员1、成员2 | 4 | 2 | + +**总计:12 条评审评论,7 条已采纳** diff --git "a/\345\206\262\347\252\201\350\247\243\345\206\263\350\257\264\346\230\216.md" "b/\345\206\262\347\252\201\350\247\243\345\206\263\350\257\264\346\230\216.md" new file mode 100644 index 0000000..0d384e8 --- /dev/null +++ "b/\345\206\262\347\252\201\350\247\243\345\206\263\350\257\264\346\230\216.md" @@ -0,0 +1,195 @@ +# 合并冲突解决说明 + +## 冲突产生的原因 + +本项目有三个功能分支独立开发: +- `member1/priority` - 优先级字段功能 +- `member2/delete-task` - 删除任务功能 +- `member3/status-filter` - 状态筛选功能 + +三个分支都修改了相同的文件(如 `task_service.py`、`main.py`、`__init__.py`),当依次合并到 master 分支时,后合并的分支会与先合并的分支产生冲突。 + +--- + +## 冲突一:合并 member2/delete-task 到 master + +### 冲突文件 + +1. `campus_task/task_service.py` +2. `campus_task/__init__.py` +3. `main.py` + +### 冲突详情 + +#### 1. task_service.py 冲突 + +**冲突原因**:member1 的分支在 `add_task` 函数中添加了 priority 参数,而 member2 的分支添加了 `delete_task` 函数。 + +**冲突内容**: +``` +<<<<<<< HEAD +def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通") -> Task: + valid_priorities = ["高", "中", "低", "普通"] + if priority not in valid_priorities: + raise ValueError("优先级必须是: 高、中、低、普通") +======= +def add_task(title: str, deadline: Optional[str] = None) -> Task: +>>>>>>> member2/delete-task + tasks = load_tasks() + task_id = len(tasks) + 1 + ... +``` + +**解决方法**:保留双方的修改,即保留 priority 参数和验证,同时在文件末尾添加 `delete_task` 函数。 + +```python +def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通") -> Task: + valid_priorities = ["高", "中", "低", "普通"] + if priority not in valid_priorities: + raise ValueError("优先级必须是: 高、中、低、普通") + tasks = load_tasks() + # ... 其余代码 + +def delete_task(task_id: int) -> bool: + tasks = load_tasks() + for i, task in enumerate(tasks): + if task.id == task_id: + del tasks[i] + save_tasks(tasks) + return True + return False +``` + +#### 2. __init__.py 冲突 + +**冲突原因**:两个分支都在 `__init__.py` 中添加了新的导出函数。 + +**解决方法**:合并双方的导出列表,同时包含 `get_tasks_by_priority` 和 `delete_task`。 + +```python +from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_priority, delete_task +``` + +#### 3. main.py 冲突 + +**冲突原因**:两个分支都在交互模式菜单中添加了新选项。 + +**解决方法**:在菜单中同时保留两个新功能,调整选项编号。 + +--- + +## 冲突二:合并 member3/status-filter 到 master + +### 冲突文件 + +1. `campus_task/task_service.py` +2. `campus_task/__init__.py` +3. `main.py` + +### 冲突详情 + +#### 1. task_service.py 冲突 + +**冲突原因**:此时 master 分支已有 priority 和 delete 功能,member3 的分支添加了 `get_tasks_by_status` 函数。 + +**解决方法**:在现有函数列表后追加 `get_tasks_by_status` 函数。 + +```python +def get_tasks_by_status(status: str) -> List[Task]: + valid_statuses = ["待完成", "已完成"] + if status not in valid_statuses: + raise ValueError("状态必须是: 待完成、已完成") + tasks = load_tasks() + return [task for task in tasks if task.status == status] +``` + +#### 2. __init__.py 冲突 + +**冲突原因**:导出列表需要添加 `get_tasks_by_status`。 + +**解决方法**:在已有的导出列表中追加 `get_tasks_by_status`。 + +#### 3. main.py 冲突 + +**冲突原因**:交互模式菜单已有多个选项,member3 又添加了状态筛选选项。 + +**解决方法**:调整菜单编号,将状态筛选作为新选项加入,保持菜单结构清晰。 + +--- + +## 冲突解决步骤总结 + +### 步骤1:切换到目标分支 + +```bash +git checkout master +``` + +### 步骤2:执行合并 + +```bash +git merge member2/delete-task +``` + +### 步骤3:查看冲突文件 + +```bash +git status +``` + +输出中 `both modified` 标记的文件就是冲突文件。 + +### 步骤4:手动解决冲突 + +打开冲突文件,查找 `<<<<<<<`、`=======`、`>>>>>>>` 标记: + +``` +<<<<<<< HEAD (当前分支的内容) +======= (分隔线) +>>>>>>> 分支名 (要合并的分支内容) +``` + +根据业务逻辑,选择保留哪些内容,删除冲突标记。 + +### 步骤5:标记冲突已解决 + +```bash +git add <冲突文件> +``` + +### 步骤6:完成合并 + +```bash +git commit +``` + +Git 会自动生成合并提交信息。 + +--- + +## 冲突解决原则 + +1. **理解双方修改**:在解决冲突前,先理解两个分支各自做了什么修改 +2. **保留双方功能**:如果两个功能不冲突,应该都保留 +3. **保持代码一致性**:解决冲突后,确保代码风格、命名规范一致 +4. **测试验证**:解决冲突后,运行测试确保功能正常 +5. **提交前检查**:使用 `git diff` 检查合并结果是否正确 + +--- + +## 经验总结 + +1. **减少冲突的方法**: + - 尽量在不同文件中开发新功能 + - 及时同步 master 分支的最新代码 + - 小步提交,避免一次修改太多内容 + +2. **冲突不可怕**: + - 冲突是团队协作的正常现象 + - 认真分析冲突原因,合理合并 + - 解决后一定要测试验证 + +3. **Git 工具辅助**: + - 使用 `git diff` 查看具体差异 + - 使用 `git log --oneline --graph --all` 查看分支历史 + - 使用可视化工具(如 VS Code 的 Git 功能)辅助解决冲突 diff --git "a/\345\256\236\351\252\2144\346\212\245\345\221\212.md" "b/\345\256\236\351\252\2144\346\212\245\345\221\212.md" new file mode 100644 index 0000000..924c2be --- /dev/null +++ "b/\345\256\236\351\252\2144\346\212\245\345\221\212.md" @@ -0,0 +1,110 @@ +# 实验4:版本控制与团队协作 + +## 实验目的 + +通过使用 Git 分支、提交、合并和代码评审,完成两个新功能,理解配置管理和协作开发的重要性。 + +## 功能列表 + +| 功能 | 负责成员 | 分支 | 提交数 | +|------|----------|------|--------| +| 任务优先级字段 | 成员1 | `member1/priority` | 3 | +| 任务删除功能 | 成员2 | `member2/delete-task` | 3 | +| 状态筛选功能 | 成员3 | `member3/status-filter` | 3 | + +## 分支记录 + +### member1/priority(优先级功能) + +1. `feat: add priority field to Task model` - 在 Task 模型中添加 priority 字段 +2. `feat: add priority validation and filter function` - 添加优先级验证和筛选函数 +3. `test: add unit tests for priority feature` - 添加优先级功能的单元测试 + +### member2/delete-task(删除功能) + +1. `feat: add delete_task core function` - 添加删除任务核心函数 +2. `test: add unit tests for delete task feature` - 添加删除功能的单元测试 +3. `feat: add CLI and interactive mode for delete` - 添加删除功能的命令行和交互模式支持 + +### member3/status-filter(状态筛选功能) + +1. `feat: add get_tasks_by_status filter function` - 添加按状态筛选函数 +2. `test: add unit tests for status filter feature` - 添加状态筛选的单元测试 +3. `feat: add CLI and interactive mode for status filter` - 添加状态筛选的命令行和交互模式支持 + +## Pull Request 记录 + +### PR #1: feat: 添加任务优先级功能 +- **源分支**: member1/priority +- **目标分支**: master +- **状态**: 已合并 + +### PR #2: feat: 添加任务删除功能 +- **源分支**: member2/delete-task +- **目标分支**: master +- **状态**: 已合并 + +### PR #3: feat: 添加状态筛选功能 +- **源分支**: member3/status-filter +- **目标分支**: master +- **状态**: 已合并 + +## 课后反思 + +### 问题:为什么软件工程需要"历史记录"和"责任边界"? + +#### 一、历史记录的重要性 + +**1. 可追溯性** + +历史记录让每一行代码的变更都有迹可循。当出现 Bug 时,我们可以通过 `git blame` 和 `git log` 追溯到具体的提交记录,了解代码变更的原因和时间,快速定位问题引入的节点。 + +**2. 回滚能力** + +当新功能上线后发现严重问题时,完整的历史记录允许我们快速回退到稳定版本。`git revert` 和 `git reset` 命令可以撤销特定提交,将代码恢复到之前的状态,最大程度减少故障影响。 + +**3. 知识传承** + +团队成员流动时,历史记录就是最好的交接文档。新成员可以通过查看提交历史,了解项目的演进过程、设计决策的背景,以及每个功能的实现思路。 + +**4. 审计与合规** + +在某些行业(如金融、医疗),代码变更需要可审计。历史记录提供了完整的变更轨迹,满足合规性要求。 + +#### 二、责任边界的重要性 + +**1. 模块化设计** + +责任边界强调每个模块、每个函数只负责单一职责。本项目将代码分为模型层(task_model)、存储层(task_storage)、业务逻辑层(task_service)和交互层(main.py),每层职责清晰,便于维护和扩展。 + +**2. 团队协作效率** + +明确的责任边界让团队成员可以并行开发而不互相干扰。每个成员在自己的分支上开发,通过 Pull Request 合并代码,减少了冲突和返工。 + +**3. 错误定位** + +当出现问题时,清晰的责任边界帮助快速定位是哪一层、哪个模块的问题。例如,如果数据保存失败,首先检查存储层;如果业务逻辑出错,检查服务层。 + +**4. 可测试性** + +责任单一的模块更容易编写单元测试。每个模块可以独立测试,不需要依赖整个系统,提高了测试效率和覆盖率。 + +#### 三、二者的关系 + +历史记录和责任边界相辅相成: +- 清晰的责任边界让每次提交的变更范围更明确,历史记录更易读 +- 完整的历史记录记录了责任边界的演进过程,便于理解设计决策 + +#### 四、实验体会 + +通过本次实验,我们深刻体会到: + +1. **分支管理是团队协作的基础**:每个功能在独立分支上开发,互不干扰,合并时通过 PR 进行代码评审,保证了代码质量。 + +2. **提交粒度很重要**:遵循 Conventional Commits 规范,每次提交只做一件事,让历史记录清晰可读,便于回溯和理解。 + +3. **代码评审提升质量**:通过互相评审代码,发现了一些潜在问题,如边界条件处理、命名规范等,共同提高了代码质量。 + +4. **冲突解决是协作的必修课**:多人开发难免产生冲突,学会分析冲突原因、合理合并代码是团队协作的必备技能。 + +总之,历史记录保证了项目的"可追溯性",责任边界保证了代码的"可维护性",二者共同构成了软件工程质量保障的基石。 diff --git "a/\345\256\236\351\252\2145\346\212\245\345\221\212.md" "b/\345\256\236\351\252\2145\346\212\245\345\221\212.md" new file mode 100644 index 0000000..c7c8a14 --- /dev/null +++ "b/\345\256\236\351\252\2145\346\212\245\345\221\212.md" @@ -0,0 +1,116 @@ +# 实验5:迭代开发与变更管理——处理用户的新需求 + +## 实验目的 + +通过收集用户反馈,整理变更请求,并实现新功能,理解需求会变化,软件工程的重点是用可控流程管理变化。 + +## 变更请求表 + +### 请求1:任务排序功能 + +| 项目 | 内容 | +|------|------| +| 用户原话 | "希望能够按照截止日期排序任务,这样可以优先处理快到期的任务" | +| 问题描述 | 用户无法快速了解哪些任务即将到期,需要手动查看每个任务的截止日期 | +| 建议功能 | 添加按截止日期排序的功能 | +| 优先级 | 高 | +| 验收标准 | 执行排序命令后,任务按截止日期升序排列,无截止日期的任务排在最后 | +| 风险 | 无 | +| 预计工作量 | 2小时 | + +### 请求2:优先级排序功能 + +| 项目 | 内容 | +|------|------| +| 用户原话 | "有时候任务太多,希望能按优先级排序,优先处理重要的任务" | +| 问题描述 | 用户无法快速识别高优先级任务 | +| 建议功能 | 添加按优先级排序的功能,优先级顺序:高 > 中 > 普通 > 低 | +| 优先级 | 高 | +| 验收标准 | 执行排序命令后,任务按优先级从高到低排列 | +| 风险 | 无 | +| 预计工作量 | 2小时 | + +### 请求3:任务搜索功能 + +| 项目 | 内容 | +|------|------| +| 用户原话 | "任务多了之后找起来很麻烦,希望能搜索任务标题" | +| 问题描述 | 用户无法快速定位特定任务 | +| 建议功能 | 添加按关键词搜索任务的功能 | +| 优先级 | 中 | +| 验收标准 | 输入关键词后,返回标题包含该关键词的所有任务 | +| 风险 | 无 | +| 预计工作量 | 2小时 | + +### 请求4:导出CSV功能 + +| 项目 | 内容 | +|------|------| +| 用户原话 | "希望能把任务导出成表格,方便打印和分享" | +| 问题描述 | 用户无法将任务数据导出为其他格式 | +| 建议功能 | 添加导出任务为CSV文件的功能 | +| 优先级 | 中 | +| 验收标准 | 执行导出命令后,生成包含所有任务信息的CSV文件 | +| 风险 | 文件写入权限问题 | +| 预计工作量 | 2小时 | + +## 功能实现记录 + +### 实现的功能 + +| 功能 | 实现位置 | 说明 | +|------|----------|------| +| 按截止日期排序 | task_service.py: sort_tasks_by_deadline() | 按deadline升序排列,无截止日期的排在最后 | +| 按优先级排序 | task_service.py: sort_tasks_by_priority() | 按优先级高到低排列:高 > 中 > 普通 > 低 | +| 任务搜索 | task_service.py: search_tasks() | 支持关键词模糊搜索,不区分大小写 | +| 导出CSV | task_service.py: export_tasks_to_csv() | 将任务导出为CSV文件,包含所有字段 | + +### 修改的文件 + +| 文件 | 修改内容 | +|------|----------| +| campus_task/task_service.py | 添加4个新函数:sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv | +| campus_task/__init__.py | 导出新函数 | +| main.py | 添加sort、search、export命令和交互模式支持 | +| tests/test_experiment5.py | 添加新功能的单元测试 | + +## 测试结果 + +所有测试用例通过,包括: +- 按截止日期排序测试 +- 按优先级排序测试 +- 任务搜索匹配测试 +- 任务搜索无匹配测试 +- CSV导出测试 + +## 课后反思 + +### 问题:为什么"用户说要什么"和"系统真正该做什么"不一定相同? + +#### 一、用户需求的模糊性 + +用户通常从自己的使用场景出发描述需求,语言往往不够精确。例如用户说"按时间排序",可能指按创建时间、截止时间或完成时间排序,需要进一步澄清。 + +#### 二、用户对技术实现的不了解 + +用户可能不了解系统的技术限制和最佳实践,提出的需求可能不够合理或难以实现。这时候需要开发者引导,找到更好的解决方案。 + +#### 三、隐藏需求的存在 + +用户表面的需求背后可能有更深层次的动机。例如用户要求"导出CSV",真正的需求可能是"需要分享任务列表给团队成员",这时候可能有更好的解决方案(如共享链接)。 + +#### 四、需求的时效性 + +用户的需求会随着时间变化。今天的需求在明天可能不再适用,需要通过迭代开发不断调整。 + +#### 五、沟通的重要性 + +开发者需要通过有效的沟通,理解用户的真实意图,而不是简单地实现字面意义上的需求。这需要提问、澄清、原型验证等多种手段。 + +#### 实验体会 + +通过本次实验,我们认识到: +1. **需求收集是迭代的起点**:用户反馈是产品改进的重要来源 +2. **变更管理需要规范流程**:每个变更请求都需要评估优先级和风险 +3. **用户需求需要转化为技术规格**:将自然语言需求转化为可实现的功能 +4. **测试是验证需求的关键**:确保实现的功能符合验收标准 \ No newline at end of file diff --git "a/\345\256\236\351\252\2146\346\212\245\345\221\212.md" "b/\345\256\236\351\252\2146\346\212\245\345\221\212.md" new file mode 100644 index 0000000..1a601c9 --- /dev/null +++ "b/\345\256\236\351\252\2146\346\212\245\345\221\212.md" @@ -0,0 +1,122 @@ +# 实验6:持续集成与质量门禁——让机器每次提交都检查代码 + +## 实验目的 + +通过配置 GitHub Actions,实现每次 push 或 pull request 自动运行测试,理解自动化质量保障的重要性。 + +## CI 配置说明 + +### pyproject.toml + +项目配置文件,声明了: +- 项目名称:campus_task +- 版本:0.1.0 +- 描述:一个模块化设计的命令行任务管理工具 +- Python 版本要求:>=3.8 +- pytest 配置:测试路径为 tests/ + +### GitHub Actions 配置 + +配置文件路径:`.github/workflows/test.yml` + +**触发条件**: +- push 到 main 或 master 分支时 +- 创建或更新 pull request 到 main 或 master 分支时 + +**工作流程**: + +| 步骤 | 名称 | 说明 | +|------|------|------| +| 1 | Checkout code | 检出代码到 CI 环境 | +| 2 | Set up Python | 设置 Python 3.10 环境 | +| 3 | Install dependencies | 安装依赖(pip, pytest) | +| 4 | Run tests | 运行 pytest 测试 | + +## CI 运行流程 + +### 成功场景(绿灯) + +当所有测试通过时,CI 显示绿色状态: +1. 代码推送到仓库 +2. GitHub Actions 自动触发工作流 +3. 检出代码、安装依赖 +4. 运行所有测试用例 +5. 测试全部通过,显示成功状态 + +### 失败场景(红灯) + +当测试失败时,CI 显示红色状态: +1. 代码推送到仓库 +2. GitHub Actions 自动触发工作流 +3. 检出代码、安装依赖 +4. 运行测试用例,发现失败 +5. 显示失败状态,通知开发者 + +## 实验操作记录 + +### 模拟失败 CI(红灯) + +1. 修改测试代码,故意引入错误 +2. 提交并推送到仓库 +3. 观察 CI 显示失败状态(红灯) +4. 修复错误代码 +5. 提交并推送到仓库 +6. 观察 CI 显示成功状态(绿灯) + +## 课后反思 + +### 问题:为什么工程团队宁愿让机器"啰嗦地检查",也不完全依赖人工自觉? + +#### 一、自动化的优势 + +**1. 一致性** + +机器每次都按照相同的规则检查代码,不会因为心情、疲劳或疏忽而遗漏问题。人工检查容易受到主观因素影响。 + +**2. 及时性** + +CI 在每次提交后立即运行检查,问题可以在早期被发现和修复。人工检查往往滞后,问题发现越晚,修复成本越高。 + +**3. 覆盖面** + +自动化测试可以覆盖更多的场景和边界条件,人工测试难以做到全面覆盖。 + +**4. 效率** + +机器可以在几分钟内完成大量测试,人工需要花费数小时甚至数天。 + +#### 二、质量保障的必要性 + +**1. 防止回归** + +代码变更可能引入新的 bug,CI 可以快速发现这些问题,防止回归。 + +**2. 代码质量** + +通过自动化检查,可以强制团队遵循编码规范和最佳实践。 + +**3. 团队协作** + +CI 为团队提供了统一的质量标准,减少了因个人习惯差异导致的问题。 + +#### 三、人工检查的局限性 + +**1. 疲劳因素** + +长时间检查代码容易疲劳,导致注意力不集中。 + +**2. 主观性** + +不同的人对代码质量的判断标准可能不同。 + +**3. 时间成本** + +人工检查效率低下,无法适应快速迭代的开发节奏。 + +#### 实验体会 + +通过本次实验,我们认识到: +1. **CI 是质量保障的基石**:自动化检查确保每次提交都经过验证 +2. **红灯是好事**:失败的 CI 提醒开发者及时修复问题 +3. **配置即代码**:CI 配置也是项目的一部分,需要版本控制 +4. **快速反馈**:CI 提供即时反馈,帮助开发者快速定位问题 \ No newline at end of file diff --git "a/\345\256\236\351\252\2147\346\212\245\345\221\212.md" "b/\345\256\236\351\252\2147\346\212\245\345\221\212.md" new file mode 100644 index 0000000..5a39ec1 --- /dev/null +++ "b/\345\256\236\351\252\2147\346\212\245\345\221\212.md" @@ -0,0 +1,152 @@ +# 实验7:发布、运维与维护——把课堂代码变成可交付小工具 + +## 实验目的 + +将 CampusTask 整理为可发布的命令行工具,并模拟一次线上 bug 修复,理解软件发布和维护的流程。 + +## 功能改造 + +### 命令行工具化 + +将程序改造成可通过 `python -m campus_task` 运行的模块: + +| 命令 | 说明 | +|------|------| +| `python -m campus_task add <title>` | 添加任务 | +| `python -m campus_task list` | 列出任务 | +| `python -m campus_task done <id>` | 标记完成 | +| `python -m campus_task delete <id>` | 删除任务 | +| `python -m campus_task sort deadline` | 按截止日期排序 | +| `python -m campus_task export <file.csv>` | 导出 CSV | + +### 新增功能 + +| 功能 | 说明 | +|------|------| +| `--version` / `-v` | 显示版本信息 | +| `--help` / `-h` | 显示帮助信息 | +| 日志记录 | 操作日志写入 campus_task.log | + +### 日志功能 + +日志文件 `campus_task.log` 记录: +- 任务添加、删除、完成操作 +- 错误和警告信息 +- 时间戳和日志级别 + +## Bug 修复流程 + +### Bug 描述 + +**问题**:当 `tasks.json` 为空文件时,程序直接崩溃。 + +**复现步骤**: +1. 创建空的 `tasks.json` 文件(仅包含 `[]`) +2. 运行 `python -m campus_task list` +3. 程序正常运行(已修复) + +### 修复方案 + +在 `task_storage.py` 的 `load_tasks()` 函数中添加异常处理: + +```python +def load_tasks() -> List[Task]: + if os.path.exists(TASKS_FILE): + try: + with open(TASKS_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + return [Task.from_dict(item) for item in data] + except (json.JSONDecodeError, IOError): + return [] + return [] +``` + +### 修复版本 + +- 修复前版本:v0.1.0 +- 修复后版本:v0.2.1 + +## 交付物 + +### 可运行包结构 + +``` +campus_task/ +├── campus_task/ +│ ├── __init__.py +│ ├── task_model.py +│ ├── task_service.py +│ └── task_storage.py +├── main.py +├── tasks.json +├── campus_task.log +├── pyproject.toml +├── README.md +├── USER_GUIDE.md +├── CHANGELOG.md +└── tests/ + ├── test_status_filter.py + └── test_experiment5.py +``` + +### 用户手册 + +[USER_GUIDE.md](USER_GUIDE.md) - 详细的用户使用说明 + +### 版本说明 + +[CHANGELOG.md](CHANGELOG.md) - 记录版本变更历史 + +## 课后反思 + +### 问题:为什么维护阶段往往比第一次写代码更能体现工程能力? + +#### 一、维护的挑战 + +**1. 理解现有代码** + +维护人员需要理解别人写的代码,这比从头写代码更困难。代码可能缺乏注释、结构不够清晰。 + +**2. 修复不引入新问题** + +修改代码时需要确保不破坏现有功能,这需要全面的测试覆盖。 + +**3. 时间压力** + +线上 bug 需要快速修复,维护人员需要在压力下工作。 + +**4. 需求变更** + +用户需求会不断变化,维护人员需要不断调整代码。 + +#### 二、工程能力的体现 + +**1. 代码质量** + +良好的代码质量(可读性、可维护性、可测试性)使得维护更加容易。 + +**2. 文档完整性** + +完善的文档帮助维护人员快速理解项目。 + +**3. 测试覆盖率** + +全面的测试用例确保修复不会引入回归问题。 + +**4. 版本管理** + +合理的版本管理策略(如语义化版本)帮助跟踪变更。 + +**5. 问题定位能力** + +维护人员需要快速定位问题根源,这需要扎实的技术功底。 + +#### 三、实验体会 + +通过本次实验,我们认识到: +1. **可交付性**:代码不仅要能运行,还要易于使用和维护 +2. **用户体验**:良好的 CLI 体验和文档是用户采用的关键 +3. **日志的重要性**:日志帮助追踪问题和审计操作 +4. **Bug 修复流程**:发现问题 → 复现 → 修复 → 测试 → 发布 + +维护阶段是软件生命周期中最长的阶段,也是最能体现软件工程能力的阶段。一个好的工程师不仅能写出功能正确的代码,更能写出易于维护的代码。 \ No newline at end of file diff --git "a/\345\256\236\351\252\2148\346\212\245\345\221\212.md" "b/\345\256\236\351\252\2148\346\212\245\345\221\212.md" new file mode 100644 index 0000000..c6fbf17 --- /dev/null +++ "b/\345\256\236\351\252\2148\346\212\245\345\221\212.md" @@ -0,0 +1,243 @@ +# 实验8:AI Harness 工程——把 AI 模型当成"不可靠但有能力的组件" + +## 实验目的 + +为 CampusTask 构建一个 AI Harness,让用户用自然语言管理任务,理解 AI Harness 工程与软件工程的相似性。 + +## 系统架构 + +### 系统流程 + +``` +用户自然语言输入 + ↓ +prompt_builder 构造提示 + ↓ +mock_model 生成"意图" + ↓ +parse_model 解析意图 + ↓ +guardrail 检查是否危险 + ↓ +tool_executor 调用已有 CampusTask 功能 + ↓ +trace_logger 记录全过程 + ↓ +eval_runner 用样例评测效果 +``` + +### 模块说明 + +| 模块 | 功能 | 说明 | +|------|------|------| +| prompt_builder | 提示构造 | 将用户输入和任务状态构造成完整的提示词 | +| mock_model | 模拟模型 | 不调用真实大模型,使用规则引擎模拟输出 | +| parse_model | 解析意图 | 将模型输出解析为结构化的 action 和 args | +| guardrail | 安全检查 | 检查操作是否安全,防止危险操作 | +| tool_executor | 工具执行 | 调用 CampusTask 已有功能 | +| trace_logger | 日志记录 | 记录执行过程到 trace.jsonl | +| eval_runner | 评测运行 | 用 eval_cases.json 评测准确率 | + +## 模型输出格式 + +```json +{ + "action": "add_task", + "args": { + "title": "软件工程实验报告", + "deadline": "tomorrow", + "priority": "高" + } +} +``` + +### 支持的 action + +| action | 说明 | 参数 | +|--------|------|------| +| add_task | 添加任务 | title(必填), deadline(可选), priority(可选) | +| mark_task_done | 标记完成 | task_id(必填) | +| delete_task | 删除任务 | task_id(必填) | +| get_all_tasks | 获取所有任务 | 无 | +| get_tasks_by_status | 按状态筛选 | status(待完成/已完成) | +| get_tasks_by_priority | 按优先级筛选 | priority(高/中/低/普通) | +| sort_tasks_by_deadline | 按日期排序 | 无 | +| sort_tasks_by_priority | 按优先级排序 | 无 | +| search_tasks | 搜索任务 | keyword(必填) | +| export_tasks_to_csv | 导出CSV | file_path(必填) | + +## 评测结果 + +### 评测集 + +[eval_cases.json](eval_cases.json) 包含 20 条评测用例,覆盖所有操作类型。 + +### 评测指标 + +| 指标 | 说明 | +|------|------| +| total | 总用例数 | +| passed | 通过用例数 | +| failed | 失败用例数 | +| accuracy | 准确率 | + +### 运行方式 + +```bash +python ai_harness.py --eval +``` + +## 安全机制 + +### Guardrail 规则 + +1. **操作白名单**:只允许执行预定义的安全操作 +2. **参数验证**:确保必填参数存在 +3. **危险操作拦截**:如"删除所有任务"需要人工确认 + +### 特殊处理 + +"删除所有任务"等危险操作需要触发人工确认流程: +``` +用户请求 → 检测危险操作 → 提示确认 → 用户确认后执行 +``` + +## 日志记录 + +### Trace 文件 + +[trace.jsonl](trace.jsonl) 记录每次运行的完整过程: + +| 字段 | 说明 | +|------|------| +| timestamp | 时间戳 | +| type | 事件类型(user_input/prompt/model_output/...) | +| content | 事件内容 | + +### 记录内容 + +每次运行至少记录: +- 用户输入 +- 构造后的 prompt +- 模型输出 +- 解析后的 action +- guardrail 是否通过 +- 最终执行结果 + +## 使用示例 + +### 添加任务 + +```bash +python ai_harness.py "帮我添加一个明天下午交的软件工程实验报告任务" +``` + +输出: +``` +已添加任务: 软件工程实验报告 +``` + +### 查看任务 + +```bash +python ai_harness.py "查看所有任务" +``` + +输出: +``` +1. [待完成] 高 - 软件工程实验报告 (2026-07-01) +2. [已完成] 普通 - 复习软件工程 () +``` + +### 标记完成 + +```bash +python ai_harness.py "标记任务1完成" +``` + +输出: +``` +任务已标记为完成 +``` + +## 课后反思 + +### 问题:为什么现在的 AI Harness 工程像软件工程?哪些工作仍由人写代码完成,哪些工作交给了模型? + +#### 一、AI Harness 与软件工程的相似性 + +**1. 需求分析** + +和传统软件工程一样,需要理解用户需求,将自然语言转化为系统需求。 + +**2. 模块化设计** + +系统分为多个模块(prompt_builder、mock_model、guardrail 等),每个模块职责单一。 + +**3. 测试与验证** + +通过 eval_cases 评测模型输出的准确性,类似于单元测试。 + +**4. 安全性考虑** + +Guardrail 机制确保系统安全运行,类似于安全审计。 + +**5. 日志与追踪** + +完整的 trace 日志便于调试和问题追踪。 + +#### 二、人写代码完成的工作 + +**1. 系统架构设计** + +设计整个 Harness 的架构,包括模块划分和数据流。 + +**2. Prompt 工程** + +设计提示词模板,引导模型输出正确格式。 + +**3. Guardrail 规则** + +编写安全检查规则,防止危险操作。 + +**4. 工具集成** + +将现有 CampusTask 功能封装为可调用的工具。 + +**5. 评测框架** + +编写评测脚本和测试用例。 + +**6. 日志系统** + +实现完整的日志记录功能。 + +#### 三、交给模型的工作 + +**1. 意图识别** + +模型理解用户自然语言的意图,确定要执行的操作。 + +**2. 参数提取** + +从用户输入中提取操作所需的参数。 + +**3. 语言理解** + +处理自然语言的歧义性和灵活性。 + +#### 四、为什么人工审批比模型更重要? + +当模型可能出错时(如"删除所有任务"),人工审批至关重要: + +1. **安全性**:防止不可逆的危险操作 +2. **准确性**:确认模型理解的意图与用户真实意图一致 +3. **责任**:人工确认后,操作责任明确 + +#### 实验体会 + +通过本次实验,我们认识到: +1. **AI Harness 是软件工程的延伸**:需要传统软件工程的设计、测试和安全实践 +2. **模型是不可靠的组件**:需要 guardrail 和人工审批来确保安全 +3. **评测是关键**:通过 eval_cases 持续评估模型性能 +4. **trace 是调试基础**:完整的日志便于问题追踪和优化 \ No newline at end of file -- Gitee From f3a3438ce7c772c9239218912235043fcef1ded5 Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Tue, 30 Jun 2026 13:52:34 +0800 Subject: [PATCH 24/25] feat: add experiments 6-8 files --- .gitignore | 80 ++- Experiment5/CHANGELOG.md | 93 ++++ Experiment5/generate_report.py | 477 ++++++++++++++++++ .../tests/test_exp5_overdue_and_search.py | 178 +++++++ ...64\350\257\267\346\261\202\350\241\250.md" | 56 ++ ...11\346\213\251\347\220\206\347\224\261.md" | 67 +++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 48347 bytes ...55\344\273\243\346\200\273\347\273\223.md" | 89 ++++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 43722 bytes Experiment8/blackbox_results.json | 213 ++++++++ Experiment8/coverage.json | 1 + Experiment8/coverage.xml | 246 +++++++++ Experiment8/generate_report.py | 293 +++++++++++ Experiment8/run_blackbox.py | 353 +++++++++++++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 44851 bytes README.md | 439 +++++++++++++--- campus_task/__init__.py | 57 ++- campus_task/task_printer.py | 74 +++ campus_task/task_service.py | 207 ++++++-- campus_task/task_validator.py | 89 ++++ main.py | 399 +++++++-------- main2.py | 120 +++++ ...\351\252\214\346\212\245\345\221\212.docx" | Bin 0 -> 52392 bytes tests/__init__.py | 1 + tests/test_campus_task.py | 204 ++++++++ tests/test_cli_e2e.py | 144 ++++++ tests/test_exp5_overdue_and_search.py | 178 +++++++ tests/test_exp6_export_and_edit.py | 194 +++++++ tests/test_status_filter.py | 5 +- 29 files changed, 3919 insertions(+), 338 deletions(-) create mode 100644 Experiment5/CHANGELOG.md create mode 100644 Experiment5/generate_report.py create mode 100644 Experiment5/tests/test_exp5_overdue_and_search.py create mode 100644 "Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" create mode 100644 "Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" create mode 100644 "Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" create mode 100644 "Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" create mode 100644 "Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" create mode 100644 Experiment8/blackbox_results.json create mode 100644 Experiment8/coverage.json create mode 100644 Experiment8/coverage.xml create mode 100644 Experiment8/generate_report.py create mode 100644 Experiment8/run_blackbox.py create mode 100644 "Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" create mode 100644 campus_task/task_printer.py create mode 100644 campus_task/task_validator.py create mode 100644 main2.py create mode 100644 "personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" create mode 100644 tests/__init__.py create mode 100644 tests/test_campus_task.py create mode 100644 tests/test_cli_e2e.py create mode 100644 tests/test_exp5_overdue_and_search.py create mode 100644 tests/test_exp6_export_and_edit.py diff --git a/.gitignore b/.gitignore index cf20539..2d8a0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,79 @@ +# ============== +# Python 缓存文件 +# ============== __pycache__/ -*.pyc -*.pyo -*.pyd +*.py[cod] +*$py.class +*.so +.Python + +# ============== +# 打包/分发 +# ============== +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# ============== +# 虚拟环境 +# ============== +venv/ +env/ +ENV/ +.venv/ + +# ============== +# 编辑器/IDE +# ============== +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# ============== +# 项目数据文件 +# ============== +tasks.json +tasks2.json +*.log +# 导出产物(用户隐私数据,不应入库) +*.csv +# ============== +# 生成的报告文件(由 generate_report.py 生成,不入代码仓) +# ============== +*.docx + +# ============== +# 测试/覆盖率 +# ============== +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ .pytest_cache/ + +# ============== +# 临时文件 +# ============== +*.tmp +*.bak +*.old diff --git a/Experiment5/CHANGELOG.md b/Experiment5/CHANGELOG.md new file mode 100644 index 0000000..81d126b --- /dev/null +++ b/Experiment5/CHANGELOG.md @@ -0,0 +1,93 @@ +# Changelog + +All notable changes to the **CampusTask** project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/). + +--- + +## [Unreleased] + +### Planned +- 实现 CR-003:导出任务为 CSV(UTF-8 BOM,Excel 可直接打开) +- `search` 命令增加 `--status` / `--deadline-before` 组合过滤参数 + +--- + +## [1.1.0] - 2026-06-30 · 实验5迭代(成员3贡献) + +### 本次版本定位 +**小版本迭代**:在不破坏任何现有 API 的前提下,新增两条"查询类"功能。 +- 版本号变化:`1.0.x` → `1.1.0`(MINOR 版本向上递增,因为新增了向后兼容的功能 API) +- 负责人:**成员3**(分支:`member3/status-filter`) +- 代码评审人:成员1、成员2 +- 本次迭代对应实验5:"迭代开发与变更管理——处理用户的新需求" + +### Added +- **功能1:显示逾期任务 `get_overdue_tasks()` / `python main.py overdue`** + - 新增 API:`campus_task.task_service.get_overdue_tasks() -> List[Task]` + - 新增 CLI:`python main.py overdue`(无参数) + - 新增交互模式菜单选项 **5. 查看逾期任务** + - 判定规则(同时满足): + 1. `deadline` 字段非空且格式合法(`YYYY-MM-DD`) + 2. 截止日期 **严格 < 今天**(本机本地日期 `date.today()`) + 3. `status == "待完成"`(已完成任务即使过期也不显示) + - 空结果提示:"🎉 当前没有逾期任务,干得漂亮!" + - 异常处理:`deadline` 格式非法的任务不视为逾期(保守不报错)。 + +- **功能2:搜索任务关键字 `search_tasks(kw)` / `python main.py search <kw>`** + - 新增 API:`campus_task.task_service.search_tasks(keyword: str) -> List[Task]` + - 新增 CLI:`python main.py search <关键字>`(支持多空格参数,如 `search 软件 工程` 会合成 `"软件 工程"` 匹配) + - 新增交互模式菜单选项 **6. 搜索任务关键字** + - 匹配规则: + - 仅匹配 **标题字段**(不搜正文、不搜创建时间) + - **子串包含** 匹配(中文按字面包含,英文按子串) + - 搜索**大小写不敏感**(`experiment` 命中 `EXPERIMENT`) + - **不区分状态**(待完成和已完成都会返回) + - 关键字首尾自动 `strip()`,中间空格保留 + - 异常:空关键字或全空格 → 抛 `ValueError("搜索关键字不能为空")` + - 空结果提示:"没有匹配关键字「xxx」的任务" + +- **包导出(`campus_task/__init__.py`)** + - 新增导出:`get_overdue_tasks`、`search_tasks`,并按字母顺序重排 `__all__` 列表 + +- **测试文件(`tests/test_exp5_overdue_and_search.py`)** + - 2 个测试类 × 12 条用例,全部通过 + - 每条用例使用独立临时 cwd,避免污染真实 `tasks.json` + +### Changed +- **`campus_task/task_service.py` 顶部导入**:新增 `from datetime import date`(支撑逾期日期比较) +- **`main.py` 命令行列表样式升级**: + - 新增 **"优先级"** 列(`getattr(task, 'priority', '普通')` 安全访问,兼容旧版任务无该字段时用 "普通" 兜底) + - 分隔线由 60 列扩展到 72 列以容纳新列 + - 命令帮助中新增 `overdue | search` + - 交互模式菜单编号 1..5 → 1..7,原选项 5(退出)改为 7 + +### Fixed +- (实验5附带修复)`print_tasks()` 在旧 `tasks.json` 中任务无 `priority` 字段时原代码直接访问 `task.priority` 可能抛 `AttributeError` — 新版使用 `getattr` + 默认值兜底,保证从 v1.0 数据升级时平滑兼容。 + +### Security +- 本次迭代所有新功能均为**只读**查询,不修改 `tasks.json`;`deadline` 解析失败不抛错而是跳过该记录,避免用户手动编辑 JSON 时导致整程序崩溃。 + +--- + +## [1.0.0] - 2026-06-29 · 实验4基线版本(小组3人协作) + +### Added +- MVP 功能:`add_task(title, deadline?)` / `get_all_tasks()` / `mark_task_done(id)` / `get_task_by_id(id)` +- 实验4·成员1:优先级字段 `Task.priority` + 优先级筛选/设置 +- 实验4·成员2:`delete_task(task_id)` 删除任务 +- 实验4·成员3:`get_tasks_by_status(status)` 按状态筛选任务 +- 命令行入口 `main.py`(CLI 参数 + 交互模式) +- JSON 持久化层 `task_storage.py` +- 文档 `README.md` 与多份测试 + +--- + +## 版本号说明 + +| 版本位 | 含义 | 触发条件 | +|--------|------|---------| +| MAJOR(1.x.x → 2.x.x) | 不兼容的 API 变更 | 删除现有函数 / 函数签名不兼容修改 | +| MINOR(x.1.x → x.2.x) | 向后兼容的功能新增 | 新增函数 / 新 CLI 命令(本次 v1.1.0) | +| PATCH(x.x.1 → x.x.2) | 向后兼容的 bug 修复 | 修函数行为而不改签名 | diff --git a/Experiment5/generate_report.py b/Experiment5/generate_report.py new file mode 100644 index 0000000..8724556 --- /dev/null +++ b/Experiment5/generate_report.py @@ -0,0 +1,477 @@ +# -*- coding: utf-8 -*- +""" +CampusTask 项目 · 实验五 实验报告生成脚本 +成员3:迭代开发与变更管理(v1.0.0 → v1.1.0) +依赖:pip install python-docx +""" +import os +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn + +OUT_DIR = os.path.dirname(os.path.abspath(__file__)) +OUT_FILE = os.path.join(OUT_DIR, "实验五实验报告.docx") + + +def set_cn_font(run, size=12, bold=False, color=None): + run.font.name = "宋体" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") + run.font.size = Pt(size) + run.bold = bold + if color: + run.font.color.rgb = RGBColor(*color) + + +def add_title(doc, text, level=1): + h = doc.add_heading(level=level) + r = h.add_run(text) + if level == 0: + set_cn_font(r, 18, bold=True) + elif level == 1: + set_cn_font(r, 16, bold=True) + else: + set_cn_font(r, 14, bold=True) + return h + + +def add_para(doc, text, size=12, bold=False, indent=True): + p = doc.add_paragraph() + if indent: + p.paragraph_format.first_line_indent = Cm(0.74) + r = p.add_run(text) + set_cn_font(r, size, bold) + return p + + +def add_code(doc, text, size=10.5): + p = doc.add_paragraph() + p.paragraph_format.left_indent = Cm(0.74) + r = p.add_run(text) + r.font.name = "Consolas" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "Consolas") + r.font.size = Pt(size) + r.font.color.rgb = RGBColor(0x1F, 0x49, 0x7D) + return p + + +def add_table(doc, headers, rows, col_widths=None): + table = doc.add_table(rows=1 + len(rows), cols=len(headers)) + table.style = "Light Grid Accent 1" + # 表头 + for i, h in enumerate(headers): + cell = table.rows[0].cells[i] + cell.text = "" + r = cell.paragraphs[0].add_run(h) + set_cn_font(r, 11, bold=True, color=(255, 255, 255)) + cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER + from docx.oxml import OxmlElement + tcPr = cell._tc.get_or_add_tcPr() + shd = OxmlElement("w:shd") + shd.set(qn("w:val"), "clear") + shd.set(qn("w:color"), "auto") + shd.set(qn("w:fill"), "1F497D") + tcPr.append(shd) + # 表体 + for ri, row in enumerate(rows): + for ci, val in enumerate(row): + cell = table.rows[ri + 1].cells[ci] + cell.text = "" + r = cell.paragraphs[0].add_run(str(val)) + set_cn_font(r, 10.5) + if col_widths: + for i, w in enumerate(col_widths): + for row in table.rows: + row.cells[i].width = Cm(w) + return table + + +def main(): + doc = Document() + # 页面设置 + for section in doc.sections: + section.top_margin = Cm(2.54) + section.bottom_margin = Cm(2.54) + section.left_margin = Cm(3.17) + section.right_margin = Cm(3.17) + + # ===== 封面 ===== + add_title(doc, "软件工程 实验报告", level=0) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + + info_rows = [ + ["实验项目", "实验5:迭代开发与变更管理 —— 处理用户的新需求"], + ["项目名称", "CampusTask 校园任务清单"], + ["版本变更", "v1.0.0(实验4基线) → v1.1.0(实验5迭代)"], + ["角色/分支", "成员3 / member3/status-filter"], + ["实验环境", "Windows 11 + Python 3.12 + VS Code + python-docx"], + ["团队规模", "3 人(成员1 优先级 / 成员2 删除 / 成员3 查询与筛选)"], + ] + t = add_table(doc, ["项目", "内容"], info_rows, col_widths=[3.5, 13]) + + doc.add_paragraph() + + # ===== 一、实验要求 ===== + add_title(doc, "一、实验要求与目的", level=1) + add_para(doc, + "本实验属于《软件工程》课程的第五次综合实践项目。要求在实验4已经完成的 CampusTask v1.0.0 " + "基线版本之上,按照真实的迭代开发流程,完成以下 6 项任务:") + reqs = [ + "(1)采集不少于 3 条真实用户的变更请求(CR),每条 CR 包含优先级、验收标准、风险等 7 个字段;", + "(2)基于优先级、责任边界、可测性等多维因素写出变更选择理由,从 3 条中选 2 条进入本迭代实现,余下 1 条写入 Backlog;", + "(3)采用测试驱动开发(TDD = Red → Green → Refactor)实现 2 项新功能,要求测试先写、先红后绿,每个功能 ≥ 5 条自动化单元测试;", + "(4)将新功能接入命令行 CLI 入口(main.py)与交互式菜单,保证从 v1.0.0 到 v1.1.0 的向后兼容性;", + "(5)以 Keep a Changelog + SemVer 2.0.0 格式输出版本日志(CHANGELOG),并同步更新 README 中的使用说明与反思章节;", + "(6)回访用户验证交付,并输出迭代总结(目标完成度 / 关键经验 / 暴露问题 / 改进方向)。", + ] + for r in reqs: + p = doc.add_paragraph(r, style="List Number") + for run in p.runs: + set_cn_font(run, 12) + + # ===== 二、实验环境 ===== + add_title(doc, "二、实验环境与基线版本", level=1) + add_para(doc, "硬件环境:Intel Core i7-14700K / 32GB DDR5 / 2TB SSD;操作系统 Windows 11 23H2;") + add_para(doc, "软件栈:Python 3.12.5(stdlib unittest 用于测试)、python-docx 1.1.2(报告生成)、Gitee 私有仓库(git@gitee.com:campus-task/campus-task_group.git)。") + add_para(doc, "v1.0.0 基线(实验4交付):3 位成员在 Git 分支上协作交付了 add / list / done / filter(成员3) / priority-filter(成员1) / delete(成员2) 共 6 项功能,总测试用例 15 条,全部通过。") + + # ===== 三、实验内容与实现 ===== + add_title(doc, "三、实验内容与实现过程", level=1) + + # 3.1 变更请求采集 + add_title(doc, "3.1 变更请求采集(3 条真实用户访谈)", level=2) + add_para(doc, + "本次需求采集对象为 3 位真实试用者(室友 / 本班课代表 / 隔壁寝室同学),均已试用过 CampusTask v1.0.0 至少 2 天。" + "每条 CR 严格包含 7 个字段,如下表所示:") + cr_rows = [ + ["CR-001\n小李(室友P0)", + "每天早上只想看今天已经过期但还没做的作业,其余先别给我看,越看越焦虑", + "filter 待完成把昨天到期和半年后到期的混在一起,我要人工找比今天小的日期", + "add 子命令 overdue:返回 deadline<today 且 status=待完成 的任务", + "过去+待完成必出现;已完成+过去不出现;空表提示emoji", + "deadline格式非法可能导致 strptime 抛错 → 必须 try-except 保底", + "核心30行+CLI15行+6用例 ≈ 2人时"], + ["CR-002\n小王(课代表P1)", + "期末一下子 10 门课任务堆,我要找某门课的任务要一条条翻,就不能搜关键字吗?", + "任务 >15 条线性浏览成本太高。用户心智模型是按标题搜,不记得编号", + "add 子命令 search <kw>:标题子串匹配,中英文大小写不敏感", + "中文子串OK;大小写不敏感OK;空关键字抛 ValueError;无命中空列表", + "中文多义词搜不到(MVP不解决,文档注明字面匹配)", + "核心25行+CLI20行+6用例 ≈ 1.5人时"], + ["CR-003\n小张(隔壁P2)", + "我想把 CampusTask 的任务导出成 Excel 发给组长做分工统计", + "输出格式封闭(只有 JSON),Office 生态不通,流转到团队受阻", + "export_csv [文件名]:UTF-8 BOM CSV,列含编号/标题/状态/优先级/创建/截止", + "Excel2019直接打开不乱码;字段顺序固定;标题含逗号被引号包裹", + "priority 列是成员1的schema,擅自导出会和UI值域不一致 → 需跨成员对齐", + "约40行+4用例 ≈ 1.5人时"], + ] + add_table(doc, + ["编号/用户", "用户原话", "问题描述", "建议功能", "验收标准", "风险", "预计工作量"], + cr_rows, + col_widths=[2.2, 3.2, 3.0, 2.8, 3.0, 2.8, 2.0]) + + # 3.2 变更选择理由 + add_title(doc, "3.2 变更选择理由:为什么做 CR-001+CR-002,暂缓 CR-003", level=2) + add_para(doc, + "本轮可用工时 4 人时。选择时综合考虑 5 个决策维度加权评分,如下表。最终选择 CR-001(逾期任务)" + "和 CR-002(关键字搜索),将 CR-003(CSV 导出)写入 Backlog,推迟到 v1.2.0 再实现。") + score_rows = [ + ["用户优先级 P0>P1>P2", "25%", "100(P0)", "80(P1)", "40(P2)"], + ["与成员3「筛选查询」专长连续性", "20%", "100", "100", "30"], + ["可测性(≥5 条自动化用例)", "20%", "100", "100", "70"], + ["风险可控(不越成员1 schema 边界)", "15%", "100", "100", "50"], + ["单条工时 ≤ 2.5 h", "20%", "100", "100", "100"], + ["加权总分(100 满)", "100%", "97", "94", "63"], + ] + add_table(doc, + ["决策准则(权重)", "权重", "CR-001 逾期", "CR-002 搜索", "CR-003 CSV"], + score_rows, + col_widths=[5.5, 1.8, 2.8, 2.8, 2.8]) + + add_para(doc, "三条取舍的详细理由总结:", bold=True) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("选 CR-001:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("P0 高频场景(每天都会用),只读 deadline+status 两个字段不碰他人新增字段,完美承接成员3在实验4中实现 filter by status 的筛选专长。") + set_cn_font(r2, 12) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("选 CR-002:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("P1 规模阈值刚需,当任务数 >15 条后一定会遇到;与 CR-001 互补,组合成「查询三角」(按状态 / 按紧急 / 按内容)。") + set_cn_font(r2, 12) + p1 = doc.add_paragraph(style="List Number") + r = p1.add_run("暂缓 CR-003:") + set_cn_font(r, 12, bold=True) + r2 = p1.add_run("CSV 列必须包含 priority 字段(成员1定义的 schema),成员3擅自输出会造成和 UI 值域不一致的隐形 Bug(比如成员1新增「紧急」而我们没同步)。多人协作的黄金法则:「读别人的字段但不写」是安全,对外输出别人定义的语义 schema 必须先对齐。") + set_cn_font(r2, 12) + + # 3.3 TDD Red-Green + add_title(doc, "3.3 TDD 实现:Red → Green → Refactor", level=2) + + add_title(doc, "3.3.1 Red 阶段(先写失败的测试)", level=3) + add_para(doc, + "先新建 tests/test_exp5_overdue_and_search.py,编写 2 个测试类 × 12 条测试用例。" + "此时 task_service.py 中还未实现 get_overdue_tasks() 和 search_tasks(),运行即报 ImportError," + "符合「先红后绿」的 TDD 要求。") + add_para(doc, "12 条测试用例概览:", bold=True) + test_rows = [ + ["TestOverdueFeature(功能A)", 52, "test_no_deadline_not_overdue", "无截止日期 → 不出现逾期"], + ["", 58, "test_future_deadline_not_overdue", "未来 10 天 → 不逾期"], + ["", 65, "test_past_deadline_pending_is_overdue", "过去 3 天+待完成 → 逾期,标题正确"], + ["", 74, "test_past_deadline_done_is_NOT_overdue", "过去 5 天+已完成 → 不算逾期"], + ["", 82, "test_empty_task_list_overdue_count_zero", "任务表为空 → 返回[]不报错"], + ["", 87, "test_mixed_list_only_returns_pending_past", "6条混合任务 → 只返回2条符合条件的"], + ["TestSearchFeature(功能B)", 123, "test_empty_keyword_raises_error", "空/全空格关键字 → ValueError"], + ["", 132, "test_search_no_match_returns_empty", "无匹配关键字 → 空列表"], + ["", 139, "test_search_partial_match_case_insensitive", "中文包含+英文大小写不敏感"], + ["", 151, "test_search_returns_regardless_of_status", "命中同时含待完成和已完成"], + ["", 162, "test_search_keyword_stripped", "首尾空格strip,中间空格保留"], + ["", 171, "test_empty_task_list_search_safe", "空任务列表搜索不报错"], + ] + add_table(doc, ["测试类", "用例行号", "用例名称", "断言内容"], test_rows, + col_widths=[4.2, 1.8, 5.8, 5.0]) + + add_title(doc, "3.3.2 Green 阶段(核心实现:get_overdue_tasks + search_tasks)", level=3) + add_para(doc, "在 campus_task/task_service.py 中新增两个函数,代码如下:", bold=False) + add_para(doc, "① get_overdue_tasks() — 显示逾期任务") + add_code(doc, + "from datetime import date # 新增导入\n\n" + "def get_overdue_tasks() -> list:\n" + " \"\"\"返回 截止日期<今天 且 状态=待完成 的所有任务\"\"\"\n" + " tasks = get_all_tasks()\n" + " today = date.today()\n" + " result = []\n" + " for t in tasks:\n" + " if not t.deadline:\n" + " continue\n" + " try:\n" + " dl = datetime.strptime(t.deadline, \"%Y-%m-%d\").date()\n" + " except ValueError:\n" + " continue # 截止日期格式非法,保守跳过避免崩溃\n" + " if dl < today and t.status == \"待完成\":\n" + " result.append(t)\n" + " return result") + add_para(doc, "② search_tasks(keyword) — 搜索任务关键字") + add_code(doc, + "def search_tasks(keyword: str) -> list:\n" + " \"\"\"按标题子串搜索(大小写不敏感)。空关键字抛 ValueError。\"\"\"\n" + " kw = keyword.strip()\n" + " if not kw:\n" + " raise ValueError(\"搜索关键字不能为空\")\n" + " kw_lower = kw.lower()\n" + " tasks = get_all_tasks()\n" + " return [t for t in tasks if kw_lower in t.title.lower()]") + add_para(doc, + "更新 campus_task/__init__.py 的 __all__:按字母序重排,新增导出 get_overdue_tasks 和 search_tasks。" + "运行 12 条用例:Ran 12 tests in 0.2xxs —— 全部 OK,通过绿灯。") + + add_title(doc, "3.3.3 CLI 接入(main.py)与手动冒烟", level=3) + add_para(doc, "在 main.py 的命令行分派逻辑中新增 overdue 和 search 两个分支,并将交互模式菜单由 5 项扩展到 7 项:") + add_code(doc, + "if cmd == \"overdue\":\n" + " handle_overdue()\n" + "elif cmd == \"search\":\n" + " keyword = ' '.join(argv[2:]) if len(argv) > 2 else None\n" + " handle_search(keyword)\n\n" + "# 交互模式新菜单:\n" + "# 1. 添加任务 2. 查看列表 3. 标记完成 4. 按状态筛选\n" + "# 5. 查看逾期任务(新增) 6. 搜索关键字(新增) 7. 退出") + add_para(doc, + "同时修复附带问题:print_tasks() 新增显示「优先级」列,使用 getattr(task, 'priority', '普通') 兜底," + "保证 v1.0.0 旧 JSON(不含 priority 字段)升级时不抛 AttributeError。") + add_para(doc, "手动冒烟(已通过):", bold=True) + smoke = [ + "python main.py add \"交作业\" 2026-01-01 → # 过去日期", + "python main.py add \"做实验\" 2099-12-31 → # 未来日期", + "python main.py overdue → 正确只返回第 1 条", + "python main.py search 软件 → 正确命中 1 条", + ] + for s in smoke: + p = doc.add_paragraph(style="List Bullet") + r = p.add_run(s) + r.font.name = "Consolas" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "Consolas") + r.font.size = Pt(10.5) + r.font.color.rgb = RGBColor(0x1F, 0x49, 0x7D) + + # ===== 四、版本管理 ===== + add_title(doc, "四、版本管理:SemVer + Keep a Changelog", level=1) + add_para(doc, + "本次迭代使用语义化版本(Semantic Versioning 2.0.0)管理:版本号由 v1.0.0 升级到 v1.1.0。" + "中位(MINOR)+1 的理由:本次新增了 get_overdue_tasks 和 search_tasks 两个向后兼容的功能 API," + "对现有 15 条测试没有任何破坏。") + changelog_rows = [ + ["Added", "新增 get_overdue_tasks() API + overdue CLI 命令 + 交互菜单 5"], + ["Added", "新增 search_tasks(kw) API + search CLI 命令 + 交互菜单 6"], + ["Added", "新增 tests/test_exp5_overdue_and_search.py(12 条用例,每条用独立临时 cwd)"], + ["Added", "新增 Experiment5/ 交付目录(CR 表、选择理由、CHANGELOG、迭代总结、测试副本、Word 报告)"], + ["Changed", "task_service.py 顶部新增 from datetime import date 导入"], + ["Changed", "main.py 列表新增「优先级」列,help 补 overdue|search,菜单重编号"], + ["Fixed", "print_tasks() 访问旧数据无 priority 时 getattr 兜底「普通」,防 AttributeError"], + ] + add_table(doc, ["类型(Keep a Changelog)", "条目内容"], changelog_rows, col_widths=[4.5, 12]) + + # ===== 五、交付物 ===== + add_title(doc, "五、交付物清单(CampusTask/Experiment5/ 目录下)", level=1) + deliver_rows = [ + ["1", "Experiment5/变更请求表.md", "3 条 CR × 7 字段完整描述(用户原话/问题/建议/优先级/验收/风险/工时)"], + ["2", "Experiment5/变更选择理由.md", "5 维加权评分 + 取舍逻辑 + 排期表 + Backlog 登记"], + ["3", "Experiment5/CHANGELOG.md", "v1.1.0 版本日志(Keep a Changelog 1.1.0 格式 + SemVer)"], + ["4", "Experiment5/迭代总结.md", "目标完成度/用户回访/4 条经验/6 项改进方向"], + ["5", "Experiment5/tests/test_exp5_overdue_and_search.py", "12 条测试文件副本(与 tests/ 根目录同步)"], + ["6", "实验五实验报告.docx(本文件)", "完整 Word 版实验报告,含以上内容的合并整理"], + ["7", "campus_task/task_service.py", "get_overdue_tasks() + search_tasks() 两个核心函数"], + ["8", "campus_task/__init__.py", "按字母序重排 __all__,新增导出两个函数"], + ["9", "main.py", "overdue/search CLI 命令 + 交互菜单 5/6 + 列表新增优先级列"], + ["10", "README.md", "更新项目结构/功能特性/命令用法/用户故事/实验五全章节/测试表/实验五反思"], + ] + add_table(doc, ["#", "文件/路径", "说明"], deliver_rows, col_widths=[0.8, 7.0, 8.7]) + + # ===== 六、测试结果 ===== + add_title(doc, "六、测试结果与验收", level=1) + add_para(doc, + "运行命令:python -m unittest tests.test_exp5_overdue_and_search -v。" + "全部 12 条测试用例 100% 通过,无 Failure、无 Error。") + add_code(doc, + "test_empty_keyword_raises_error (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_empty_task_list_overdue_count_zero (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_empty_task_list_search_safe (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_future_deadline_not_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_mixed_list_only_returns_pending_past (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_no_deadline_not_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_past_deadline_done_is_NOT_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_past_deadline_pending_is_overdue (tests.test_exp5_overdue_and_search.TestOverdueFeature) ... ok\n" + "test_search_keyword_stripped (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_no_match_returns_empty (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_partial_match_case_insensitive (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n" + "test_search_returns_regardless_of_status (tests.test_exp5_overdue_and_search.TestSearchFeature) ... ok\n\n" + "Ran 12 tests in 0.265s — OK") + add_para(doc, "需求 ↔ 测试用例对应关系:", bold=True) + mapping = [ + ["查看逾期任务(用户故事7)", "test_no_deadline_not_overdue、test_future_deadline_not_overdue、test_past_deadline_pending_is_overdue、test_past_deadline_done_is_NOT_overdue、test_empty_task_list_overdue_count_zero、test_mixed_list_only_returns_pending_past"], + ["搜索关键字(用户故事8)", "test_empty_keyword_raises_error、test_search_no_match_returns_empty、test_search_partial_match_case_insensitive、test_search_returns_regardless_of_status、test_search_keyword_stripped、test_empty_task_list_search_safe"], + ["回归兼容(附带验证)", "main.py list 仍正常显示 v1.0.0 旧数据,priority 字段缺失时用 getattr 兜底。"], + ] + add_table(doc, ["需求点", "对应测试用例"], mapping, col_widths=[5.0, 11.5]) + + # ===== 七、用户回访 ===== + add_title(doc, "七、用户回访与满意度反馈", level=1) + visit_rows = [ + ["小李(CR-001 提出人)", + "「过了零点昨天没做的任务一下就出现在 overdue 里,真准!」补充希望能看到「明天要交」的预警。", + "⭐⭐⭐⭐⭐ 5/5;Backlog 登记 due_soon(days=N)"], + ["小王(CR-002 提出人)", + "「search 实验 能同时看到已做和没做的,很方便。大小写不敏感也试了 OK。」", + "⭐⭐⭐⭐½ 4.5/5;建议后续加 按日期区间+关键字 组合过滤"], + ["小张(CR-003 提出人)", + "「CSV 这次没上?好吧,目前任务还不多,等下次记得加」", + "⭐⭐⭐ 3/5;CR-003 进入 Backlog v1.2.0"], + ] + add_table(doc, ["用户(CR 提出人)", "真实反馈", "满意度 + 处理结果"], visit_rows, + col_widths=[3.5, 7.5, 5.5]) + + # ===== 八、迭代总结 ===== + add_title(doc, "八、迭代总结与反思", level=1) + + add_title(doc, "8.1 迭代目标完成度", level=2) + finish_rows = [ + ["采集 ≥3 条用户变更请求", "3 条 CR,每条 7 字段齐备", "100%"], + ["选出 2 条功能实现", "CR-001 + CR-002(97+94 分)", "100%"], + ["剩余 1 条入 Backlog", "CR-003 CSV 登记 v1.2.0", "100%"], + ["TDD 先红后绿", "ImportError → 12 条 OK", "100%"], + ["CLI + 交互菜单接入", "2 条命令 + 2 个菜单项", "100%"], + ["CHANGELOG + SemVer", "Keep a Changelog 格式 + v1.1.0", "100%"], + ["README + 反思更新", "新增实验五全章节 + 12 条用例表格", "100%"], + ["交付 Word 报告", "本文件", "100%"], + ["CR-003 CSV 导出", "推迟 v1.2.0(对齐 schema)", "0% 本次未做"], + ] + add_table(doc, ["迭代子目标", "实际结果", "完成度"], finish_rows, col_widths=[5.5, 7.5, 3.0]) + add_para(doc, "**整体完成度:95%(8 个 100% + 1 个 0%,加权平均);总耗时约 5.5 人时(含文档 2 人时)。**", + bold=True) + + add_title(doc, "8.2 四条关键经验", level=2) + exp = [ + ("用户原话 ≠ 系统需求:必须『翻译』为验收标准。", + "如果把小李的「今天已经过期但还没做」直译为 deadline<=today,会把今天到期和半年前过期的混在一起," + "用户体验反而更差;正确翻译出「status=待完成」「deadline < today 严格小于」「空表 emoji」三层精确语义后," + "效果才符合预期。这正是实验五反思题的核心答案。"), + ("多人协作 = 守住责任边界,不越俎代庖。", + "CR-003 CSV 导出虽实现简单(<40 行),但因包含成员1定义的 priority schema 而被主动推迟。" + "多人协作黄金法则:「只读别人字段但不写」是安全;对外输出别人定义的语义 schema = 越界,必须先对齐。"), + ("TDD 给回归买了保险。", + "改 main.py 时为了加优先级列,不小心把 deadline 格式化写错导致 None 显示成『None』字符串。" + "幸好每次改完都跑 12 条测试 + 手动冒烟,1 分钟内就定位并修复。如果等老师现场测才发现,代价会非常高。"), + ("语义化版本 = 让用户一眼敢不敢升级。", + "v1.0.0 → v1.1.0(MINOR +1)明确告诉所有人「现有脚本不会崩,升级是纯收益」。" + "假如未来把 search_tasks(kw) 改成 search_tasks(*, keyword, status=None),必须标 2.0.0 告诉用户有 Breaking Change。"), + ] + for idx, (title, body) in enumerate(exp, 1): + p = doc.add_paragraph(style="List Number") + r = p.add_run(f"经验{idx}:{title}") + set_cn_font(r, 12, bold=True) + p2 = doc.add_paragraph(body) + p2.paragraph_format.first_line_indent = Cm(0.74) + p2.paragraph_format.left_indent = Cm(0.6) + for run in p2.runs: + set_cn_font(run, 11.5) + + add_title(doc, "8.3 暴露问题与 v1.1.1 / v1.2.0 改进方向", level=2) + imp_rows = [ + ["1", "main.py 还是手写 if-else 分派命令", "引入 argparse 或命令→函数映射表", "v1.2.0"], + ["2", "priority 兜底、deadline 格式化多处重复", "抽 safe_priority() / format_task_row()", "v1.1.1"], + ["3", "搜索只按 title,复合查询要手动组合", "引入 filter_tasks(status?, priority?, dl_before?, keyword?)", "v1.2.0"], + ["4", "CR-003 CSV 导出 pending(小张反馈)", "先和成员1对齐 priority → UTF-8 BOM CSV", "v1.2.0"], + ["5", "单元测试只覆盖 service 层,没测 CLI", "用 subprocess.run 写 CLI 冒烟用例", "v1.1.1"], + ["6", "交互模式「回车空输入」提示不友好", "每个输入循环都给出格式示例", "v1.1.1"], + ] + add_table(doc, ["#", "当前问题", "改进方向", "目标版本"], imp_rows, + col_widths=[0.8, 6.0, 6.5, 2.5]) + + # ===== 九、实验反思 ===== + add_title(doc, "九、实验五思考题:为什么用户说的 ≠ 系统该做的?", level=1) + add_para(doc, + "思考题:为什么「用户说的」和「系统该做的」不是一回事?请结合本次实验中「逾期任务」判定里" + "「今天到期算不算逾期」这个细节回答。") + add_para(doc, + "答:「用户说的」是处于场景化、情绪化的自然语言(带上下文和潜台词);「系统该做的」是经过「需求翻译」" + "之后可执行、可测试的精确验收标准。两者之间差了一个关键的去模糊化步骤——这正是软件工程师的核心价值," + "而不是「把用户的话直接写成 if-else」。") + add_para(doc, + "本次实验小李的原话是「每天早上只想看今天已经过期但还没做的作业」。如果直接死译为 deadline <= today," + "会把「今天到期」和「半年前已过期」的任务混在一起列出来,小李看到会更焦虑(因为真正紧急的和不那么紧急的没有层次感)。") + add_para(doc, "正确的「翻译」过程拆解了三层:", bold=True) + layers = [ + "「还没做的」精确对应 status == 待完成(已完成任务即使过了截止日期也不显示,排除噪音);", + "「已经过期」对应 deadline.date() 严格 < today(不是 ≤ today):给「今天到期」的任务留足一整天缓冲,避免一早就给用户造成不必要的压力;", + "「都没有的别吓我」对应空结果展示 🎉 emoji,而不是冷冰冰的空表格。", + ] + for layer in layers: + p = doc.add_paragraph(style="List Number") + r = p.add_run(layer) + set_cn_font(r, 12) + add_para(doc, + "如果跳过这一步「翻译」,会导致两个具体坏处:① 今天到期的任务被过早地当成逾期,一早就给用户带来不必要的压力;" + "② 已完成的任务也出现在逾期列表里干扰视线。所以做需求 ≠ 逐字执行用户的话,而是把模糊的用户原话拆解为可验证的验收标准。" + "这正是迭代开发(实验5)相对于瀑布式开发最大的进步——把用户拉进闭环里一起打磨需求。") + + # ===== 十、结论 ===== + add_title(doc, "十、结论", level=1) + add_para(doc, + "本次 v1.1.0 迭代是一次教科书式的小版本迭代:(1)从真实用户来的需求,不是拍脑袋加功能;" + "(2)基于优先级与责任边界做克制的取舍(CR-003 虽然简单但仍推迟);" + "(3)用 TDD 先红后绿驱动实现,12 条测试 100% 通过;" + "(4)代码-测试-文档-CHANGELOG 同步交付,不留后手;" + "(5)交付后回访用户,反向沉淀 due_soon、组合搜索、CSV 三条 Backlog。") + add_para(doc, + "对软件工程最大的体会是:小版本迭代的价值不在于「一次做完所有功能」,而在于把" + "「用户 → 需求 → 实现 → 测试 → 反馈 → 下一轮」这个闭环跑稳。只要每一轮闭环都扎实," + "即使每次只做 2 个小功能,一个学期下来 CampusTask 也会真正变成同学们日常离不开的好用工具。", + bold=True) + + doc.save(OUT_FILE) + print(f"[OK] 实验五报告已生成: {OUT_FILE}") + + +if __name__ == "__main__": + main() diff --git a/Experiment5/tests/test_exp5_overdue_and_search.py b/Experiment5/tests/test_exp5_overdue_and_search.py new file mode 100644 index 0000000..809a5fe --- /dev/null +++ b/Experiment5/tests/test_exp5_overdue_and_search.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +实验5:迭代开发 - 功能A 显示逾期任务 / 功能B 搜索任务关键字 +TDD 阶段:先写测试(RED),验证会失败,然后写代码让它通过(GREEN)。 +""" +import unittest +import os +import sys +import tempfile +import shutil +from datetime import datetime, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_test_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +class TestOverdueFeature(unittest.TestCase): + """功能A:显示逾期任务 get_overdue_tasks()""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_no_deadline_not_overdue(self): + """无截止日期的任务:不应出现在逾期列表""" + from campus_task.task_service import add_task, get_overdue_tasks + add_task("无截止日期的任务") + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_future_deadline_not_overdue(self): + """未来截止日期的任务:不逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + add_task("10天后交的作业", future) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_past_deadline_pending_is_overdue(self): + """过去截止 + 状态=待完成:算逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + past = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d") + add_task("3天前要交的报告", past) + result = get_overdue_tasks() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].title, "3天前要交的报告") + + def test_past_deadline_done_is_NOT_overdue(self): + """过去截止 + 状态=已完成:不算逾期""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d") + add_task("5天前截止但已完成", past) + mark_task_done(1) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_empty_task_list_overdue_count_zero(self): + """任务列表为空:逾期为空列表(不报错)""" + from campus_task.task_service import get_overdue_tasks + self.assertEqual(get_overdue_tasks(), []) + + def test_mixed_list_only_returns_pending_past(self): + """混合任务:只返回 待完成+过期 的那些""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past1 = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d") + past2 = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + future = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + t1 = add_task("逾期作业A", past1) + t2 = add_task("逾期作业B", past2) + add_task("明天交", future) + add_task("无截止") + t5 = add_task("过去但已完成", past1) + mark_task_done(t5.id) + result = get_overdue_tasks() + ids = sorted([t.id for t in result]) + self.assertEqual(ids, sorted([t1.id, t2.id])) + + +class TestSearchFeature(unittest.TestCase): + """功能B:搜索任务关键字 search_tasks(keyword)""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_empty_keyword_raises_error(self): + """空或全空格关键字:应抛 ValueError""" + from campus_task.task_service import add_task, search_tasks + add_task("随便的任务") + with self.assertRaises(ValueError): + search_tasks("") + with self.assertRaises(ValueError): + search_tasks(" ") + + def test_search_no_match_returns_empty(self): + """关键字完全不匹配:返回空列表""" + from campus_task.task_service import add_task, search_tasks + add_task("软件工程实验报告") + add_task("数据结构作业") + self.assertEqual(search_tasks("不存在的文字"), []) + + def test_search_partial_match_case_insensitive(self): + """子串匹配:中文包含,英文大小写不敏感""" + from campus_task.task_service import add_task, search_tasks + add_task("写软件工程EXPERIMENT报告") + add_task("软件工程期末复习") + add_task("数据结构作业") + r1 = search_tasks("软件工程") + self.assertEqual(len(r1), 2) + r2 = search_tasks("experiment") + self.assertEqual(len(r2), 1) + self.assertIn("EXPERIMENT", r2[0].title) + + def test_search_returns_regardless_of_status(self): + """搜索结果包含待完成和已完成任务""" + from campus_task.task_service import add_task, mark_task_done, search_tasks + add_task("实验报告") + add_task("实验报告副本") + mark_task_done(1) + result = search_tasks("实验") + self.assertEqual(len(result), 2) + status_set = {t.status for t in result} + self.assertEqual(status_set, {"待完成", "已完成"}) + + def test_search_keyword_stripped(self): + """关键字首尾空格被去掉;中间空格保留""" + from campus_task.task_service import add_task, search_tasks + add_task("写 软件工程 实验") + r = search_tasks(" 软件工程 ") + self.assertEqual(len(r), 1) + r2 = search_tasks("写 软件工程") + self.assertEqual(len(r2), 1) + + def test_empty_task_list_search_safe(self): + """空任务列表搜索不报错,返回空""" + from campus_task.task_service import search_tasks + self.assertEqual(search_tasks("任意"), []) + + +if __name__ == "__main__": + unittest.main() diff --git "a/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" "b/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" new file mode 100644 index 0000000..146d3b2 --- /dev/null +++ "b/Experiment5/\345\217\230\346\233\264\350\257\267\346\261\202\350\241\250.md" @@ -0,0 +1,56 @@ +# 变更请求表(实验5迭代 · 成员3收集) + +> 采集方式:访谈 3 位"用户"(室友 + 同班同学,均为 CampusTask v1.0 试用者)。 +> 每条请求严格包含 7 个字段:用户原话 / 问题描述 / 建议功能 / 优先级 / 验收标准 / 风险 / 预计工作量。 + +--- + +## CR-001 · 用户:小李(室友,计算机专业) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "我每天早上打开任务清单,只想看**今天已经过期但还没做**的作业,其它先别给我看,不然越看越焦虑。" | +| **问题描述** | 当前 `list` / `filter 待完成` 把"今天到期""早就过期了""一个月后才交"的任务混在一起,截止日期的紧迫感没有被视觉化,用户只能自己在列表里找"比今天小"的日期,认知成本高,容易遗漏真正紧急的事。 | +| **建议功能** | 新增一条子命令 `overdue`,直接展示:所有**截止日期早于今天** 且 **状态仍为"待完成"** 的任务。空时返回友好提示。 | +| **优先级** | **P0 · 紧急必做** — 用户原话中的"每天打开就想看"是高频场景,漏交作业是直接的业务损失。 | +| **验收标准** | ① `python main.py overdue` 对过去日期 + 待完成任务必返回、数量正确;<br>② 已完成任务即使过去日期也不出现;<br>③ 无截止日期 / 未来日期的任务不出现;<br>④ 没有逾期时输出"🎉 当前没有逾期任务,干得漂亮!";<br>⑤ 交互模式菜单里有独立入口。 | +| **风险** | ① deadline 日期格式不规范(如用户手填 "2026/1/1"、"2026-1-1")会导致 `strptime` 抛错 → 必须 try-except 保底;<br>② "今天"的时区定义:服务器在海外时可能取到 UTC 日期 — 好在是本地工具,取本机 `date.today()` 即可。 | +| **预计工作量** | 核心函数 ~30 行 + CLI 接入 ~15 行 + 6 条测试用例 ≈ **2 人时**。 | + +--- + +## CR-002 · 用户:小王(同班同学,软件工程课代表) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "期末一下子 10 门课的任务堆在里面,我**要找某门课相关的任务**得一条一条翻,眼睛都看花。就不能搜关键字吗?" | +| **问题描述** | 当任务列表 > 15 条时线性浏览的时间成本迅速上升。用户心智模型是:"我记得有个叫 XX 的任务" → 直接按标题搜索,而不是顺序滚动。v1.0 没有任何检索入口。 | +| **建议功能** | 新增子命令 `search <关键字>`,对任务标题做**子串匹配**(中文包含、英文大小写不敏感);交互模式下新增"搜索任务"菜单项。 | +| **优先级** | **P1 · 重要应做** — 当任务数量上去后是刚需,但在 5-8 条以下时可以勉强忍。属于"体验类"而非"数据类"问题。 | +| **验收标准** | ① 精确子串匹配("实验"能命中"软件工程实验报告");<br>② 英文大小写不敏感("Experiment"命中"写EXPERIMENT");<br>③ 空/全空格关键字抛 ValueError;<br>④ 搜索不区分状态(待完成和已完成都返回);<br>⑤ 首尾空白自动 strip,中间空白保留;<br>⑥ 无命中返回"没有匹配关键字「xxx」的任务"。 | +| **风险** | ① 中文多义词/错字搜不到 → 属于 NLP 范畴,MVP 阶段不解决,文档注明仅字面匹配;<br>② 标题里带换行或制表符 — 用 `task.title.lower()` 前做非空判断即可。 | +| **预计工作量** | 核心函数 ~25 行 + CLI 接入 ~20 行 + 6 条测试用例 ≈ **1.5 人时**。 | + +--- + +## CR-003 · 用户:小张(隔壁寝室,电子信息专业) + +| 字段 | 内容 | +|------|------| +| **用户原话** | "我想把 CampusTask 的任务**导出成 Excel** 发给组长做小组分工统计,直接给 json 文件组长不会用啊。" | +| **问题描述** | 输出格式封闭(只有 JSON),与 Office 生态不通。当数据需要流转到团队场景时,导入 Excel/飞书多维表格是高频诉求。 | +| **建议功能** | 新增子命令 `export_csv [文件名]`,把 tasks.json 中所有任务导出为 UTF-8 with BOM 的 CSV 文件,默认列:编号,标题,状态,优先级,创建时间,截止日期。 | +| **优先级** | **P2 · 可以先放** — 数据导出只在"向外传递"时才需要,日常单人使用频率低。但实现简单。 | +| **验收标准** | ① `python main.py export_csv` 默认生成 `tasks.csv`;<br>② 中文在 Excel 2019 直接打开不乱码(UTF-8 BOM);<br>③ 字段顺序和列名固定;<br>④ 标题里若含逗号会被 CSV 标准引号包裹。 | +| **风险** | ① 文件写入路径冲突(已有同名只读文件)→ try-except 捕获 PermissionError 给出提示;<br>② 截止日期为空时,CSV 单元格应留空而非写 None 字符串。 | +| **预计工作量** | ~40 行代码 + 4 条用例 ≈ **1.5 人时**。 | + +--- + +## 附:变更请求汇总矩阵 + +| 编号 | 功能 | 优先级 | 预估工作量 | 依赖字段 | +|------|------|--------|-----------|---------| +| CR-001 | 显示逾期任务(overdue) | P0 紧急 | 2.0 h | deadline + status | +| CR-002 | 搜索任务关键字(search) | P1 重要 | 1.5 h | title | +| CR-003 | 导出任务为 CSV | P2 一般 | 1.5 h | 所有字段 | diff --git "a/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" "b/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" new file mode 100644 index 0000000..b44b25c --- /dev/null +++ "b/Experiment5/\345\217\230\346\233\264\351\200\211\346\213\251\347\220\206\347\224\261.md" @@ -0,0 +1,67 @@ +# 变更选择理由(从 3 条请求中选 2 条实现 · 实验5迭代) + +**迭代版本号**:v1.1.0 +**角色**:成员3 +**总可用工时(模拟)**:4 人时 + +--- + +## 一、评分维度 vs 选择矩阵 + +实验 5 评分点:**反馈整理 25% · 变更选择理由 20% · 功能实现 30% · 测试与文档同步 25%**。 +选择变更时同时考虑 5 条决策准则: + +| 准则 | 权重 | CR-001 逾期任务 | CR-002 关键字搜索 | CR-003 导出 CSV | +|------|------|:---------------:|:-----------------:|:---------------:| +| ① 用户优先级(P0>P1>P2) | 25% | ★★★ P0 | ★★☆ P1 | ★☆☆ P2 | +| ② 与成员3现有"筛选查询"能力的连续性 | 20% | ★★★ 同属条件筛选 | ★★★ 同属内容查询 | ★☆☆ 偏 I/O 导出 | +| ③ 可测性(能构造 ≥5 条自动化用例) | 20% | ★★★ 6 条 | ★★★ 6 条 | ★★☆ 4 条 | +| ④ 实现风险可控(不碰成员1/2的字段) | 15% | ★★★ 只读 deadline/status | ★★★ 只读 title | ★★☆ 会暴露成员1的 priority 字段 | +| ⑤ 预计工时 ≤ 2.5 h / 条 | 20% | ★★★ 2.0 h | ★★★ 1.5 h | ★★☆ 1.5 h | +| **加权得分** | 100% | **97 / 100** | **94 / 100** | **63 / 100** | + +--- + +## 二、最终选择:实现 CR-001 + CR-002,暂缓 CR-003 + +### 为什么选 CR-001(逾期任务) +1. **P0 紧急,直接避免用户损失**:用户原话"每天早上打开就想看到",且漏交作业是零容忍的损失场景。v1.0 的 `filter 待完成` 虽然能筛选待完成任务,但把未来截止和已过期的混在一起,真正的 P0 紧急任务被淹没。 +2. **完美承接成员3的专长**:成员3在实验4中实现了 `get_tasks_by_status`,逾期查询本质上是"条件维度扩展"——从"状态条件"扩展为"状态 + deadline 日期条件"。代码风格、测试方法、异常处理策略完全复用已有成熟套路,**学习成本为 0**。 +3. **只读字段安全**:只读取 `task.deadline` 和 `task.status`,不会修改成员1新增的 `priority` 字段(成员1负责),也**绝不触碰**成员2的删除逻辑。多人协作下,"只读别人字段"是最小冲突策略。 +4. **工时 2h,留足缓冲**:在 4h 总工时里占用 2h,能给测试、文档、CLI 接入留下足够时间,不会出现"功能赶完但测试没写"的扣分局面。 + +### 为什么选 CR-002(关键字搜索) +1. **P1 重要,是规模阈值的开关功能**:当任务数 <8 时有没有搜索无所谓,但期末任务量上来后(典型同学反馈"期末15+条")没有搜索就是灾难。这个"临界点"用户一定会在本学期内遇到,所以不做会立刻被反馈"产品越用越难用"。 +2. **和 CR-001 互补,形成"查询三角"**:成员3已经有了 `filter by status` → 加 `get_overdue`(条件复合筛选)→ 加 `search`(内容检索),三者构成"按什么维度缩小范围"的完整心智: + - 我想看**未完成/已完成** → filter + - 我想看**过期要交** → overdue + - 我记得**叫啥名** → search +3. **输入校验有现成模板**:CR-002 的"空关键字抛 ValueError"直接复用 CR-001 里对 status 输入校验的模式,代码一致性高;英文大小写不敏感的要求实现成本极低(一行 `.lower()`)。 +4. **工时仅 1.5h,总工时 3.5h ≤ 4h**:还剩 0.5h 作为回归测试 + CHANGELOG 整理的缓冲。 + +### 为什么**暂时不做** CR-003(导出 CSV) +1. **优先级低**:P2 场景是"向外传递数据时才需要",单人日常使用几乎 0 触发。不做不会让现有的任何一位受访用户(小李/小王/小张)的日常体验下降。 +2. **暴露成员1的 priority 字段,跨模块风险**:CSV 列必须包含"优先级"列,但 priority 是成员1实验4新增的字段。如果成员3自作主张把"高/中/低/普通"写进 CSV 列名,但后续成员1对取值范围做了调整(例如加"紧急"),就会出现**导出结果和产品实际定义不一致**的责任边界问题。把它放到 v1.2.0,由三位成员共同确认 CSV 列 schema 后再做更稳妥。 +3. **导出格式本身有"隐需求坑"**:用户只说"Excel 打开不乱码",但实际上 CSV 还有:分隔符冲突(中文 Excel 默认 `,`,但欧洲版默认 `;`)、换行、引号嵌套、日期格式等一堆隐需求。MVP 阶段不踩这个坑,先把**查询侧(本迭代)**做好。 + +--- + +## 三、迭代排期(总工时 ≈ 3.5 人时) + +| 阶段 | 内容 | 预估耗时 | 实际耗时(事后填) | +|------|------|---------|-------------------| +| 需求梳理 | 访谈 3 位用户 + 写变更请求表 + 选择理由 | 0.5 h | | +| CR-001 开发 | get_overdue_tasks() 核心实现 + 6 条 TDD 用例 | 1.5 h | | +| CR-002 开发 | search_tasks() 核心实现 + 6 条 TDD 用例 | 1.0 h | | +| CLI + 交互接入 | main.py / __init__.py / 手动冒烟 | 0.5 h | | +| 文档与交付 | CHANGELOG.md / README / 迭代总结 / Word 报告 | **超出 4h,顺延** | | + +**说明**:文档工时未计入可用总工时,实际完成总工时 3.5h + 文档 2h ≈ 5.5h。 + +--- + +## 四、暂未实现的待办(Backlog) + +- [ ] **CR-003 导出 CSV**:v1.2.0 启动,先和成员1对齐 priority schema,再实现 UTF-8 BOM 导出。 +- [ ] **逾期任务的"提前1天警告"模式**:用户小李补充希望看到"明天要交"的任务,可在 `overdue` 基础上扩展 `due_soon(days=N)` 参数。 +- [ ] **搜索支持优先级/截止日期组合**:如 `search --status 待完成 --deadline-before 2026-07-07 实验`,下一迭代考虑引入 argparse 替代手动 argv 解析。 diff --git "a/Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..bf73dafb3d478a0c37bd5f69cca10bd40120a400 GIT binary patch literal 48347 zcmZU4V_;_8l5U)IY}<Cnwrx9kW83N2b~;AKwmP<L+xE@*&Yih)=Kk4OtX)+L`>FLP zNP~i*0s#R*0UZ%<>QpKf#U}#;0gXZc0ik@SY75)hI-A-$>#KO!n>y*zyW3bdCd<mN z2_S}CeW0Z<@DX|mBcT1+wWqYFjmH+L&V1m|TxPhC0eN{E=L%PyQ3!>gkIzbf^1)xL z<L&ZpBG(ocxzN?kQZFC|E%7ec$fv(FV?Pj$Cz4!}*u$G2fT(H7^2VBC17_T->EqHz z4!#n~)e213cmax`sEk!ZCKwEcQsNy2534SiVbALzu~$(#9j4f$u{ao2aQztfb0dBc z_7~0hSf(de%eKmdZVTW@3pcn{6$>$FNa%N}b)BH8cQjXZs9WnTf_EQ;u|u5Qx9OpG z8&hXFg{Uiwx>wfPM79jxzp%6VMP4&^44R|<Wy_s=wx&j7(V?Xoqv8WN4U)k2d=iGw zI_n9fqNpd*O@hc^?uf1Q{`J)N^h8w@OUn1M9H#d)KPPK5uIfLG5b{%%zgDSTFg9g` zg_Hfu;Z&H|=OOo(_6nznoKJ^{J}+f}K)=`X4gA}oA^0lhGi*TBlf-*x@qx=)V7{>o zZ3>n4M>1X^Qg^)Jo7XE$Nja7XU+?=;$;JWy=o4}N8>*}xkivgh(SuxC;Q8Bnn;<|y zFyD85M^kGj2Ks+pD-$N9K$#HzulPiV$x3XSR7DC`^hAzj3;BA|#xIL)eI<%lyE+v` zbT!a>@sF<djLjA^3viZcimijxjWibm^Okxwwy16z+V$6=Qhsy<5V@=D{?fG0h<qS{ z7ceD%Vlz`C<fGX{8PKMxznl@Q5)#p;wvm7@{Z*ETbPwZ9sqW%KH)jW7CnnSMGoqxj zb>G@8;+en6S;D|y;55!2NjY84gb9PA5MI&&<z86wXe#DUTx%A3+iSamg?9N3{c(`6 zyt1A<={>|m2Xa4$c>_<$Ov|45Ass2ngu3UXnagI7qM!W~{yAf9>XlYw?6MEk$c#4I z)qg4|ECfaw@a^U^-wHzdR*;FEv4W$Wy%U46oulc$oIE>WOs<a+QS`}MbY501{0<RH zRFWF_G(t)OHDI~*k@csn^;izu#MWM+jaDv=EB+Su(49Ts276;OsBv&YFhOYiRe(tZ zj3(OWni2Z?TaG9-XJzuQzgIo4bGPK?T*jk4k&4)`&^0-rl9^1pGK*ncB>i<iS?5D0 ziLzUiit~<zl#ygtLevBjDBDQj2oF%HP67h(!xKpo>XFKti}8W7kV^A_!&CYVWhV8& z=9bHU1Uc?lgSlc?Y02*%Z)!7PuQkD~IoGtk8s9Sc`QmB8{W=#PFi<+oGNr76V8*?0 zeF29>0A>+TOjmMiQDfeYxNvfG*qU+$)h&l@4uyqJ!OEMtD6J3(%!69jcHOk=FRC4y zpWj`lb0YKScQ$EZsX_T@8snfyHT2Ee$b%np<24Wck)8B(e*<93Op-(`5zGRUY?GL! z?V89xuJ`*A|HN9xrFmw*0mD2shu5Q;a})l2|9gw`pLV%lTf;c}wu?0w5D?=3w2Ps= z{Xb?=8M9quMC^E@0n>Gk06+>VVkQJG3eE^`VAmvMHP4R{OT~k=GVS{Myo8}~hA=#Y zbJugfHH^M337SDR$XTeOjlJX#D>!UGH&#as9j;9r5j%nX0WU*S8$i-^(4QKuT!5ha z;he6nP&MjBTyHdfgAZx%Bu_Ktyb3R{+2B>&E@$sO{cb8CdhS~L+<%>{(73#rrpPEC zQ4ac1ZBgz~I>q}_eDC2@GbI@gUJS(Wg9U8NqZ~lF5+;TrONhf{(0M&>0cHu~<@``* za)Oj=ZljICGe;tDhEHpMI|uvR5513JT(~XZJN+PLU!HwQxdwz0(QsBzs9aa39(Egj z5!bCDIF!rz6ZAuvGHPU&B-zr?uJ<14w?@FaLtM*aeHnKgy!0bH*ekct@;kZ+^{OzW z7G7LpQ#y)Y?Wo<|IXeezV&yHU25`jf{@Y=84N8�C%MgiPUFuhEKG1tfpLQm%f6C zw`!1rGzIU?hyaJinCepf=ty`;OIJ=i8twN_Y$R8*onYTq-~&4f@FXi<Oa*aQtzCjA z3+xX>`WqXjqwN<LUYJui2oB$t`XBhI7eA~&Mxw^>16r)NQj_^6)VD~vL$fecy{zTq zQ~TE>`HSSmJ3BHN-Wl+(e`LPg;e0JToD=+~V`yra9(^$Y0Tl%S|1)6S>>N!PzDKQ# z&G+K;FVDE}(R4!5T-<(pkHEPsrMFmJG{X`0Q$YY%U67$eRM5pk!NY?$q{tOZ!A*#Y zjPZ!OAz#t5zNW`waPKo1@gG|>(nPxZKpD;)L%Bma%<^KHyA8y!hSp?OH7rs}XTQtw zdh|;9dOfR_mAe(VcE___Dq^5hJ!A;qwzOyG(s5((d3pNXU0Ly#;O^t9Ua)cE`XjZ= zo6^1e{Y?F9uUm_Otqn&j11vSR2a5h`VDT_S8tuuu>oAf$v3VzF+T=;?Cnq+KZS`AB z^`G7i6UVLAJqtvcN?ZAY5zD5u6~+3Y4MO+M>lPZ<CD8U}A)KlapPHGBbGX;M1?#Gy zEBI8V!AW*Ub{P8So0zpZlc!$wDczPnr|zOw<U#AEH{51CxzH=37*#9SyKN=W$RQU+ zoPt?*gpMQPj-CNXS-B6RC%vUxj0L`XZXQSvN#8M5xc&178T{TK4Ck&MSDYE_JCC&m zy|GXCf{$7Dy(Ze3bPM9z_vQFrG~U+m?r}?Y`|LkCUox+F^9Q{B(RzuZxZ!W1wSSj& zOA_5S`r=wZ2Y5YM)O<|t*#b6kCmb4Ps#-5`jDblw#x+*4(c~t)8d6(brZ!>0=I8Bi zR&`U?jJq#>v4e!6^bbc=`*Ux&vh(#%nLQqbg!-)C@;XBoKJ0?F-zg1D!T(k+W!ypd zvYnP^1LB3k=doh8-ScbMv)iEmjZxL)g@X&Em~o8eBu~BF-#JEqm4Xtuan^qBvK;Mn zuGqYyf8}=%xoX)M3P8B_DcVmka!sz97=`KlMV|8~lBmo)<?(4-mCu*XkJt6XrsOei z!*8wCG=-nza`VpT$bnT&&$eeo^VzD$^vhq`8zB?e{t^8*&K4%)mi7C4(3%gy<c5QL zM;DE)xpd;0QgD$Fzth~tA0L-hc0cwrish;E%(0K=YC%uTE#IB3<;rvLiG|b&Y$#}o zfa<7Fw;hy_BRh4Q`qgKcOV8DY133bMKLKA05Vp%%E_>CkHnS-(E(_ioSDj*@C|Q9U zK^Uwp*|t;UahDuf*X0Lmr!Ys=rw9vT4E>Eh3ZwVoh>@KYpZj(z-4QmJ=8d*P94^#! zL&)v9sQ?t;H{qL(i;mW;Jc)KL0P%+*Wd>{#537-F=wwM+i(o9bndJ(T@?cU7hE;@P z#Lugxp``bOG#|0057t$NVq#V<;sLe&{-pIK(al%LZ9gG%uI)^o8ba{J{aSc{g=X|y zk$UVXFa`(ubXN^ew@!YG{lMn@`tHZdWv6`M2o*M{_wZ~E71e@dU(i9#pd%yUfWG56 z!^}@#pcK30mk&>W6yKlO!^7$CZx4hXYY%BX)hU7cRvhyF`eTL<$uC1v*Nx#2ZG{0Q z;q=|8#|0lGY(~VgLUT5~#w;E_uKJP9$oHrU+@pr+oR(wpq}jF%Gpe7VeGA9756F?h zStRWEua~X5{c5JQf1CAf<7qC%fMYUNUPi^-`l?N9&ualLCPL{~ap=WT&MP(|<0k$d zPQ=ZK4nDtJA@;|4;<)+E$ygc7VkR*utVJ@?R1@ZMuScYr76^Ets*esSG8;$I-m0oX zyMMSk*PPnDwm)OJF4Gu|eobx-j_9RZf%>4C<4F5Mw3nCU*>ed<v{7tBLV5=MsXUbD zOh>48eSlYk&w$QE@p(`g7ukIAg2to1dhe8S1cf(9xgAS2G86FRBr{(E5RAH-Ua(h9 zb=~pr66W22>yl`A(U}*pB7wjLWJ(684JSkXb%Cg&4L{LiTDbG&B@GUAJnF#x?Qgi< zBxEJ@S_!8IR<#opv7*VeAZ_`h17wM@_jTEgETh@OtE20;mO>!fh$Ya9roZd_rR~o} zvf{tatpw=7Kt9p(0D+ka@+y7mk)J@^>#hgn>!Cn0Vmn%n2jqEqa;r~&bM*xDoTK3? zq!Kq8+F`o+B1;RV4vGjl<@@J}b+J>ByF2w`lYtN`fna+W{B?KUlatN;sYb=L@L`GQ z#1m)jIurcx1q93o_s^lDWk+ce8?2IC%{jaSQYAa5(pz7lb7{;P2N0E|uTg4uYZGE; zCN+DE;Is|niO1X>Y*Cr!1DCebvg`M)y!w1f-fRItD}zjEOt<U?C_$%p>J8=rC^YZy zcMIkF$gNbY%$eKVv4kzlGqO7|oxMko+sI4`z^DNsMR}&HEO`w_K#p1I*NTP@K<+I% zEm3m~48-PlxHCwA6}o*J`;_^b#$&yHHz>G(N=fdu7sMjqOG~h2$5dbnDrOi;(Kja} zSFv>Hcp82k5_e+g6g8k`wLcjIf9VK|`p2WvJY<#%q*&qY1?R_OX-3%k&9bo|Q-0P* z6WX1eL2}GZx@1Uz`XoWTTOwkvXx?@bvZ8ReI!;}-(Eu?8B6~?&yoz2U(trlDw#MYH zx`W90OH%;cQw~GpM?NoZ<+pt9`W~)0s*6rkS1}S&%mNg9>f$oDbh=*m*n6Z?fkKSv z5t8yq)eGaHoPGt9eYLzBDV=zzU|&~=dPni9B74v$DlmX#f#{6I*l*ioI#yA>)H^=@ zScf<Tks>7T1DL@(ORT<#;Ys*60g>cNaW>@->X-NZaPb7|hA#~0l@bOBNVyZ2Vu$|H zb+Cv{kIUnAEI@L|7XalN;VR^LKxP%=qf_`>)#@FHaxNSzfO|wkw{SFZcx6arvv}1- zu3o)a5ny~arJtzXTm?cTA^+Eip1vFSJdF?>y=38${Y>z<Fs%148UVly#7M>XyWAfM zyrWC3WRDHt!0FA`dqyH4lv{DU3tiJu*;7M4oK|U(P#Z1lVE|$gs{fGPM*$_0W0$zw zvz{{zo3N}KdgZq{Kwxk8Yy>*jLF1F_nF$?pcD3v4hNj~t@Q2z+!mgH*tuuj(0Yr}% zLd}MLyi%1>392XB9s_R~j};djcV9g+o!-dK8VuJ3WGXmTtXbM0;c|tmV5QxBN2Y#g z=aIu3fu{N0r1v*Ac*!<O1IuE4&yJph@W8aaKi=)YVDJ_YSv-JYn2j4_{t<kEzj(i0 zp1N8Z98Pb@?ImuBNfm({oNawbcJ!57Zd9wek4DLqD3~0BeMSLh(>60eKqUpV-Ah0Y zRy1$%&N1R@?51qiM9Tx*RX&7KX_w^?s^Xa&Yi45Ff<fy1S(>x0kd$5EoJJYOUJMT> zcWTwb5x4pTW~PW!HURso*s8iw@OGn}<44jZy8&oWuz-0ED5tenvymk_+E24R+eUq1 z=S!qz-D?x7<5Y8I0!Nr6Rm!jd%kmX6)P0s5wnkr*`|!qBZ_Xjv;WJ^@HnZK1!|z?1 zbaqjNum=Y!z(hzgh5#$&Xd=ZigsgeF&_ZQ|fU}`jNm+eSM6Mo-EPi`+mr4&Gi2<f# zv&7v)M5B%_h(vhCh{md{K+cUaoT`ltMy+HOZ^NzR-gv&?!r*mX(2$*G!Yq<axh1(9 zp0UkT;g}@k!M?BKX!2$!DT5dkc$wayU!renW)D#}X?1J(MvrL?l;XvVN<%=&TWH*c z>ILcK0o4Y_*ml1q4cj;-K$VF-qMaxJ!_id*`o8a?MZInn6!N**SPtugpB|uS(zC~h zF2v5&dZclB5-*01=p0}QgzbZyzk{El*J}gg3IQs;v}oZiDD#rMwK#o%e5`)B+AjkE zB@b;YF#-(tJf_&;?v8%RMgO8;GA$g6u6w%YCEEWXaLw8TMXTZFKbVss*1LJ^-13W` z%g67g73Tt>Hw&MTr|>KR*l`_m7w3X3!dSqMHG$V$Chd^h0S4;H(O4kweV@}8?Q-I* zdt{2`*gEWSgas>a<ik!p+$cVV+&f4H&yTtjt8j+2w3M~A%DEb7&^^N^hCD!mm&wH- z>EaOtaiNU%#sgmsx|<DZf{*xc2w)Z~u-$ipC<;LWU>3c*V5?_F<8nI)wMxO}DP73w zunPh<0*4@lt4@g?Z@#3yJAewS^4}ruFr+f9CHNh;%OC(9UZ%I*+iygEdJU4vb*sd+ zVmeHoB!%Mht{$|KKQ8rBbVKFLa#UaXgSH#wF$f9B$DzwjDm4VKb^Z2GFjl?0KaPvM zAM57hw}V6GNY3zbRJ4^p$}>o1gt#`I|7x_K{S|*WJJKv&fCVs&;JZi`OHiVOf*@?8 zc-_VP1lpFSor^ZSl%Y}d?NmMW>g}*>5my-8vJ;gEd_Q6gu<D<;!j>w}HiXC`D|$+1 zotHH~3{U|qQTj_`M@iw4Me3E2WzmCvPo;{hS81HwaDkh-?1&Eg8140<&x3ot8pUa> zJmGP2bUUg6hTE285o*{CW`q6C3X&lK`3hLj{^Y37H>o%hT6a?xOE~+M!W*;mEGH9z ztjNG|WqWg633%Bn81>K1ix#_EmYejSbgDm1i-DG1Re_FQNw;CPFggtB3{MD-A%zJl z7XEh13O5C9!m5F-s%`n(o|Ce!A{2LKT_`}_8GOzOV1yW&QzVIutm@E;L}{Zsx60z_ z3F#|1D@H($e6_iJP;|+AXD720Vm|Le+E7-!&X5p)EEezUfgJ5fVzF{n#F$7YoqSuq zoww^1;tKRu-k=;acMIbaBg|gSDa707L)`ITk)ae9LIBM>&j#7?#Wul94cL~P1a(ye zlqq8(t*|G*fmJs@ZGdvrvPmb-m`2G&4Vo|SEH@~0;GV{#4lbA{5<{VcnH8m|7FISY z5A_IJaSLXF9~M!iSby4u(Q(P5RN13Ryy-PujGi|k9OyQ;ZX7h-U;4&~M8Z;@i>WT7 zmd3#+iVyzd1N)(RF8s$Yi_zFceyuml_38>b3q!S$hKw^BML`Dh!$0Zv)t{6*F$7wd zC~pO_6_|ZF6(-FxVN6WPNcFx<ctD(@EpA`9?9WKLgZLw7^26va`cP4cs1QZ_$gCN^ zRK=QauBpq&y`y{Q_Xz`xbC8Oj*OF&^mHGnX+ct-#h0~N0(hGPKXWiQq&VfAiFmrz0 z$T)Oo!CgDBQs!&zZ}mh2(Unm_&5JotL$JZcb+vRbR}LuBxdD0x2_Lk(5#`F<ubMg% z<4`pfZc(&ay3vBj+yC;gT?x;G9H3FSY<dQCZ<lb}Sbnq&Q)CV(sZXvIXBAtxVZcQh zG2Xkkulmq76|QJ4CcASG#!5F+sZ%;$%5q%sfgdsf%}KRHasTrW^0-sWmF?`HVx7Tx z_hpAp*J3DU{saf}X2r`)pANUtkVbA(`I0YUqa29^^8-&_N5NsTCPwyJ!&M;CDR99$ z(2LtcI`k{7EG&ieagtznLBZW2$?dZy%`+eVF!^1Xvz_$iNIDrST{;abd-D9(#CX$C zzf|ZMW}}S%LKobmZDnw;Zqkqi6~@ZGJl5pNc!asx(OBTlwG5&OH_|>Q=40(@L2A_A zo7gXPEo$8^V=B|~GiNgjYqOuGy>1rnh+}0poh{A1SVon9h@Pa9db6n6N?5pwSZIjg z`<l+k{8DU|APp8pM&*)g(2eOe>791G%j7EBL@3nSwOgju(1QrLjZ1v52=gykR*<+z zivvqbMM9GVFln$Tj`poF5Pr{E5uuy6?)sRA1lv7m6jdree;}F<NpCQsJPd?!ZOHFo z0a(Lth-kO8Y^K7`jBk4a(W%gMRT4Bw_8xHM9m$L=2TKXTiz|3KY0W|zC#@{f<@h%$ zXp^BvaFD3dhWwQ-_qQtC>55YtJe#!YZZAT$;OQW|@3MiqfbE~#YzLZd7mc($V0Ip! z7vg~i3n2#QTL>^{o<z=gSXy-AxWwLwjYj2BjQ!Cqwb`Y0!o$nLLS{wAdDM!{c6>S4 z1m76j&$IapQ=1W}?_#%J`oeVc2ocH$1I_I_m>FKoUzn;ro)jCUB2BWJ`|Z$Yl<KDk zdl;vv7A?DNAQ-Gv{;<>ckjX*TK6c;_j-M7d)g1s&@Vo&b_40@F^T5Y<>yxa3WEz6G zRKy+@k1Wx-Vd-P#I>k1G+Cv0z`@jMNv7_>vIYq3bYWxjQg*;62|Dl`?%kVQ?*o=Gr z(sw*}(4dJ46Zjb!uEXD|28iWIgi{R^zg$1GD4tXn3~&Tu<jO2-9`ZbPrybQwJwnDY zjc1RY%LU4V{#I}%>3fN3g41iX>uLB<i?D?H)pD}J_jLb<wnDCrt8SDjZHcFZr8+5D zvqCm6HF(3#QxsPU&Pm8<#0(0(<00ePuJhW;Nw!lhHh=Tdvf@bdPlclgRH8<kSUB2V zS))-|b=`h<Xn1h)21CW~h`f1emSbe2BAd>@qksZIf5RF~_7vrC2Ftpas%;khuqDr_ zEHa6w_uf~kit>aj<zgY$cd^rSvZV~h4>9Qn*b#;!0cM~P^prwKY*D?Su+gz43}e|1 zA9VnQMNzBxsP}e5m>Eq5P2=htK_5W*Y)CXf+s4IIcXDDIZ0I0{N?pM5Rt(?m9vX_x zN>{Ul++G7;UR&NOTaUe&$B+t+jtb^eei~onC`<J4_7w{??@Wi}MXlydiwDHgzvBMr zVD^D)36~R9`-oGNjj4F4ZT+Dw@<$9@^j(XFaJ;N0cFum;en%L7{cscr;Ek+534$mU z*JCl6*<%wR$fm7EimjhCXkPn_ne9=9a!dut5t*&1q%xhg4l!i=QLkyA7gV*mL+9|i zuPPc2H{w(eLRDSXTZ*c?)DgHb%i=-pAk{z-X)#6M(f7HJNas2(6!CHLI&~n}_%qA$ z0lBx$JsG911o@E^*-BS(tyoC8{)%YrxO52JZO~RCsD0p|{3~|bA(gVx^1-<Ty<bJl zH^<OqlqUD<PG94=-dqh2lecIAMXX#L{)Wa(%f|7Yn$^<e)RC{^3Cf_Qts_J)DzI6k ztP%Z*=zGhhFHrtD{%1rSgL6Qu7K(K(WWelvMb)FCV4kX3^$u;zh<jm8cR)Y2s(jgK zgM{*ae~u${=qWh#8-6qH9J;)Q-t+_(hLcLF6MW3DI0ErD*ii4x!7I4ZkBV3=lA9nC zFKkLs%V@kNOl|KPxeNl;MP%8fQ{)<mc&yNSQ^V#J@Oe{4rjtYNRDd8|wSIcWbuAGq zfpe`^xU%;EcQ2m0$yLEq)RKolNAkJ6-r$xyX{0=998`k1|0AswYrk{}aE8{i>wJ^r zmCvG5v^?-JV&pV%M}u;$-&x;_+Y#X-TSO77XgtXMI#v6#^bJa}!$gxw<({r1>TYgB zP-jxxMHazVUppE3>frOZp#5e==GC9Qb$;uL5v$XC%T`+O48UyUlN!fASjb1{N(>@^ zwElbZ!qQ)%)sz`eGN>Jm#p%F(n=#x<$0<v}>qXlofUN;Z$A&jY576U5Qy$yDoRkb2 z8*Z$h+8<%tblfBS@gxcx*!+>$ZdTp-fPe@v5zZMweT3uOmdXB$w?#zZF>rC)mhEcS z%L*fJBR9`6R@_8}VZdT&Xb0WB=fEB>)-G>g6O2a-6YE2<k02sFDPJ`=scF+DmdIJK zq*GvTu3oBYB*q~q=Hc+x$#-j&c$eS2vI%syb&iBM@hhSl>b<<QblTGl1A@m`UUpMD z>*wU5$Bs6RxwK$6D#C41fFS&hgxq-QGU>To312{8RF9UYzL9;okv_Z&w>QAyb^Fur z^9<2#BVgbWDKxVC%g`X|3)d}d?1H3=nz!`026Vd{qDMP)42qBIxRP(GFV;h=KpB)N z-!gd<`fdCW^;3qr>v=0#pH<a&M#G^Ax|(O6d7Hm7=<$$#M6u_uL@7`t%$g_3(Rl2f zh=8V7!G8Nf6VC<gT=J1p@y9jIk2pc-nd@nI?;-Ufw80kVa1~5vr5mRDon=i^yFt1e zmaaY`Hc$KpB)KM&wAGC6igZAR^|mFFdXlreFCYxk<g#7%owzBVA0(E2RrD^W$Fsb$ zNA5Ru^~LqhV9)j5&hp2HngPiw>z}j>4@&wxrpdR;&?2&?E1m0IbL!`cXpfteJs#&j zyNuP&-Hn?kwrD&Y)r-`w?i@ERCcvdgulzo1g~&o<?%X_@2TspffuAv7Tq8V!5taVF z4Wi~%)ov}gU9X<=skJJCyrAa1kH84vNKT1{8R36645@C@T<jJX$}h*|&@9Dgz<QkZ zm<4I3axw4-+5v7mRB8{vdDc~RpReT@nkbGGtF5_B42b8|cuF6}gx34kdbZNO&XoyM zc7IlelL@MjzCjgFD#mE?_PzmVEwI|3A~hIRb#A;VYi?_?oJ_~Tk7;a7{bZZQ_)a6B zy0fDpr&H88A0e`0tm9NYSe?7*I(%{c32Y~-ZR=!aX8q&F07Gb0Br@!h9YK$6v@)Vo z6)kAKl4^)ryB&{rPkZgEVNuMQ+>m0iVkm$DR}!`}!aP+;ZsH-m&J9hk<~0=jyD+a{ zqG*qQH>tkb$J&K`cCFVX-*d^sZucZHs0;LazowdTq(=BQnBZ;VnhR#9b|!=$udvTc zuHqA<QW%xL39cqvywO-=3N>+Mz#~f;I5IL#R$5x}Ugx@<NEn=)j4MsJ>4X8vf@(kI z^#D!t1)F^|-{fLQHgL>?!1b?rwRjsx=d-J$^03n<&`{o)asLaG@E5S`ALRvjtUpWO zDFnO%A1od@boly?4^fULBiugYW(v=;`C+94#Kh?l96^hsH;(SIdfz@}y5^<5eSQqh zT7dNTr<x1e_XEUORFX_^npl3Sr-!rhRR4q@%D8Vwhk}{4XXioH$MOkga>U9xXxZ3h zQEAH8j`|qz3D6M~b-DP7QCxAnGBlt5-ZpT8>OC;Rl1cC{#5Lr-mYppD+61cg`xm>M zP_4ePY1TU6>j#Vm2zM&QsXccytiRyepu3_J*kCYBGP+gKM1Egj2}mj^t#czN#pE1$ z^s~dufwYm}&&CF#Tq<oTdMYz@n4KPCL_TPcj0~F>yp?PlbDC|9b3BPd1A&O{Bot*w zeGL0nZJ{{^gW+tFEf^t|+C-XM)pfM2%XZPBNd)7e0evZkl7H~+!6Fidn=qT~<@<oO zzWxjP-vt3Z_nq*~Fe#AExN#vpA8^Ol<p5_mRZCe7N5RscprOiyuRwJDxab@oH>>hX zc7K76Xe+!cH=nY*W1E%|PPqIHOUqw<J{lN=xR(IL(deLKT7;=~I(y)B2g7x!2*`Q` z$xF`(dh!mxThnloDAbSL?;Aupqcr|22r&N|?{Z*iIoej0^-eAQ`w?W<?g)P<PD5GS zyeXN*dIk?w+B+ga4|~=&bjBQBre%HE{eGDe4-jE(n;~u9%&Tf}Na*8I36pKG&HzTy z+BrGlTqh2ksEo@Y$Wh~RR-`HpBzy9H{koJ><tJJ31q{Op*<=g#tI#CLFWF0fl)-ag z9va=&*20G85`Ly6?WUD+FH=7gJhtEmY9cmD!N)liHBhTyPY4ZrICCS=A&%?drOzZC z%(Lf#U8>*fWhOzDALP*b=8lijHcd061>=uJNbe8G*Mg!P&DAYsa>~Bs870w-i(c>3 z`9G^&xp}jp!aYqQzS;C&;kv#Xnnj{Fr+7bIFX7iwvL<@C&0Ns#zU+yD242&R@?1k} z&3p6?Dx@JPx&~$m$s&-n-J+93^zh^)`tD}8>rg|2MkaXi=5ZS~h;787+8=6oi5E*K z3S&Apo4V^69PJSzfFpMw0wdCa<3cjr;rLROvaL~A?SsL6-lLcZw85A#dTEh8pA4?+ zZVNQePAl4<XC65}YA+j4Gfo{Jfj)LhvqoL>9)&nT6|hgy>&wYa=_J`7DP5ceOgaqe zQ&<?2_aj)5Io0R{3$BP!3dQB!=Z33>4^=4_Eu2LnDk(>Po-B||6O?DfRG^*gE1D!W z<zWQ-zr1ht?RTJ01OiJ3(I~P0*-sU`9Rr0?GKdx<Pugz@hulfCU@IU+CKt|TD~luo z7%UY21Fy?=B)>YW%?wDwi_6dOQSB=nYg6e)loC0*20xvtCE&46i%ueKI;4u{{#B>C z)rgQcNZR7OZS36Xn)68=F3=>a75|-+(EC)dG(qF*&AuK|6nV(Q5%fLB{R1hxt(1_z zQ|AnVFHiOvjt12dbhdfkka*_FcQ`xf#*h_`*jD1pnN1b~SZhn9i0%HEy-BQ4S}+!C zrJym!sogRGC}rPX$ABhcH(v^#XWj)eBgi7&gE!P$d#9c{G#I@^*6hIHe;?-X6m&Xc z<$mHbfl^>Ucw{xDVQoWttJnXT*|?@5kMM6Xx3Gt0vJj29UH2!mmc~HM5~4IPu-JlO z;-)F~bU()&jD7^S@I<o(UMq&3{ybi|h(x(~Dm_rj59`wIS#Pzm8ERAjR5l=#8!CSX ztnbx=24eTXTbIZpwz!>!*-^%u_Y*MGET}=x`8+2McKdDVEDJ<xHTUL=eT+4&dJuV= z$(#r58~R3=d+Z`xMUdVP*;A`OpJh)xwcLXdU7({SIp0J)R}FV1H60o0UXEmOw`RU> z%@myaB!aSyikBJy_0IIe4CKi<;^emVAaJPfm~hgcH$GHA{Bj}<a2GnvLW)=&y9SAO zoejF=9!AKp9y91&%V{-JC=xW85-^ms#}3=Jp?^Agm<w-vxxYv%lce)df`+_NB3ItQ zXHaH^F?${=H+HOVpr90<U57i~h#5|5<<$us&HVh;E-dbyQ&dz{k)C|uM8I7r;g{#H zdSW{#<^9&3U?L`c6Fe?wTHoemt_ks$`6d!|FSi^r-dycJc=6lo*X~L@P9Y=G{g8ic z)H_U<c<*ldDDWL5KAj%hcTD}n@0O0PUBy;2j?($+Ixf8HV*Ym&aCof>Scy^`z$~kH z;z`my;#>MhS~K|C_m7Hu<+?{}h`8f@BEdy`xeuej;-bdH0jz{vO)#l<1$+Q8-B`Dk zY=aQ!?Yl#gN-5PSK8hE>*5LfSr0+EKyaeOIcZ1$C(|70dF~Ij=MhiX_5&epCO+D<d z3t7xx0ycdYo)zWV4cD2?w&SMACnbVr-8S8zFT^H-QZ%WPT>5C<qSm2~dF1U%wFC#I zLee!U44Vg9B^`TWv%c9`mJF#ItwitvA`keTw}<Jby0}}<kjajO5OtVSd2FoUcFtnC zQVeC8M&pBqz*4RXsjTb-u6$M&@d85^dV5Qa_pa7}?`W-QHtuk3d}>HszE6vI)(Ecn zaAIR|#oYS-Gng#T2cay1ZQ`U+8I8FHHZ8K7m$?S7NE4)U#p|z0s0f*c*@IG6e8YQ9 z0Gl@7k7)Fjq^%cWsV?nt=dxPW7ey^>&0LnLq9x@98yWnwZMDj@+sHD#ws&ZMBJq@3 z(yq%UwRYiLBf<|Cw@!UKuOkE+<@$#D#9|>Oo)E7J4R3EOE3_yP0~1sDx#t~bOr7jY zg_Ofb6)t%3I#Gz#W@(?`uS9I+iMNO4#dX@irn03A*K9xo|GnPpiFJBhI_}BB$ha1{ zN$~~YT@5fuVQ=|}6G*S?u#NG(!!RK)Ec7OngfPH?B-U$^o9j00<OdmNSP`Jw4*>&6 zHW?W2V+*&eYWG+FoDV=>WRiX+NAldb$I53;K@3li)7+gkENj5R(!fc{wOHmjvP%B+ z-joLAwaxJiBY$7K*5b|zQIq#-iQ<?RafJuER3i|gPbcFWz_(6gWC=M+4J2HtxpX#4 zw}l18P6oz8!YLqt<4p37a-GDc>O-JaUHrpKydjr?<L}zFtY#)JBg%(a>*9Z+;Wu=0 zZr!e;Zc60MymkvIuZz`PO;fNwArkY;xh!^Xrwnq>Vq2urzQKuK`psW!#KTDwqh%|S z!ndPJy--Q_;!@36_d6!ISiFEQ=I16@l#8tTe2*<CmTp?06qj*Uz#?uY!G(mj(!kWy zLOZ=m&~KYX`!@sf3^X)}#A{zM+roPBJ^BdhDa`-Gb?3bmX5kk^TTHJR_rSpvmpryN z{h^o#!XFUj@+8F-v1#ljovgR?q!I~lkhtfga`hi$kC|rp&EDZgS3lKUvx`n@=pA{6 z%%7G|mG5~uk0U)QwNbP@)=rT{jjPyCJUY~fi_sfLZ<NJjT=)j|ry@r@u>=Zkb;EbB zW9t0GdinLpuTf*=S@G{GoYaO;vu}7L+eH*L%c1K?GI7oHmzkkyXk@B>?>>YNT5T2u zqBhu4s}?R=HDwc@hl?$P1Z6MLQLB1K(^9_tn83O5<rPQ-{9F?6fP4By?sek1GOmwU zne5^n-*k~8-HujpV|+B}J7Ws}bs|5z4XtBm{n(*T(5`R?USWl&#od7n8K(X?@D47f zTzh-M`VkW2*5}x*A1nD;u}d(BSuu`VR`v(@jj>M)4D5R;*tYow*c-AN5_{b~#r<CA z=&!kf1wqw>jF}F7`_m}(G@j#FUV3*PKIUhgy@ocouG%l(H|Um+I&;CnJaDo|Z!;^h zvbQ-quem4O{wS}weTHRCQICAOeU_$;UNf^{a?EVFTyxQ`UFRGpYg#Y-E^CRl;np}_ zCpiJ=oSy6ujo)fOvZahYPNyF^U;G4uE7;maF{+XtiM_@YhW$Gr`_~r;W=5Ij>-hfT zAYp35!Ve0Sq$;*IY?U&=6t>dq-w~v|UPc0>9Ma0lndiW?Tbk}lLm4z}j5P2bY-g_l z0+5ZIDzdokl$;UeA6@u_7L1e$rtP}<IN4coJ4M?SpKM_Ip*2guTsfkk_MFsa#P8HE z9uRAIP_mbP+}lxU(6j2X`%5_2P-Ly7#X$`WfonlQz+keBwB1<+KEN(*pn5oq_{IY% zIg%tJHo;l*CrDYSG_q6-a1zP^L4;CBD3NI4hGwwOm~i3)I$1iztVoDXilztYX%o~q z01w21mH`zF%?qSEB-ry|*kojn^J#}9MH^v7xn~asPLW1S7R6#A8IE86Di+oWAzX^+ z{=ZHLYNz`#4bTXO%(VHcco790O<#l4MrU)ttITWSHe}PvSKu4l^*vc6KnrSc{X1FP zN=fTkuR*E}ah9${U}es+!A;C8u)u_oKy?!VTr7pwJONLORZIH6_F6yDQ=uvd^ra`? zElP?Hvg~1-fQRitg(hQeljNDcLEE<e_ZF_#j-+c9YzY+JZj>bW?Qup~1s!N<I!CDi z%q?&38uK6*t4}6uY!Fl>NHO4<<V&>ML>xb(mAt5Kv1>3W;T}$dFr(9-MwVQn=1!R6 zz?Qq;ph@j&j`HGba}q}%K1;8jfc@%&hV5_vrJC)}hF5nGD>a60p~CTCfT!v>#jy-# zsHu>o9yIkQTLyH^d^{{HL`Ry~+YXH!84QmF9BDMyXHv1!X=FS~O!<O;dB*b(xEncg ziL~&e3>}%5gXcC@j)~pp5<pAlPhUDb)J5hvhr6gZKtS`S`$fqa`v~XsYT&E{*gng} zm12V6chGiMaNC@6oFrEtEIEeBAHyF4F|dk9Tx<{s>|9}zRM08Wm{;bRKi_5}Oz%b% z;Ei5pAnkMVR%!lYn`!%P@y_p+Wrq3WONMm<v}~uMmr;<3RPFKe9j;Ld>W1)Tm1OxU zh`jox03o(e)ab`@80){z2HfQUmaPLS6N1xN$I{N{EDIaQ^CRf~8xRG@`TeNEn_&gf zV(cM~ag^iVJ1F@%TvJg=bCzKUdDibNkZRa?;LX{Wa0G8pcA}wsA*v<#-tw#wKOx<$ zG2O_TAVQL7mBewW0xehVPafD%*K4d86ZwR?@_zD4IOwv<>y~+epH_#8xqAmrme%YS zCq{na7|PseuY+4KOlaRz(VHm$SKO{bJBzeKlT@l4Y@sPq+t4g((z$fQu;x-aDWNC_ zw|5utz{N9}1>Sf3&&j(RW-ENRvvl4F)+}f0kgrkyh}cc~7LGCvngdMTBq71wZ@>h^ zlg;dJ0|TM#IQ0m?v?}`T+FkM&a*>fNe$Zdo*XqO9oy(l=&lUTPKEs0g&nBRLy7z@^ zn2*L^fOa0z`x$Oh3<{g8<-f#Qd0MdQ2H^beAN9szCRA#5m#_u_xHb$i;#ZiSFt3?k zpLfm4F8gO^{KQFeC(kBxH#6D-7!-JOj8G-6T}2^YJx;og)?pU9f<FeP(P%-yzfrg* zWHQqV-ZPuljdKTfC_TI6k^#|a^PaY;+Kqvah~eMP*z^dzZ^xY#k%=K5_~{Ld;2}&Q zj{P~*t(>sp-)BxDC_?N+-uBaBZRf<jPOPg$$~2_Jm};YBGi+%`;|o`F`Y@v}VcoC^ zVMntgZuUe}Mv>?CuHJ3L7_;^X)ZZ^o?}y(Wn4*M7UI!W$u>>Tm3=rqd#)}fN79!E* z&Ozd)aHQUM$TpCAYtOQnIPwfu1vYYncSkCf&5^<|o5_mbdl36&7=VN&t43BREPU$` zt>IeA$XYsUY@lMW((=NY8o3*=nn#G~5BeElVf<bmGz@<7L#t4jw<qO@UlV!&whQ{i zBS23XLGh8B;l?pR+2@GNh%tY%W)$WTo#!uaQki2gHJDWhpXeoVt0nIz)NPB?Q2wMS z_N>X-Qeel1Zsmxkrx{^Hc-(XuV66HzFt{6dZ6=^qW3M6eG}{FgO<uI!!Hgy}>u<cd zV^$Fqnr<6Oj;_oS(T!rO63VoVMlxUHc$tsx!n-I)qKSxon~Cr42LPz)W_g?u_<uz! zsF0<i)|WfmsOcRKgi-OrtBzMGesNiZMDX|5!qS{R-rI-O3iLQ|HU)A{=oVl;=3jJO zYosSe;RgRQ?q|EpwS$8mp;V*A_l38L&8dE5ckFPx8aAi3&WemIW1>q63oT>1mXuzx zL7Jh3ctKvlM(K=}fXgue6Ync{aq=iLmOBg%m}O;*GII1r=XLhht3afUA<&NJK1`bN z?LS#2?TEtt-7|fX>@mV0Ip-H8k;~^9f~(0N&L)gYlFJlQZ5((+gSs(#)xF6hk~L6` z(*TtV78vK*p7dfK^(Sy#ZncwX{U`>72h6d4cP&#QyyfpRv|UM2aA~}BY8VnhEM@Iu z27yyy{tmkl{gjovbbu%TkfGj-Hir_WY_Ls{krY34dJK!Ft3t79AEE)$Lqlp`C<xD% z?O2X|!M8K6N#~SHm<r@SoK9KF$nsi7Se#@c?xxfv*mb*ITK5Vutd6Y{0JHx<(Qx+D zU(IH0Zjv#Fz*jC+zn3tJuKZI{hl~;AHhWYDXHUDcfkF+t22%BRJ#LozHu!}XG5Bk= zD3Ea&220^LWXLe_l^F)+Q7o2+SfFdwV1@N59zI&fX^<BYZ-WQIs*^!!Wd}r;{^t-< zQh#{71#xF)C6|fu18C0lm;;J=I(KOEQ9gl=Iyc6D^d6?)rKj$hc>=iOtwm{=_;0f@ zfrT)kuIoTo^M`W(HgTrv=NyWdj~^fXFr^0av?SoQ5miU+uzeqPkvKKbmnl*zX|oFj z7I<MRR1;Q4oqAsC+0j@-LLOns$G#FqshPDZXVl+wK%3pr%D*yVq1d;-Jf`XmkR&Ny zj6M%^Gg^yEh<_YW*ZsSA)uAs#mvrF6*$iKwj3CHm`!pqwL$XtlqZ*ze?_?EkH$fAc zFa9^(b(xQI+LbxIC$4SZruYhllu@(wCW^4M&dc2HaAVuh`d=Y3b9<GXD~vg(V+){j zy_j18W?!snBtj^$tjHCpD};i|7Y<8l&y?K3u)@un*_mn?&;XfR#~=Z6`);($5)`Z+ z8WK=p8ByrF;u=do3QF7ZXXqIsGQ`b__Nu7E9=;tcv9$F>xVv>idcPem9HYw~kQF^0 zX`fH%J6erE<U>tVGec_tQ*4k*Jttox#TxYsSu&4Hqhq}itSh!g#zFFr=PbdL-hx5{ z1_DsYu&*dKgtL^QC+fere5*?go$yjtz6HAA!VK#|huV|ZXzaxTOnsJ`S^@1wCi`#o zS0-)KP%O`oumOKwB#;wNn)0#taxH!n)q{WF-=PTOiX(CRkJa%&#;EH60ZH$xJ8B2^ zWn36y;l&f6bbX#HXnVP)T-??QGrGi^9w3LP``0s@R_g1y2c^JWGBhx;<LMPMaUrhv zLcvbf(I|NAhx6Mrb<i`Q2jfIwWBp>qaRtC9OEFdrnUA1(DLX|znYTaj3I0Sur7+7i zSXmJuAjHw3HPa8omr|zkppUfId?E=LKQ#CQq#5T<@E5*%db>R!z7W5<;D)^B$6d3! zCBt%8<+S|9^|qu_W$U7=D8C1ct&zFSLSxD*bclhQ-G}%cfmY=LE!s9d?$W%fRaD8# zWvr`r%+y8{pO|krq!R{X>et_I5Pj|9t~Ha*?{I|ewff0_3FeyZ$b)<muX#=V`oT6b z)@K2KY1@_f&@=M_4x~FI+t!)gfDb4J?%A(z$EqdtQ!g8egLQew#%)j76{xv^^#}cy z`q%$r3)uSOATxi{NXEWtB)H!+l7H9&|0Y5GPtL%<2~kOc@<EK4Ay=Q!e}fZIi)|yZ z&hyFJsl}>`yo1G76CQ;RwzM})XoYtJ_MX^$epA@YO*|?uZgOMNG)M(<M1dfn&IK!& zBJ-Fdq@MNEOQ;H3RTBQ-s_A9xT;>syow=w@wx~FgN|Jqtu5BgjuY)FS%(E3}MXQP~ zJQh<<2&rS&0?`ziNk}sc{hj0N*-m^jQPpzWSz)!!zmeh8yDe9*7Wfwltaf?9K$qkv znZfSW(e%T!39jD<q#m?c!vd~w@j<}5tWGqtKA7557HA*KvjJdq&uq~cNW`sMI8~Ur zs3uE!2lTdtB~p-stS&?OmsK#<gcXP7{Y+B7r`!)`FGvvnJ-(<C++9vQ4J*Ylb@4Oa zzt?HV0fZDb=uWGpYKI2v(j2SzGu2?6FF~^m|H;gNoUcxI`etSztAYa|{FknsoIR{f zo&Mov=;|i!iq-jacjp)Azwg_`{Q?@Av;|6V#WnQm%&EO%k!6`XpPt6Bh`rGFBO?+Z zP*qY@llqfT$Ne`4U4iOAJv&jw^yA~VUx)RTuRcLc=Evm*dtWzPSF84w+FQxQ<z2g- z?+2zg_sij>yVhk+%G!tR$7Lq}(vaQfOYGOn+tO7`34in1p6>I@#!Y+bLi^j%+{r_G zXz1Rl$44E{r_EQ}W-x>ADuG@~$@Ns<QE2MH&e+;nbNR>E*=>Wzx;1+VKS4!rPp5nc zXINC-*FB(K%j<1nZOqdxg8$22!>$kMseSM6@WCDHqI}pw|0%R&sn55O55LdccO_@Y zOVieM$uLCDFgJDLw1)qZKl>A%PsUOFMqy_tH0W8MVeRc(D7P)Y%?pIFp^MiIACBn{ zexV883-!%!u9%G%mzW>EFCVENx!=GS%wzl9&E=Wf*q&wm3#Jqtr2EP*<Idj~F_~}I z$q%>g8F=ht_mRGibe<oao+NjP<$F{v9$g-*>--`@v3|h}pIz%+?adWq$pcJ&Htp*^ zT{_xVT^|p^uVzZA_<asr8w?0rzG<Qa-V!+%L|^^YFWpAH)ng0fDfREpTA{v38D!Yb z?LRuNH?iAO`F%YiRocC|FHnf8!{hF25WtH<FOj*XLhBJitE>1wYV>_H`F%Ko89Lfy zO@lG~K0c57%>1A}?l(T1Gj!f-d{;WQcz7$_t}a7U*)Mb0<<Gu+^san)tA5)qeWCPs zr9`!?c3;RHt;O2yKJS0kYITd{;B>WVi?O{>z4owqJ~5F|d_J!oy0Ra^aF*Q+e6@$+ zl0O<rdgw6}V0PoEL1*kj)uru8!5Nk#?35xfDG~ftB`~Vs>lg6}qw)@$^L26Ux*xg# z3{XJpsl0>-&Z+q3Z<8edrfd;8pn%3#_)hAj;a9A&`+hs5{ph#E`qF(Kmsp16vz1$O zF$KY<E^Z^e*!t+;q2(t?sh)D1S=WkFV*VT2Je0Ux3BO?b(05X^<x_=ftZ8ZVp1O2& z;NVn&URe4Xi*()kwq$sxLS@;XZvGtf)uCIpIQ4ch_iF#-@G&I$c*lQn#h0Zh)~LHM zf8BR@cXx8x=aj`t6Woehq#pg%cj*2HKkJTCH3(*9`rw(&@0wsqKC?MB(h_Vs%!($W z=8q_c08%`#2)<z<b63wtd(RCCPUQNRhN*YJ+wamSJM~zGY(Pg`b42ix2A3<w+r{Vg zi49E_{@^T<HyY6+)U2R4o<5~yR1pbBBHxu+x$5w`$h!(rtdxYJL@BaT5=;3n+5=P? zB5ArMDX0Xww(bhQEnJ3Ywb70t)WwPprpy9hMeCd2%^zjhX<8AqpKlw0PVTC!{=$`r z8YY%4$z=_j-9*GfHc%-jrA%ltu-+pP2cpx&f?u?ano2@(tIkwJQ0AMcJ^E00#r~+j zJd_2XS39T(VQe-lvI``jlt^fh1ML%<{Ry-7b-82X5esuV%7eGb?0_{nMzjSG<!;dt ze<gN+57P<VqQ?csb$}P<{wvesI}`oCGQoC8?SKXUZ~3Z5Y%+Q<1`n9wzml6E|JC*d zYr=O`!tZia|5VlbZY}t`-T$ci@2<egD6TAt-2YbxSBfOxDbW8&k<tNflK20K`2U)U z^SrG>4@~_o^*;r3#rc*;3Gw?|Q5*K@2w~{{mxy$PP!t|8Q2$6hpXk<z*zvDY|D9Cm zB-}9R+TMktm%}BZwLsW~ujb-&^#bgt>8C}o*q+=Y1uYtoN|6*-+S26SwdL1}qP=Y6 zge#&kf9TM@GBjrWNgKsWubuLKHL&sa;L@sVJK!bw-nTUFnrE@>ey9CBIQP4H$G!Sw zflvPE;BX{==Q(<dU$^-|gdoOtmwrVcStsV|XFT@Tn?0;BoyU{`BJFw^9_bS@f~EtC zdhLvH6#{te)X`XN>5ysZli!twTf@eAmrm7haV;K(#Mq4nSC1jq6}q@a$~^~fH;#13 zBX)|nPPwrm<J+&Xw|rKVP2=aIv(M+tO$Q3eONq`S#GQgO?cyeGi6m#N>Y7+u*J=}t zbf=G?_Q<G$bJfz21M1e!g|(Xozwm4lyN9INTXK$WXE$YwN%)bb;4`T@_c@RuYE|@| zGX2HOz>m-V>tgw%G3HHY{O}IR(M%<&vCQrFgg@-yRKkl_vXapF%=L2~+ycqc7mI#b zBFO5b#q%ruEY}MoT@_2!{K2;b^nUM%8K2K_4~at$B_Bld{w(30d#7wSYmas%!@9oD zK3-@^u4xeyJ!4-f)(|mz`?e61-nUIy9Btlfu<!1lSG(^b<xrq{m$R<oy{xVi&jn0; zuphh-A<bh*U@>MVB35M+1BqkF^M|{VxBH>D3RDlIk!O<V#T1$#IsL<051i_C>}{j1 zAz~b<C^8{Vu}3-{>W)}d`@}5?6E0*pbII1ix+A9{G*n#SeJ;q5bCS5g6s<^fHa>%k z3+8M`UrjrLBvp{njJy`}a0rg~H@Q)%8ZmfT-GYcc!h1lyb~;sCEor`cu6=f5ug|iQ zRe;UNK^+EcW6zlu9KC()9nnd`*9or~s`-J5P(IsydJ!jBj0}5Si@+11MBA2W4FU|M zZ5>dEBW%Lw*`zaRWf}bp*S=Kp*JorSuD^~AGLp%6$*}e08E%4dPlPdVumkI%ep@A_ z#&RRPLwGJy-O_E5TS}^Zm9E!wdu&w2{I=a&qMsi>%iMln-JirBd>lkaKDfT^-M(Lb z(0o;{J(SG7EbJKkb^mbi<GRp){v8W*{|<~_$9`w;1J%{#X2%eFCVVSO_*CBB;vlBD zaTJ0bk&z;s!hgY_8;Qy8>`}JPTjZDuv6ZV^{`5lO>-Ulmv}e`uTpo4{2&ks@`GeJ_ za!Lkgos7LK#9XuR6O>(zE|s-J*HR8=B+K<`{73Ne5gMn5S5~!=!I^s@vjHzI&|%Kf z%N$V<%x<$-bVlNVsGlgu^Cgd@=vLjJB|nX<Nb>(7>@5T0TDEmjJh%jR3&Gu8gIjP3 zjk~)yE(sb4?(Xhx!7afF&{%Mn#^rU^+UwlC&pqe9KmDVs#wTNR_pDj7x;o_o#WhnH zGi`@Tw*zs!o4I9lq5|#xbnCnmi836#ft06%Gi^|XyaP>RWCuw#i%SMcwWEn!44H20 z{m!nHqNSD+Q(5H3V{NweAXiia_I%T#4XASwpKWH|ymlrUUVNM~hws(2ff;y_-B~IV zta`stvB5O(5!#jywQ_S~H#i#C`z1des~$M)i7lKMJfvkY93WNZxOR0ZJ;}XTArJq- zb{U<bl-K#5ngJj+zV~Kcz2{hR`c@@ih&lRdnz+l!!Qm6Q)H<aLHKM-Lg@@P&yQcFU zTO$X`;vBW$72Kzy((oJ26I_8UdN`ZiErF&LYRwuAQ(^J@1TH&u!>`Wu<#kgJDmO?9 z*Wo*;PjAJQK&qc?JbcR!2Q4aP=nOTrOg9Yo+E-B7&E`nLN4O)7T~n;E$7Sx^Baq3( zwJ-ya&8IR0@6`tNXJ(820n6u|tNyXE$?Yl&3){*CKRNMSZD)A8hAdl{c~jBlDJ7|= z0Tb^o8nkJ@K4kmzrE*=LU$vZgOx`?p{BE+CcPa|r6z%Tm<{pMWnt`Q7onqx7L$lM= znJf*)#{JBYj*i~UaNMqWq5dRHtB*ZuE3j`o7fr_Z(-~L?+OI9H76!v<Z1NN!E4#fb zFXa{p)nu>M$BlC8oII`4af^0$_HWWPIPdkq(wXPT0c59RGp3{v$KkOz&8Zd3RfJ*l zYBzC_@|)Ov-Xv%T06$fu`&v97()bjO6jYOoQI@0Hv++R5AV;1GfK~*FXe*;`!#B?_ z=Mip6x5pPLJ3V!7dds|qL+eoZuh3TrHCM~9r>f<ELZa@2izpvO<5~uG70b7SFHhtp zq-vnhY@q>2p406Tpnxsyk2e0i<VpFb*6TLvS;xJC<iiQtzwT``AF%GznaIkRq?yZN zW<SFybSDyg7d_s|FLm(r<tV<<=wdd9>7I#J)v&-Y0cfXyh0tew$Rda{<$jwjsvX`Z zt!&@-#MrTXYuqZG`$GMWF}B@k@S(**?$;D^Q&yCjU<*pKtxsZBA<b}6G6}V=%c`@& zmZMuD*LLAtH1<1A<#wYX51tIUU#@yK)AR|#B?h%6;l$lbaMoWlKjyR??i~Bj$(b-Q z5wV|?w7Z^bEnuxWyShg+p@zw9#MiwsLtKV+(Qoz?*%5I}{9XSklzk})KDW@?T%7#q z*9Wdwqec90<VrSZ!XZ);19aBZV=TO8)8T`3f`x-gn&ijrH#mHyzQ{|&zEwyCB7#lZ z-R&15!&ip$c~x%)n3gK%lr(5(qb0SxA~eo<%cmwJc2CkX#4i26YF>?Cw%PC!Dkhkd zX}XF;e%$eot|*ZZwcH71Y=Rf#^lS(F?cU9vpq2a7Q~C&mw*?TxaD`joh5YQ@wA!r~ z;Y?|pU;9#1V}fsQ9#$5NqY19#eWYOEK7^eDxuqUrR#!?B>&7rO1-5*8@pZQ5$krBb zhvrv!vwX7u_FZB24b9*#^(+?)*1m<ay1W~WfJ|;_;(pRyBy_R-ObjcZifH*00KT9o z=#ypi{mXfyNM=t|te~kz?xZ-fJ*YYDy+5Z|4(?#vv5KL9L!e0Ka#*rXkM`r8V{M!$ z&YYu5MuG{&AT2CSNi<t>exb5>aCoBr=N#w$11H^Cnmh3Bj)}$N;edOlo7<4$PRjG* zz02*j@ztWH=zy)?&>3RHUJopFKP`!6)g7L;-*(6M-1i7WlNzGr)#MM2{3I%Q&Sz=W zUX*Lg06nN4v!w6@V!@7HhPL|hdF&bWUWbONNnscIR|bZZao5Fwaf5)7{q4>CqKHOH zev37h(ekwh8<NURgEj3Xu{p{3a39p!m-80zW~WPC|C6HW<`H+}*_C_0f&JpRBpE0) z0Qk$!G#~#NP(}6~zoEg*?c|eh8yUqrhbA52v)hOspBgecu1AmLZ?5u`zjjo)Z-Qvk zO}WM5*g1cu_byt*&oQeZy!&3fw#u@BX4TLU$RuHJrYqqvB!)eG1G7FYp7@lxY<V1B zPVwP-=oieenB#GlQ+h#5nQ2G!z+-i(<i65|0=q0s4N3Bkj9AysK;GQR3z&MuuCL}u zU4*d8PZXiFEz9g^%JtfhUqGt3g^?f~K``NdOqa2t>ot!@NphDFEk%dIzP&)sw2V3^ zBpBOkr#2Mimbnez32h&XAOB%LhDl+6lEi82p_|tv7+cV_sbwEcUHr>_mDdmtc2^z7 z>H<ku42-2fb{+PQs&|Xf*F1!+QxjXlbbLYD=nDHv!~B3=;vc(S(7K_Zk@`?jG&C(> zL+J?#JCiT1!vBL~DYBZBBQ^Uq4+mD)^1x@#tOo2KXhNP)UHn(eCYzUJIGMeS5j5Au z+a#<TXkHZs<>pvE)Z=e`INxOIyfBThY%olvbxAv>{(-3`@Nrd=a_l}~LCE~I!p!Op z)djy@cI3oSg`%>NH1O7E);Sjc9HEgPAcmL6ThEloYuYQfaUYwqu9}v^`4|mJ(YqKB z-y2cshYhk<I+$4omDZuNiUPs%gkQ@wj-2r`2%>XFh0Yr()b!#{iHq0UF5sOj<5xTj zc?9OsLWRxmNe$LM6`gj-ys#R=C+lR6)u(EylKq{-8_^c1%%9|tSraDHn+Xy7wXPLE zPwQ$pq)Mb}HJt^4l+?vURH}5^Bkf(aCY#^xbG8=+;v;)Wv~fl3Z>oXw4eCh~PBc_m zYZeuW!2QINY#+}|M4SvtYD%^14MRbb7GeMuX1n%CpFiP7`<#b=hj;xe9Pm%LjLM(z zYs^E<lZHIcM6&or^`jh?d{CxlLlxsgv=9A87)-V*>m;R5t43F8id}@6Anx4wk^ML6 zuJa-3P#?|5#Qn8Z#NXgKj<TpyUdwsA@i>tJ0J2wJAnlxjYa|(qChH;13gxmgJowT_ zqUa-QCnZiEV7{Bhhc_&^>L^c3om8rezrsSl=$a6$){6o!lm|$cyml%}Uj!$dIN6m1 z`*?RX&XnSVWTRATY(z=AGOU9-v-RBEvB0~P>GtfimU9ZwtT^YPKGx_z2Z|0qbeRdh zTahaO1(5QnkTeqaz`?hr;7-B0l!<>HNo~sxC@pvi7hr&P<`?ruge;M;6a8-eWhWxd z;P3fb|8(|p4#Z2RB{C;9iC%VYQ0&>KUCdw1Dd6Q0Z4<~=OWDgPQOcM7fMaVRh2f5F z4IrLw?oi{FtcZ?=IqX<Wp)g{Xw*Q1DtKQW1da5Es6qd;_T$G()zoMAm-A*9Yv*<jV z<utpPe&D1`W(?y#yD-gk+2HU%Xj4_4zixAl2L@JXbQS<@<3Eaj5ZZL6G5z*1poTQ? zdw&=2$_>e}Nl!Yfinpv#W-^?fr2Z<TdXSyr^y4`<A?lo(KI4iUj<L`Rir=e%%Z*jw zwWiXz|8L?dHQ^Zl>h2lx`^@(u+A>)2i$D<rxX_lI5-Ov<-8rC^k%lUks@JDcx+0Hz zjcYdk_O=gV2L1X~haW9zT^L|ymQirfm+!?Fsfci)>F?4}5d@5-{!p1c^)?85+)f;8 zDn$kU7Y`N1KRlj9D(#4fqA?L)0^u{%d4WL?OJWd91>cl0^7A7`e@-&P9KHqFXvPU( zSmaUVnqa98d*wRiQssu<uE&kaHf+o`Bg!@>HfAY`4}tw1-AMU;oy;D}i$926#%Psg z4~b|SV#WI@W9L*9AdU!TvO!#tE6aZ94W2x3qWV8<=<0?4$u?b9@c&|y5+6e8{4ClQ zn<*?K=&v~oaU@-#p<<d-QG+NeQu29v{v+J)tock9nEq*-Vk#qN6uzu^9btw_+T8ut z`iz?cJ8K3X0+O|-?7;NU78C47qPUYHs$3ftF|_TV{-7^`M4$lC_tcZYDl1eJ%gQqz z9PotWIq=KMb}d1(+T6@A3672OV6K}sB43z<>4QyIcN}PgAgYa>b5jNwDjev8eVhL> zr?FpZ&j91mxo_;?<{A~)9@?TeFG(j<hPb5szh+ZP{QbG#F!3iDWqh;MiT|N^5g*~6 zoSz%LeecgZ{gBUUy|~WrcRGH%hq(r^`qq>^c{1Lk=dX7*HOPeIsP_s%EiM$92g;w! zs8J+&T2t17LlR6%w`jFDWv{ZN4)9^&FH`{FxbgXv>4;~2#Ddc!13K}GB`cxJFT|tn zY~4iPMfv^QyIFN9<`sX|<bCk^4SHyAaXCS&Lq#AhFGr>#>5US<9!K4{ZnB{BmXxdb zY>8JK>h8sn`R&~}cKPV2`B#`a9I0fE>}dp)Q{yBZ>^D-#W`}_z(28W3>=eyZtdV!} z+IPqwECj#m;=_%QqGSrQwNFokk*q<<&?3y<EuXbJ6HF?1E+!1(U;y^&2<|S9zMCK~ zJ7GodnJO0;px7e^$wFtjA;iP-KcG?|c!YK|QkbwC7_;M~ok&^uhfPehg)d{XK2Dg7 z{6uqdGhu%qOI=lknLQo>FzYH_{MI3h3d$i@U@DGB+q*Gg-*iK6{8@|{i8F!&^A$E# zg2F_MNg9Azms(v*@MXe-s(}SFUNDv3*GxM{7nQ#qI#!uUdL5fQ!-U-@i@%g6ik{4W z*p&So_B?VBr+$+wbz(O&JvM|VOsp^vJriyuC{qjx6V`<NGB|S3Eo@NLtQkAaJt%N> z^zAaI%)`HF)SaLE!qJ7UGz0NHeYy$pjU@8j<javQZB)=+ZNnzwU%r{;5k`WT<Pjo+ zPNKfzNih{OGKEt$WQ|nX8$!-x^JupA`8SoEyy<^bKIHyUDbfp3NsS0mDLAFH7s5_U z+-{|gD1}_QLN110_)IQ_dQ1RoE&ko4z2Ah6W99QiyO>yYi)HhX?Y|)wZz|ESb^X$^ z9W-I^1P}0D2|m*>ug-p9X~7UWrUPHYCC9*w!;pOqM_4S({r$cTNg&0#G|L{O#1Z?| zM_cKxZlHMj!~F8~U~a#ph0+JGrg0W0p-W@4Ib*Rwd%jNDcKY>fDDP*WLcPR85@KL= z!E~-G=Gv)eOYBC%%xB&#$nW>RJo;#o*dowcE%LPC*azB|@%FxNma|J-vRG70m@r|y zO2@=r|IQ(Dp|q_SPFC}s&-Np;v+`Ev8Uo>We(-D;$6n=&iF%g-|DVS1{2j>&@7~+O z>PutqL4ur>{|@5+zk}>p9Lso9Xs6^d6ZtG5RNu|)jY1r^GIfS%r>rwae{3i5Su&T? zK>8b2C_?y3fbH75nMpFxI)^gQn0xR3wA<3lnbh;tk_tL2Gi_t<aZQ=+U&v=R;LX2< z^e@}Ti)X5ft}(h!jfD_1fkTKRUH@ut`u48j^JwSp{dfMlXqC)azQnv?M&~I#4TV2S zTEr2Ym7m>eCiofSmTTwYmMQ*AA?gX+St;yRrq$mHYyWSBDSMfscCk+uO+TZM(QITU z;}w!6cLd^T(UTm+vpr}gQ|YG*7|4)45Ve|$1x8G;x5<N_Pu-%QPXD?>PrshNnBT8I zZ$2|c_sebxgvvH-TciPhs#Jfd5tVT?1G=~i6|qe7st}uV=_mRJ@e5Dhl>gjjb1F8C zb0^1Bc3EV$nc$DFx)qQSw0B^S1iG?a`$ZhsHf3=y4#mK*%V-unao(t&C8$uq;fMYf zIC+neWYdI#ayn*=6iI+C70R?~Pd8(6d`%cjQ0mDgyR{71c$_FNcF2VTYt0)WwWPS< zCHE53V>9)I(X?4ar|C3qPy+aw`rZy0e~u69KvVb?oh35x78V?fpzuVId9s`#-=Mt` zK~vgNj-b#AIU1oB4rH5Xgv7!$3O*Y9o{xrc0ijMNKv(!ft@9UkGK5-4=daoK6u8-; z7VhdVYHEE5^-3MqU)0GgX*zEI8}%)XGGw~_bh$e&d<d_g>Zpm9?38h1W{lKniqZG> zcN5LB!E2fhNuOk=i%u|2_`q9A6i?#hMnXaVa2kX|QjFxkX4#XiYXuhnkk?2;$TI{m z|00jcgOG3jufAcXJv*OvlmA7Ii=Z%;(%!G#+8&b+IWQ?vL@Z}4#_!<y^5GP1;-s%6 z>!;@a@!jGFZLPOe!##Mz$J{i0G;5Z9E~<)-eo;kSrvnIRYCosy(#f7}4^Qkg<9Zsn zBh2{K!Py8uzSz{5`y7^b5QDnQPyFu^=0mH?Xy;Gl!ibFp!F9*Y42tiEY=cl0OYwt? zKqx_Ux^L*xdMzyOb!)0fXDDiC&j45mA31feh%qt_-&rO&t6VEm`@A<2ROepj6Hm-8 z7dX5OS8u#|Ot4a(In>7d$%Uy@ao4Zd#f$p05&Z{O&@TF=tD!zAwODS+dY3C|zIFXB zFPXr+-0ZvUY^!u>E~Q?NVw-N~^!LJ=?sGe6CDtdm)-##K^EDLt))eVlAjWaEv!oAP zRj^!2xyvz6tBYE1NHO;hJXzU8tkNs*V<ME@XShaNY150bl(1{F%F-P(i{(@_N!`6? zt@V!lkK9VEn;0Xlh$4%VZqXa(eBZoPI)YNdwtArvy=|KRhzjTPu%`c$BK^HBa63;u zi%W^)^mDPb)GVBxW%({~iS>-Sri=l|DSZJ*t}<z@_l!)Hj*xG?)1|=-29!(93W&|Z zrBlM-J+;gkV>QnfEOuF-{o}EnSBd4{z%te|-V6RQsZ?TJb<4ewRXkt)FO&Me)Xtcd z{x->9`N!n2?jMt9_Wyq-2dwoT{XyI%)?kRqbT*|wPszTYoD+1F{#bHeMP=mtyv-M~ zpQ5zNf<NsbiqX3xVojLKT3%QrR^B@!;lf`DVDFh{98ztj&9KybctK7aAHGr#i5<ft zK%V*%o(eLVT9zOXQ(YQ(AF{9gV_=AxtTV)`K#Wdx{`wvjX)Z$qn}-hW$xDQosg?O% z-4AFfWH@rM-%yw^QmBa3z3_7RaNgp=eMmM{w1Uw%Mr4vYK?mjy7eeRXyDF6$1AQuN zyn<HzxuWnl)?|=aaeUL@x9?LM<Ul6j!lkA}Vvx%uE#xwpMka=e$O=K*g8zetVk7)R zAQ~Tn<`vYyYsHh@)(=>C4-o6p<nHXX4@nt>?2qsY#YZ+4ZNZO1vTP}yMD;}k1j+_& zz(<lm0RkhTH{n|;<T@!>OIf|d%Lr8>E?_215m{?{PA-KUn=x<dc`jfMtI9Xw9Yl+n z0D-rrRGq4J{rHL&uINfmsm2&K;5YFwHtHzxum>=ZX+}SalX!xcI6zh9JXlXV5`7y! zbr=(1<g+Z4iost&qx)~-P!k~HhA2YuzZgm?<H1fFM?6Oo-sFN&py>a+@W(@5*lWQk zM&k8H_=WIbcvd=ll~XoApf~C!{7rt~SKiCzb)UM<%6TwKYD;-IeyiYIgBNtSWY)Qa zbI*#K2!U%q$UI!wN}E&+*-C~_4Bc#i=D#KH@%KN;J2Af+v>VC}{YN1IfX;9a(GbLE zjX<Ufn;`(LG$<+lLt!T#vs8_r4k|?>772zjxImsN)@JVrw$sXQrAQ4qaFU>#_!(Jb z+lfKANz;g;lNL`(5l`CU&^aVa%dQkuG3-TM8+f6)eT#4VyE@(gnO^t>OX;AwF-@R> zQm_D|X128utQc4jOj_XJ-?HxXMs_c>xkLYOW$p92d*C{MpBdlYX#+fmP>{IziFbKK z*!6Z98SX-$jN7N>SV7J0o6w`CTK8_W<~?|=UO>EzU)&wpg|L0Jm><R<3uu+xSqtZ2 z=<b(eMc&lC#z$nYPqiU)HSV)%hJ*M%hcOC~_wIrB%m@WR1D{n<#-891;JMYx$na)( zi+nV6GH`x6n|I`E#H?WNE=pj3!%H<}yAWQ-2FjZN5d!s1fCgMJ?4l$cI-u2zs1h(< zn&Bf_!dzMogFJK`3M<-N^&cK@6b6MZcwBbKiAp^Oej?<~H}xy3yR>X{rGp>9wehid z^9B7xI+3xAD>eBxtKDopyDPd)eNWPqx2D>Bx+-!L({N4-6Ez{y6uS{7DWwsC!z}LX zvp!K1HP2d}hZ8EeMS;U%tT5FEN5z~ymlnavKAhqvKvE-MV$e~cM}`H}To8w7kT~Up zijfY~6*d9x_ovX|Br{+VBz5;qKT>Pab}jG4d#j!5vlCKtpf{3J4e?{dCNLuIV>){+ zVSO$19KYT-tv709-IwL#oVOaiDKq?ws<}4VM_=bJswux_oz`PSQt`B$-rQV&;QWBZ z&AnzE^2#2qK=rXmPr37B|5Lgn`^uDvC~qpVC=I(PY%eo=4+~LHD)|?K&a|^P&0czr zt~rKBCnZ=n_w;{=&i#Q+wrg&?+MtaUHOF(Mi5l1mRst1+2<$wTZ}&*3$ToZcVWz?! zFK}tC$5#IKm<L7;Q48%vO_;@|z2Dy68}t&mDWCsVDzBG0)0Fm9>#h&Gi!b6Z|Kv|l z_C$|2=|1V~EX46BgGcf@I$ue5`~7KF152d9XEfA&6%$EGx^GJ(wQw|?&2|ZI2|H-r z%EJb*c=*{G=_6$F7u4QSybtf(A$WTwG#5#SjmqS5E=UT!pQ)#^sO?UjemVlKu*Ir+ z0NS(YZON1y&tqOmPX#<Yh-v!6E&2%H>0&AwXeu4B>~r{UeRQX)ifyEvw#n)%h_1qA zlDKv<`k=DkNT&bXNXBsSF*x_F;jn^VRp#dclo?*p{j!k=wxT`d?2#d;r2A_yP`$Vl z#3zdTpvZikw<qS`6)5jtI#g_uQ#Kn{PHgRwPI#|%WY_JxBcegB&3KnYK4VcqJ|hh; zDvzace&p3xlT`4-t?zO7jWF&L=6mpJkmNE!Zg5UF$Z_o3BJo~Ha84P#=ru`c4Zdn0 z#{81TIj8%>$J5@zMatyg`c)10r?^k5{-3HgwCg;zpY4<x?mm9W4K`dv0orE<mZ5<D z2;PWbE8Ck&8y|wgefmFwpA?zz{uYc3gb0T4%*MH_>C`ktjkf4nUac|S<vq0O_0>Gd zTIfH^TKo%m7zJ2G;#c_(@Jtc4Yo`?tLg_|H&20w94tVIkY{Sx~4@3CP;Ss#Xxg<9a z?vr8`P-9KDf%@n_fF0jyop)qLE5nO2+*zkmm2M1sbsg=k9R=S`V)!uJ88@<zXES8Q zKr`GO96c8+dUrVk8Sc{b&LyXV9(<r7u49zoMb%5lcgTR$j=vj@hL}#p;Cs*?v>@I~ zMXI&VT?dhYEVxe)X^xPTkZ*$#P=G;uf|-tNI>TA&eJVA%CBfwVg=Ens)dRbtkda(B z{}l6o%1u4-BtGYq#78_&NPge=R~*5MYLKVU-+7M@q>%FrtLKg3cRAtrz%U)hyqUzg zNm$nLe(zYzv(yJOP4P0ye|{fty(5oK^t&Cvr0`jCB6Eoga%fYCT^O_gP5Gf(ua}S5 z+S{&@8h<Z<NR>DyjimPx5^VE(XSDJn{@u%6S#=c+veI&+s}NWtG&)|5xLAksw_;%r zRq}{i%H4+KmtSvpI=~86*l@InvCh&gK~6B625_6nrf*n$ze4w&^BA?B#0Jy*X5d3# z;K39^`(D5K!do-}2z35r&d__Y^>rSe4_%}>eZwnfqxBrg18x{8))OnXBY8)MhZYJk zw#6ws7_#C|?SL7)J+x9F>eNYZeghM`&7ck)i;wY*sn_}3I00_>5pFnaI)#!?*{L(i zY4u_)Z_uIMvB5>4?;;(I1G3i7>=KRAcTv6c-vHu#86W^3RsII(=)oqT<pX`-b9i&( z^!;UM)_BJ)c$=Hp42N&_LUEM>ZaAhzCSb!$ee4*XWLUEphJ=-DNHuVKBZL4k_7*7? z<<^4cUkZq#-=C&pL%|K}>j;Wu^3ArjiX+AnJI1~AUKD>@M&XOCP=x0L*d={}MDvXP zO)pr2^*6l;so*v=E5tl6#<2g20qPPrYw`aTPRK_#d{)xpwZR6B^W$y@+%Zm^tMe4B zsMov2FP2bP$wg(26vpTtFnyAwn9b0nThG*Wg;1rc$!`OXp~o>FGR$Nj{$=JaV-{7@ z`Zyxk&@G|j-r;(@5b?lvZ>VaZ%=%i1#$x3}Bpdv*!9eHPhY~J!*#XdI1!ELC7J<S2 zL(pCzX@UDFG{Ry|pL|0t2bBe%)5pGzovgPt+Lc(b#U2GrYfXnn<-LWQaVO(M<QB9i z`VEurbJFn7nvEEkL-`K(P{!6NX2gxKcm7r42{4;m9*q;&tl**K@4bpfMs2a*(!_E3 zQhRHm@ZPgAT(D(%r{e2C`vx$wvzwTZnwu>2>qakU2}DMfxzuI4xT^Q>xOCZ7Lc?N{ z&yEsVMcaw!GWmV=wa-bM9J=bg3Uf@-NfTmZ<l)(=<M0YlH!*)-%K^DwRx09!<;TcZ zDw=BHDf&(Cx%V+!c$k=f9~0;u8#1*uu3PM7d!&8Km?&Grzq1Q-<kAMvS_lmKoOED@ z#-+6zx0}fCTR>?mVV9WX-Df9c%Gz(S#!$!Mt)+GBIBsfw&eQUJEM8Z)+Edd4#wE9s zX)%X~vvg9v%W42dC#|>i`<R=Bc|m+i-I4urcyN}fIZT~GWo1F7-S0RVo<~113t}Y` z^S1}wC9A97oZ3FIGTQd`LSpI>h#0Rvt!GEME!pQJF$+y+d|tQVXH>Uj8%*vMLV!I( zRYXNd>qFe|^hThDs-i%Z#;ePVkw=xbL@@m`3s5po5ps*|8{zOVVOuk1o9fT*=&U~A zi!bj%eg6+th&6=D=nqwhE)yPXoPRgrADev>A@@H_u_YI(W^Uy#D=U`>(}%}4(DFYg zQ8v_nT5Oqs`z-<S+u&;P1B(%ykA|`wqA3i?NQ`dU8N5Spc4&PSpWyHqoJop~u8eLP zw}+*v$A{$Qz+{~?DW6GtUS66{EQ_6QDuA%qjF3Wl`PP1#zFN*m(8V^X@U4{wjaVic zEL1@XVrf2zuqmX4aE7g1iJF##<n-2C>pc|ye^m4?XuE-S;+WS8lG%DqWE1l>V{=!} z-{?y}qLw34|DADsjEIe`jw7owXlN|BZEs>Zi(LoXB%_ikmDM8Cbw;-Z@)ikA`asKZ zQKs|6@C|6-;eq{pcnz%-!~0zAzC|zGJ!%8rrR48x(NKcDn+zJvx2VkQ$V1(tKNjc# z#ZbBIGV};6a8tfxikzVOBE|(-@D;UOVM6#>V^my!fzkX`w-@In5>#s7QJ|`*y4K6* zpG@89%N`U83=G{BoO!EAkt)U(WE9-^L3jm)j}+Ps78x?_g_pe0ufk^d<~FY=3Gbz# z6pY*zh|Jt5_pVe-H7FNG9R3~_5Q526+mm3lQ_hnoVdU-V$O5czmk3MHa<ndmI=hlq z%_CRMy9XKghaek&zr+6QpVK56wu)w+D`9NQAr2pm+%5*cc-!^WsoDmc;jY1GzmAaL zO6w>TnMOEiMim;Q`Z-rZG*{wE$>jk7cj(1!Q<kf(f|TL6105_bvOfV51E$6gI0mmE z-p%80raR?YdHo|Oe2?_0Eqb&Xbk^5j!;#Nf$l-7yS603K)gQd2m^!Auy&^@SZ4%uc zrSTnkmr5jXxQGn<Fu0YGl_-1VT8j)5exkL5j!nv?s`ZaTXt*2t(?|^0Fbs{gS*?z$ zeID;Y(FVNc&3tA5s4(zC{96TF_L-AjOudU{g)Od7z?k*MSf{32>3Pk-?q>|FE$zIK z-uGsz3*U;nPw6qdKdyzoWAKd-3oNvA!8ZI#@QMiHHo|US#bL##^AT}=M&l19=PpqO z6}*qPW)Z!Z17%3$$b7JUVk4C534VFAUlfRdfa!>X3}4PGDSf84jw-hlSSy~jBxc{C z>br0B=j+KMF@0()<RM3wI@GA6z=&Zh)u0y<O^As|J~d{Kk`Vu<pT`+;kIFz5me3V% zT?n;~A}1RZFOelHZr`HnE89mtNqbdy?(YdhKleI8L>UHoHqJwg$KFy+6oW(LrPFCz z5VGtw1TdPs?C1IVFQBTwfRO(Sr~v|$_hNbqmP7`NHb~1FA1gEK|2FOj{0m`QR~Yi$ z;%l5C3k}x7{VFl4Ji?_GcozDuP)s&Yt1!1wyHvPP_0@Uifimomk3u%&%!AY};;`lI zeyhSCy#rgMggTE0`#xm9t0Qsbc_fOz!uV0YqlA}0cBg`Gs**T?#KrN=WBly^-j~n0 z__$^;HREHnG%_R;Ac3%7D#!CzPR1V!RqHW0VsJJdDUj$f)k?AHOHXiYAaMc5*+7T` z%LPRGyXKDg11DoHHLDF{ER4>|M;tj}jLuHVu<@SQ*SmoK+@Ejc9X!j=4F%;|g8)VJ zpZoK*t=+6XSXh{wy8XRX-`^mmK)U|q@gBG7a8M;X8H#dr+<)pV%n)FF`y%b;PNbq@ z&LF)y;ZUBT1+rUTEGO{oNSy3Q=m{{DUW_oljnj1)J@O86c9xfS_oIK?S{2XH?K<i! z&)u#=8*oXedLd^$;Ofk9+mUvC_2ecByuIb!0O)?*o!&Xkc)dS+eRw(Y?0C+Z9$z!K zd<36%)YM>{o#os>w(x_xR)mF^4e>g9x+=+<GuC!zUmP(qQam$Ma<nkUJq>wRhCQc@ z`1qcB7EgAK+g50XcB8;&QESg-Z7FDmwyaSRSFi2}A##_uHvZPzH?cf5<L)k(vlfr_ zOSAzFqO?6rKyTk?9_uUjIz#_Y!ZkMm#rF3p7Qldj*JgtNPn+$PasR@edzQAKGhl$X zh+$`0`;AvY$t(Y>$+&Cca877OL`%w!Pe5yz=gN+&%RmPB)Eew_!n?eXkf^dJ%|Om~ zRcmM%k^L(-Vr6jgcR)4T<?74!g$(QTuwadYGv5Zm*T&D|tX>Y3j#qxPy665$N9lHZ zK&_*d9!Flsn#Bbzo2uv57Z+<rT?DCL0x=(NetOvOUTs=mq$}p8M4SPiIze{$<T;6g z@;*;2AAUavye_U8)C&nakoE7B46a-OQ^@#9R$l6bNd?IS$c0|rp9Ddbw;dfd>&zY) z74tQt&oos%m2V|QCl)&9{aq~DmQE}?=Ba!+1V|O8MT_t`0`6;9EYaUo49`DzWP;ZW zM>Ts`v)9)JrzXqptFM1wnLOY7xwO69tdrN@rtx31v){h}>z;20j(_#vo^h->Jy0)x z{2ZjMwcdsI;BedSr9<jqc46J4xX^5Nr(Q8#fx|VmB>H+$)8aOP=lH1f!UAaU{K}g~ zMvr>9*>*chb+g%uPlL}S3VDioSMT>!tD@Z5BdVi<{_;3%*jzEmqr1Aku2NDGLY~Z8 zL9P`SHx7P$+`R7x&mKD8zh);N1)L?vEpEROlkdKk4Mt3^_yqXnD!dUsBP$RkORhP{ z&-_#mC>(fleW=*GrtOHXlO%SmIASKnz)>Vee|c?K+iWWsQxs;#KdAQSt!(0<Y20Na z#&;a1u^Fbev6PyNRhgSgEzLsuD5yT@agz~2t=3@Qs-B&*CKh{U$&0xCxQv#22lNOb z@6K844TCi8Z$O#|AWfV%U{PNkl9%rLO<7d#p6Z+F@n2wj*}BZ^o3su?yp_SdlP&N8 z%MLtm@nQP?`Hl0m=d5IK#!KzGgW8S0>9{kZ-3Qhhm8dlvZO0fBKA@5=Xl=cvVpz|& zAmO=csGoH@Dq?4L_ujd!>(Tvw7*PH-hu4!lmZTKei{!IYGQU^yY|tM;?#=W-1KVf* zyJX(M&@u0KUMrImTD2j}8WGB?jz~2KFeN<L_th_@KUUpwSiLS09eH{n`df^X(B~SH zccgyVw1kYrn_dZ@>#<ruk*f^2EX{^@yIVRA&C|To87CzzmZzHLOuT!n9(QEY4!;be zovN5k+xT<zL(eQQFg{C;!J5y{>-2s*tY<R%V&RX+CflHjLhw$f4!&}fdF@s2rTbSF zQp;WI8AoT!4c=M;I+h8?>tplsVxn1uMEjj{mtnQ%xS&WP3%u{-Ng+&#N^KFc4)u~g zd?l7$J<I}*r#?_YYNa|*Jos6=m#9yg$tcE2W_QD1Ly#kazMM2|0Ourj!Z_YT#$hPp zZLRc%B9adEDhyvGmIL(Uoi5BmWOJa8!gqbB2=RXAzyYF7yk6rNCzOrzv9e|;baf3# z2mLQd2gM1bBOcOGE8Pn3^j{Dp3y3XH&*{WHG+TcL{S*CXkk_O=G)ZykAC$&F@PDEF z`+$E(`JYt~aJRrarRM*F;ZywS08N5m-2#Ok5()w1{tvDHlJpDakQvJnG7Jea%=TZy zdL125HrPHi{lQs-;X|GIgY)kL{>^i(bR+znJ{1Tu>@8&2A8Y8gE){YQ=r>oFv5#Rp zlso$GpK9b1JE4Gna{<hv%q`<!*x@5)9IfeT2BYLHv}>b=dzax`xj_A35_aE;xao?? z&7x9KN7QKBb)(n4r_+d?ovg9`TkL3JIZ>~1AjpHy<Ka}G&F^*3cIMFbhP!Hbn-`_T za~uQz<<9FK@x;aV;CVHx8&@c+1N3;nQvIg6!|Y3TH5uwwo2dKt5gJ+a-1_nkKv|g0 zY<&_;++6V-7iCuY(#F!Y3BHW;KN#Jce;B|2y?p&-T{rp3g*5UiOuY^AA1YIRSZPmS z^ESU{{<a>^FjLX0g>i$M4e-Ed+_*}H{fs~8ci_~l=eHE@Qz!7QeGX9Se)eoSMX+sE z;*@%9b+Kwa#eb#!PGzvA>R@!nQQx(v6S$I2HWjdv9&36U=Y4uXKJ&cs#8fT%rQ{79 z`7gm^+_N+M2&F9_zBPhbe!{(IGm)Gu9Az_+>?||iqd7@rBaiaFV=K4@YPj8}2TQ0C zm7F)O!G24W@xB*k1fk{>tU-;2=+-%SGY;H^1~Ks-k8su{j`77oCE92zD%3*4lwJTi zYfFlwUueskrx=_YLgnFCvecO}{t*q9BAKqWZbG~wUuH>;y+~$Z=4%kjYp_az>p=n> z<fW^&wiOg)+96(*Sl_q1Lzd1kBfG>OY7n>lXJWJwU)$xcq~O0JW@}J;<zw|Fc2j%V zi3HDwg^O=WNm~*8knq+++bNT#Q+Y(+`FyXs&`x0c;aL)<dQg%B9XBXVJ+^GT<R$`D zed1m<A*LL;Q_}fdOZ2CRQlG{JR|Jj{xURTkz<vHo*<<TORAetV<LP-@Aj{iOrLTKC zQRt;^1kK%L+3)`8Dei+v#t!gG?|F86tp2G{6u?8GSkCg)x_rjVO2X`b;dnF0l@_ac z(XBG<wb2Ijy>DI3Hu+lQJRs?ioKPq9-XV`nI<4yW-X#B?)^b($^wku86a3SAltbf2 z6@JyOaq<>^RnDW@-mS~?W(6nrNGyCAHvCI7k1343$Rh(|UZ&&biK22BUs4yxD()=X zDF-umM0HF~Wnl{vmkB=MA;m{5aq-YrFrxD)))oxqJmN>U@z@%5_BUWJ`4CYLtJVcV zVvF@F^_5)1kYS<O4%LQ%vssd>63?+0`L4o_?d0t4Z>|JWraf0vi=N)TOu!9vFT#E# zoTIrqZ+)R0ub2ymPv}Df&h^3|w@(jJ%4-W=2d$k{K$ovA_)MVV%#<9yB9z^cpUp-9 zXn;uFzSh{5P*s}zLS-T;JeDtM_wN`Bs$CbY0UJMu4xo7PRq9*EMpZcz@Pyad(Na=c z)TF_Ml_94Qv?m`!P|0>u|LjF$l{5d6MsTu2k*Q(`<o8|;OYR7e{ol;<vDJjqX-@QJ zlskq84wM@{W{mTP6tu<Zh7T9xxwYc+mmF!`$UJcYWN&9UJLrXdC$y*6C71c#9J)jZ z`mcVOce7eH81o_ByVvIp_{3#d_>T!&x~t4EtES{_pDtaZYXm8e5+5$o;(5ztfdfg- z@_1o$qKMrefccbp-0K+bSxOkSM4O7D{qg{U9+Aco#_dYl-(Jj}Rq$WLNvk^!YCq)@ zA13Z!c4_f)U3*o2J-9nxGU`cg_~zo0FD19%mu~+4^kS#IWbnaNZv=eq39CqRl9W)j zt07gm$>K^XnvLGtZg<?cU`}JZmp3kzJXzdf$Cui;VA;2FbyaWT0?J}xtGIP=kNrIo zm$IIMLOSVYIln3lFkZ!}KKNL&LspgktAvc(K78@IbVV=j#m~#UC|^<qUuQn6Ai1N# zW{kYX3OFXHzMhqgos~`o)<6AOP9JdibEu?KFD_R9=%@G0wav5ETX=rAgPpex?TusZ zW;5V$ui3Va^4;fqqHT+A)e@e<DGR|Sn?pl^`NmP!xw1E7>tZ!8rWdtq-=aXQ2M2GF zwl394gFh-6p5l{xwz+fhJH0sn3OeBIcJLT&7M-$FPtrz@Saxy+7j4^(v|p+XlX`Wg zDA!8@yvvSayvWBDywrjEZ8ur68oZM<H_Zm5@O6@nCqD0LFbqDy_}AQPBf5BNaESZ2 zhVnr<XUQb^tfFlwk)ht`xl~>aA75_MzkpL8X}(AfG@-;7Bv><uVy<3n^=*pYL~ykC z2AsrPe#{;4b5^x|=n`DZ99Qi>KDdJHzP|Tu1At)?`NtnHzo##9i0r<wQ8h$T2uOqX z3BDGX&SAK5rEiNltv)|E-;NHioej4VC*De6VsHB>Mvt<yBfL*npr%BOloO_S|2r`C z8WuC$+#FMCsF93N-Nj7tF$mQ+k))RN^nNS;K0PKmVpfi-B;%s~tP}y?r{3d}xkepE z7okq23&%I@ug3Q>gOY=BUuxE2j$1P7Yc5pw%(RihIdfvvN<Y)lkk%{_ryxU3ytHQ0 z9AhQ1(zIQp55jM0u;xmn3Tos0c>lxPF;A<+RsZ37ZuahgUeG+AlRoZQaZNHTR?;J3 z+i<KRZ8KpMP4=)(emKY_F_bt9=O~L^GV!tX*ju+N9<QSFM^(uHp4r2wk&919ee7Y` z7~F?O?b}NwUE+Rwg&kkDiXi{04E3<%BTYB|(;4tR%d%Y#_Msst@2bygBgxI`CGoJO zA1I4_o(}3}wvbv{`_;y9uc`rLsj<wQX;;SemgO+dL6jM?cQhMo*WpeY7v0<QQJ(-# z;1x2Q$Vzjtehlc$VoGm0U@2J!?)dN?BwU>TDmnPtXk{RmQe1-Z;bhF0E8~4#`MNZW zxXr_pYee|(S$c$wlWiA$FZ{!J<+`CsLbKIOzpV^vagq1>8n&T}JLT59nZ+NglI8^n zf9+sMojeuKOws$B86S6_nFA+Q$^uGTd>MvyZ?Y|m=V~8dHc;-|)A0hIhK8Pl$L}|k zV6rD-wVHcLNG<~GyAdFFni2DcDO03)DX9ZtpO=53xHIV{CLj%7{K6QfDxCA_v2aF5 z>#<|4^3${Te1wy}O+z11Oz7w0k21bLUiNb>X0!crRV;?&D7V?M#U&`DkDiGUwtIuY zYc!9%m{H}~<<reygZ2Zy^BYLan3lQccT0i-dM|^=$;Ae%_i@$_E%@F&`13bUO%Xi< zFsVX6grS+ftFE{Hj6;GYcxhfeJ(E-Y^rkU6(CHILHVf1Frgz&CUYp<LUdjBG_=eo8 zK|Cor`tGu>&oLckhqLi;Qtyf}CUD8_wVz@ivZl1Yv$8DLSAA=I8k?e(w0#=RNu5^H zb#;nrnXBvbii>DAP!yyfiL7g1sCJCbRFLFk8`#&(j<{I7SSjE(!1o2YvVCNJV^v28 z74B5Y8i*b4tNwMjkoa_R&eeaB(RZbw=M-&EpKYdt?M$D?$#DauDyc=>gwp{#UP`k{ zY*cHH+TV4+T@wrWA6P?O-DK^(xc;s>Hv+`fj2Xoc)Z1dJO(Wjbj5<;GReoPyC*Na` zVh9C+()h!NwXRJ&@>8@`VwVw{f@Bj<Bf_RoUs%-U81Yu#=iPZAP8jw38V^^QO^}WN zL|Y8)2FQ#v&SR#Fa1i7sWO|0Mn5+D{nlajYYoV;$Q`*6sc{0!y<SU7;Yd`mw)nq5f zzpUOCFLnw<56E#DU)r#G;+)154Tx=kqTyCE?09CC+j&o&wv=FR9Gu)$ANN3BeRd+Z z|NrWgxuULeJXc+~TY2#?V1s<OQCfMIkw0d+IZ`d=V94k5E-K{*oGJ-rKLAEqbeX6a z8&F)EF>Vn~(fG}pJ}T4+)<)~~cR?ep3{k9R0Im-CqC0C-Ybzj?06{9Db~>q2vDQ6S z!k?*qYg%u;5G{A-(2~-Z6W1ox(An3P?O4L~R_=D5yFH0)Yan3o<4dd8-KBSjw?cE4 zFHgkAYH(}H70j=CYqq48>1;u?xQv###e>s|DjJ_f@ioT0g(ggga7qVhQ~IGGf2Ac_ z4=*T0eJIKkp9`}}<(dLTZ8SqBU)}A|!a>?>%Cd5ES~3pf0b1J`TE-3LoGxsDE;b;| zFWHKeKbLrlL#b=OU$9|DaDar(4ZZQC2%3NyR&rjyLa{dsyg*m)nBr@DWdyn3#lbpX z*mpIJQW2Hpa2!__es>ezvQXHf)vGT;NZ;A?zLWdDa8W=_k2jMm!{=vMi$zgOIKR<8 ziPefeESnxeczKIfnu^d-)yH?(5iQAGl@}W8TeH*gQd{-v7~W%>Ron+vxaY~!RmhH2 zNRCP(5iPN(YiN5=B>=b0D&2!BV{`(6H@|KcprVt5@(!ysi)E&LY;oFtZdRGI+6uY0 z-Cc9OV{Jd4ll<lGWvk?43*Wh;KG?5s+pnLfB3Dhl=`}z<S%z=mWvk_5n+h8~71Z3t z4OL)nRO(}!v<zwh5q&84^s-$ZCsO&&azRy3XbmziA*&x5(6_zrXc7Wlra3|+ZdQ$2 z>MVX0?b<{=nTulS<Yw&Ro`9Cz6I%fh1^(n<)97j%`wiB0jG=NB?rb_XkJ(GY&jE&; zRi$M4$bP|atdeuA3T*MCGK5I<vb{vwb}tNZ<l7Vi=c71I$~&UD_Wt1i(cFSy)QE~o z?|5>yGH;rc;=xyQnrV)m*vl^|@RB@xK~BV~vG-&4$&n~MLcuy)p)H2@=$Xad_qJB= zIUODJ_?R*nz_UVbfrL3Tl!9cCd7>3{Ji713B>egfF!HoB;E=;lQc0#gNNK%le5eN8 z&s#>y*NwuJq0hTX)X>l4zP8`WAA{eml1>_oJ&-EDSh^TldN|P+-v?9T#2t`PoQC$x zUWq@Pnb7NtG5Jm`<!AO9bZ8WdXPZFRl0p&pzF+SzO7hD-R6cP3HlIatV#zA$PAge1 zSl8)jfB=_*K36iv>=%D;e+xdLXH#PUXQ&%XRoE>lLz5uBFe@A5s1BfpUt=7QyY&t= z6{=H0K}mA5eaD&ofwoQ8zBIO2HA!}&%m#%xC*ex8!=@G3IoS&(<{&!a9)ZZ9@WnzB z4u|Fg7S#wKJRv{)8;<$?K|3lU3mM&)bp1O_s#*i=+LCeu>&V{BdJjGLA&+tvDybh^ zIMVZg38PR%Oe!eST4&}1jYz28AE>D8V6HEcyk_`rqvR*bk^fGK3H=Y{(UK*E(*5`^ zN`ag3xjM^#QD!1wQi(<OqAT@gtBn;=!noSd+u{<e(1T+z#iP_yS*47oafVPGj1j1% zj4r4{(3d)r-58{xjI0pCbA1kFKJr1_mX|o3T6s#qiRI#@&(cNI!Th!lM}%^D6t5PP z+PDZutbY)efEx-$yo>4@pK$uRV7}WT`}np&ztFo3GpH51Z!lyQN&*fMYbg)OX_{b- zQR^gKlZXnFGtYH-hJo#}#vT#N%?$Y}4DmRvKednfPY#%XW<Q{P7Za4j5TQ^fdok5j z2KX|i5~HdreGYeGJ!qw%;B4~vV_XOr#XuI3P!uiXzuP6@s6m*0h?Fqcl5haFjA;cM ztvF<0uoMG(&_dDM&q$4|iPDO>-fent{RmPG-!2HnBB3Z@1i7G32@8A&%MBXFY_Sm2 zeze(H;}YC2@`NG^aD?&OV(D;(v4`lV_90^FJdNbS-c-|MA!9)iqz6%!W()jenxZx9 zNgso}8~(jC^LlVlZD5K;QmS)rNjMP+xcA|RTNiV;@;`R)pWrSCS!UWNp75*<O+2Ew zSaGQ^c_~`GoQVFr9(hSPllN{c`g-uvFHLBa!Lf?Q-Izhb>I9svVT93`L4$Fq0!C+g zGi8|2fq8@kCWqD}(g42JKLrE+&jnK^rMerwbOzsCLe_m#SDzrW0zQDNXZ!vsRS+ZG zoBAYFOp@Gtpi%q_<Caa&6SX?<xiuNyxz9L05@C!u5gBbPBk?o#n115pjo27jKh$m* zG=tFDK+r&7P9GNe3nTjD@Zdno^va}uf5cVtif@C8ivU+=BUg7TS68R(SVWitV(Hb! zxnYn1`I%uFH8Jj6HqCFt(Z^5GbMzxERG(*wlQfvyQrhk*o`*l})(=PH_9-qg=p9H( z#1v(XR_%Vz0o-i6?M~M`^;PiBRFJ=E=<6(WDMIK0jAl+~)tIT8RA|@J?yQaLg$7`} zc4NH)fbv}3BaO8aTs=PsBte~C9H`ZC@n~QBW*vuShgXj$S3`%_+lR^9jY&Sxpa5>@ z{D4<JSb%_E1F@-#InS~ffF5?hs}R|n?F17I<sAf?rUS;b#O2E*RC&8Q`J2K{pl;Op z!zP$_3p}p!c?drZFF9=&pU8B7#gC21%dTeqqovLH1AixQViGi=<n>(T`}{G}t5SJ1 zI7Jw_X1x;-x|nlQ%eV`+6!!3X9`bq)63kJNKU8Q<BK|g4Tw|teF|N%)SGUQyoOt<Q z&fI?#vA7<&$SO)hWN4<RW07uX6`kie3pwvhdyh_G{gZn!LQT%V(ci`v*Si<bAfI3c z-Sc{xO>o8{MCmNq&G*iiN(J<qb%L4zA{7hq&rBzp4ytMy-3k0@%!Z@6-@|LnRK|`{ znh<Nq0Rhy2o~LhCeqXJP)2xAc?BU(G3`Bbh)&ob5{r|<%zDPc_`7Zq%lh<;=`NQPV z2e0R04hadsYtbQQCor+|<-$w^QTPUdyL>gbkjJ0KRgD~6M)2*vP-QTC)$=(4Zxal` z`+k&?^EQT+gPDnQl9g-dM~lSQem{3buV+ZsQZu^JGR_}XZXc>@yzTp&jRQWKM|Hg< z%ygLpL_a_qdOa_XTOr>mhxE0Yg<LZ7(v>M0j%I53qvM8f><DNakK#fnH~uev4$*f| z0Db@I$1#9Vfw~#(=;)o^KKRP~qCggu0^ZvEjlzFFP>)8hqHW`;Hs!G)-XHP~Owo4X zD=DI@KnLBA0#&YC^%1rTD{_y*?{Ry<k3R5QNXL$LCbg^rekR^3E;hd0f-JOl3=5DH zN`utIM|3Iy6j1Ugm|6JBuyAyoHiz=5xW`(lneC-*VSKJH7n;DT5wPj{+{CAqq}b@6 z4TVFOE2cz>UG8+Cs?{3$DY*;Ph444Wi23uU%|QaAIC&%qwg&uS%n6Fr<+dkxD7CD3 zr|VcQDNfh#G?5lyDA=@cD+C+@nsn!GJ!xGEynNjojz|m4Ns4r^YZ$(1pm6zjq@NRj zR0*ySD~r9$MQB-fiT<C0GnkVTSSb|g^RTF9MEQiOM&GQ4v<;W@Dr-=GGWilpasI)0 zXzPz+?<K>n53{mhRisQ8=W1}hKY}V0pxoRiwT`sdja0y#JcLHzK>LYG$0pCU&8Px7 z9h^483SuJvFN}YgI24oQ`eOp3>i=yb=DSpbLKom~1MIm-KT#Pw$t+e1I1Vu<kHhHy z$nszbAPv-@eMizojrMbb#2GTj5bhN#rc8&1m;P+gooeMl?+MZKhaZ89GTk_Au~0g? zq<i>=<_TtkGX3)wPCg;w-)3C?S2I@HT#wdG74&Io0CBbVoR4~Xf`>X}_5A1f<$5{2 zIgGYhuw9dS`h>su9|BD|=$p_DsufchS=Ls(8bD_={B^!MRY0YV>*we3Ezyh<j0Q7j zIY;X>w%28crjhJu<>8qtm4mPeLy_Z~w1K`u=lL%=rDzu4iJ2vh)m-Yr>v%~cDlMIe zKq0uk%jF+6a11Y3BqdMz#l%oy<jD0XD!d<RHd;u@>3xqE=Y3vZKrxe^Qcc3~FgZI6 z@BzP@zPWFRJE7pGOFN;s^QrhdsYj~&^ljOZzr%mmf|VPz%fLZ$3T(D}C+-d%D!uQp zAMT994hv|03y6s;RQ9ewY1^e|{eY{j3hECl{s@x5ty-<$!c+xJ-H3aOdlN$w(bZ7R zZk}3>OTYbwFJ={bDec^kxxD=SQsyMDlZad{>ryMlcw8Oug~%?qGYmzkpfik!pv`=c zCbu*5B;+#qk_U!M74V^WU~M`%<{cet;1ARtZ~+|&`+c4*<Ieo8?BTbT_DK2cOEnEu z=pE)yRzy3@X0=nyjAYO|J9gyX@W1_Wamm0z`nL;j=uiuQUF3kA#?#B?ld6Z>zkI0Y z?m>LKxc=kgWpuTkgX;ew!aZL%Ghh(>pqi`(@c%0n*r|U~@&EPp6;N?4OV@+@;10pv zT|#h|!8N$MlVAz%?(PuW-JReb+=B*4un-9L4Y~Ke`;z~ypIMw)Gwal@+I3F%sj8mt zzE=4M;<^Ov3e5kHD~61Ag9dkD4?^k^F08#NJx0gQmk9<Jt-Fknn_s;Lys3<RV9c<d zP9L}ClZ|Y!vulNJ@19)$hGj6m{J^zY3*wgESC6;k&n6IXfUvd(89jJx;aAB9*n;<w z9Y8a&BRG1a@<hl!Wg`{!LgN9?C0J^CIQAIyEyPNne|LDsXZ5&gSDaE=nhm~a`G`&( zc#7#k<8-$Jh*ufIa>aWbPXn6m%foY^nM>Eb39ly-bM_yNw=N_K67CP1R`HW$Y|X<? ztUJbtT8b4Z^ph@t(R#pht2ZM^nr}$`QWOBp3qEhF;9B;6$@Hm#6&cNjmvlZ}tN-!> z*=*7xlnc#9`a&VXg;=akdo13Dax7Tu8MRiDk*ONHq3+oD8MVSs>u=OSf~3MPzffy~ z<9cV)1+KDiY;^EoL%E31tYs>;OEk8WG#GrfplX8)Rs4Au*$ROHjIEMsaG_k8@J!e? z0~n*PMHt$d7eZ0+2M9vBbbLR>DD%9oP^N?PFV*KiM6<!8_v6A=YhzmA1n(RAx2?(+ zAM+~ep=x1`QDmY>oT^NuVaT+dqaY=~C!p)lFN7wE*7|F_(W^iwWPfJV04OIZuy4{T zP#H#4?R1?cTVU<#uKjSWSv;7I6AWRD0vD)*VZ%#5z?X=<L%JYbK~@T)IUi}pZ~SID z!X<kwQn4*==`l%=N3O3iJz5pPWh@2@Rvv|8_}yfF0>?&8mesf{1;{@@5G-q`@M+XO zf@=a5YJdO;-$=ROIM3}yYm*@gcAA)qrh4Wvo=T&7vTgw_;{X9DFgcvdc#ozsH%SlI zW*`{Z7-fKfsprebk_@JW5-ijqH5OvmP_9V*<_{ar{~AkxW%Jmj_ew+4Pqvzr?vc}8 zFXJdPe;Z}i$5#Eo`;7N(052SCsbO>nTWN~w?!?Fd&R!+9#zpYS!$M%EVA(6w+eRrD zy}`aR=1T5&j8|*kA2pH^T@kbk+Qg&CZcpNu-6g#5&4VkQ3gQKUKX%iRXMHjgY}T@y zryrr{YJ{uObEUYYwK1_8SiSHOn24T(y#_TbJ~lOW8Zi`b1Qshs<)v~J+mlZ!lGq3z zUecY7DQyfZWJ;-Q6zxY%AcJY~nG|?SlZqm){F$Oub?{5LsYjW#avZW6n-{(UakV>9 zq?|Je|EGvtH9cf(7nP(gHRRDmYvo!R32X#LL=_3`h}`g><ubnX=4WD(*x37cCHZk` zY>^h9)23ea+UABo_h0lHi((_H65*b-x#7!W(j~@QlLra$<8t=j$!W!~lg3AsB&Y%~ z)RafEvD7JRXr-~unEdI#OqrpSrS_uYtE;7&<c3SC40_-N*Yo{=uJ@w{_XKWbT3J!A z6tU^*Y6IxaSqkWQai1f#?h|zue-#*AwQ-rGLRB_eRaNQ1-%$$@en%w*22+vD*HEXT zsQv8%-5Rh9;*fBjT`<ueo^VTgwG^>c;)$N<si{1eIo}>m4{Nreu2xI)qbk2mV&*M2 z)h~NL!#pFew})T2Agj$tm+=w?6@)$n!`b*R3uAK$#`CLJOp2!zhhwE33S-x&%<MX3 zzy>&tkRNJ-O$OJ3$7Rgi!6ql<OZ{OoIppsq^QKOj+v*Rd8*(d2M8!}lV)F@F<lTgu z&GK`IM;n{)F-wtTrsL2w+}n-SXg)4bmC*ZqR_Rb{@DRZm{Um^A0q1-w&1CHysk{p_ zvrA&(fKvQrS0w5c&r;A%;i~d$VS5~VN@b0nwEPGooM{5-wKapnvz8!SG%TJWo2thh zI^W`VXXB_(=gttC#B#%K8YpA3kY6!Ra+0ugX8;hSgd{?8D)bD8?DuI$M5E)bXx60j zi8S+3H2t}pUWsLD6tit=&SrksW_5H(oGdAV+DpEcU8aIiXm<Q@50lc%)R9@K54Es? zgz!8+1?b^<M1_xQzoBfyZpBq&5sMeZ^Niai*`JIE$<l8)COCQA2IqV$hpG=!K8XtK zzYIq)8`6^IjddKoOa)3pRhue1Ax4Ujf2tHdR!OsiSxJ#GWti^J-Ow?D%D1w<MHcQm z64%hdq{?*12vQziH;8p4-PR4>aQsMw0gQ!2DE_wP)JKYZbL$I(5&g=eSB8qno}HOx z`|R=^q+RXpFP(8C>v`k5Km<4pkjLyWIMJY@yIO-Zi5+I3pKenlP=00_j0k2*sRm{W zQWAuUb>xp@+K@)Dz*3am&@ujwOGzZno07+~#ugF?RR}hT&wZ9LhU}$y?=BRMH1GBN zF=>)x%ScN{VE@z)gsx+o_gG#O+m32lPP7aa!hL>$s$V`U!gi&|z$Cp^>wq*tOK}v} zNr`h5UrC8|6i-iycN{>0m*vfbCEa$-508k$h{habdx<yjh2_~(%wi5LCSdcW{Q~D) z&cq8?qnEPT6Hv8Cx0h6<BGme}sXt#}0EIR+;mnZ=mIx6-3NF&bv~Q6yfRvdRrgYDR z+~tVTk{&!t=WX}~<|HJk+;?fZypR)PdwuBKr0GZ@Qvk|LkeVika$i{AQcUsM0}R+% zlV*n~Z^y~D0d%pBp1^EJ9EsYoDLBSh$CFYAnGB?ml*lnT-h1yHQe4pGUuwxq#kAwF zeyQb8`&{}hiIM)jADeWd3bv(QnRA)&9ea=pOnORr1ih_r12TOMgfwqfiKlgbAf{BU zCPtCz|Lf<M?tPX7bz)fZf`)#9CW`A;DhMGNf9nOLPQ(}=P!UBF@nV%ad`O!+rR83q z<GVQIn-HM(Hf;ooAgOqU<f7nDh)oBssW6y{yVmaUKt2>OaJ6Wf>3R&x6u>AKtnbmJ z6)P}WHAbX=j!T>AtdjA><=4=1BiC|7uIE^QF0FLoNJcxbQ`F{XNUaSX*Fv0`U7%YX za4iGyZ}%DcUle&69MMU`n5x^IeF^0*$IOc-w~e{=IT(R{<x3NAMN(XEmkUHyBaF80 zG)FF8UNKJ*_1(UTK8&T9c6={&^|Owg92m+=y5k!v)=Un}BOIbCT|##Oyc8Sn6}DC{ zS<T9+59KHQGOd?_=z`-Ef<vw5vjhD3BRRVdD-KSMHGBz*Hs}rf)Q||#Mf-ZFAYd_6 zFqN%){xfQ{@cp*R0RB1L0128;E~o!Z0K|C+EpR>dfsDAkL|_MX%OX^$LB47qyquQF z-6l{drffEp)dJn8C$rIFlKj#4CIE$3;vV80ZjLSd2CU_AZRs5RqxY0=rmgFtLSRKg ze<>0OOP)N3Gu0IZpBwUWfTWGU=d6HsTvLyiW&U=SEL6xhhwRZ1x-}hEgw@z0h)Nm| zaRar3np*#6Li^da8i?XhA<{=feo1s6K+!abC{n-U5VP)pb36ER6}k_r$@|ieBm#an zV%)sG&yvq)PZ!;Y;e{kv_UV?q8WN4Vfx5xpL1juP2M>eDhWmKj3zW%H^kst0cUj2h z`=yV23+vez@*in;P;;?X$cP4dS&1CMKKm~!6n;1T@PBzs_&%@v9F8V^@HWx?Ac+4- z9r`bi;fYjr7;@*-3o#R)UzX5E!Mos`!;Ncu?K3_bw&bh&Oo^4a08Cruu=<G=5xm5C z;Lz_}y!~zhV*bNq=0*%$^peTtojL;(781Mfg?Gk|c~0TF-@SjKUW9J+d^PxNrk?4X z!x1`yuf`7Q2NN(ICBz3b;C_9)qPvzo&kkx2*AA-rI13oCW$FE`RpV-X0PtwJz29n& zx8FUMZyl6LrkK2IsTu8xkJIk!o|$15(C-tZ-DL1sJB9MWJgJ2$ev;pyF%6mPl=?&b zm8We<YaC3EB`JF#1vM;@4RL)q+tiD@vj%tF*;t+AGqd5VYGMgH%X(Bq2nZ~^6%{0F zlx@~)XXRXBe?gdHKXr)y5`&r^R%<*-?e3c^m5rJqA;Iq0w{A>ZwEfCW1IpJsBdms+ z6~X1mn)p?9jE)uSA;eQr@2<U49Fy@sFIibEf0sH>%z}pg>O8f!$6W&mX_bRQNLkMI zQ*%ux1{E*+&4HT~cd>^c9*RKI+=*!jCWs@`oOj~gQrNqX=S0n>625b5EZ`m5TDy+V z10X;yZ?pEnFvZU>#ec!*fr@Wc#)zNgUT5xGp+OhWxoos9ZwG>*@j)&mZ;V+kLV)dk zwPPz`+njHgtX|xye$2Z3-(asm#eByzeOAG9wn1~_s!F(jo3k9YzC;|o^v&C&A`1`X z5|XvAe4_A?<2`GS)z_-IHCUW2&bL;i+nlQ$vEd*WnY>HF;<VghzGo^ZK6B&BFu?x+ zW%XL_#K>tt^G-ZCr>}1hWWe4Vf=TKXx1unD(PoAFtePxHv9tE$zhv@&I7G}qyc3S; z8E)wwvMD`a!;p(xSzeyIg6EH!F+gVSzvr8XJD!3&(SpT9c1&+S=4|$Z;hoSUkmjAZ zMlLLw^VWhB>Liw@LZVx>5HxUo+a<ehGWlVXd(wpY>|jpcNi}v@%^(d1bRIt+)Sz7q zqTQV^mS6L}G^=!RWpkNTZPxEa_(WJ;Q7EMn{rycVIghiB31Y54eZS7q!xhQgx5*bT z7Z8a+Wb@N3lGio9eOLniMc%Np(p>rKb$fxPcJ5;ZgEZPAjiYwR+&o!jF^qEVbihg@ zF$>sD&CTs8IQ)V%4lDUnV#E%%2I8KH`KZBkQLFov>un*B{@$H9DkZZQyf<z7<7aPD z#G?0|@^mae?)RLXE|^)LD!uLQHLxV&zKj}6I0e@A;Hky;1-(+0LHyq3T&a3UNq#Cv zOGQ2*#&DFTfiVOc2hqSb20CbnG5P5j=jiN=ciyIE&ER^5SpgY0f!cVpeS$uMYx%~r zu=}}b!y;8#ijz8kQ2S4mspALeQyj|$8ROtiT5>H0dT#)EuduK~H_cg8*$g-;v)z|X zM?+pYMoaUok_N1j?<mC?LouI~zTCpS9YK`O&lz%09NEE~p5}8c$J?cEC>%CqB7Cix z-0FOE$W}(3d?fbDd{$5{N}yfR#nJ0oSYcNNni<+u=o4sh#$dAmXN;lx^T-De+T*y~ z%+$Qh#CbeMRS>N#iYOnQu4SgP3N1%01&Y4CM~X5CG3m*<vOQ?QED<yAnhJc_&&#}$ z=ffXN2qhLAUL5puCa=hJWRbmAdQD5Crwo#uao@$+Y_6gWh1uJ6UQydCWGxJO_vqas zu~&#PF2WZVp!*UA?%5T4lvflyTcrY)G0yC$;6DCIntT)?N>WKhrB<FIx1<6-D%Ua3 ziNG(Ekg*CqlopMhzz;kK13p|%!+U-}=T#=E54yN-Pn?Di<bwPvG4cWfE-IISz>nuG zRCk4YH8nplYU_Slhtt0=7U%l*avO)spPLmfN*L%|Y0F{<5O0=b&6$ETV#|=?>#q}` zfZs91t1BW}px8b|<k+a9EiKpnkg1fo80f6hGR?bJsH8(7MR31%!JIThVQ|X6__G8Y zE=r<;!$o#acuv&4LW5BmtfBYg8;h5Xxw)C}3^Z#I@vr7_MrAY;F)dD}hECw|>*0wK z6UMAmlM?>6iJQZ-!IzJg#yQr8ra%e_Fq1vgkJ=xa#St}FW2*!*FL1=Fg<)Qx5aDAJ z<D;Xi(gB+^uNCx{N#f!}Xqq5#SA6_E@xpfq;DZM(&4UVPhxVGBTXUp441AYp@onoT zp5!$tf!gvjp&y4yY7k!4W03xC1x``|*fV?L|M84723#I)jZ0n~UNXw44AHm+N0W1( zCnYV79UYBO<(oT#)?LT2Ht~3cOvJE24RXIRYnf72lO^$_c79jnbKwthFI|>1UU9rz z^tR7_tL+|4wAUq*ujvgq?J_@7x?G5B5@FqPqW6%XR%!O%D4|pjno_<6-GGNkTC-Zz zs2JSdbG^Tb`QfMg2&ZC{ml=dSCr4L~=bwzj49#?EqRxk=`qxWOQbe8$?Rf|@J~6II zWsWse@;vq_E}g2_tueoDH!)ucYuIXAG<Hr9=PLvs%&*rcGpt=TD88tj!0b8$2tzws z?%25{gaFnppF;ByD?PqwM!uqwK?rA^7PQ@lm_~GAkc1?=Hg0~H@FXqfh}AZ}^X?V3 z3*j)`aA~}9tSa&Dd`!OMKxf{PFsMLV0C^b_c=^i!kFEOML~;ayzYU<?gXP1<A93Q$ z@`995l&35XvKWq=%a|1-Eha6Nl@tNF1%ChRs1h#|(wL_u0+KS)jTiU5m7a*7Yg232 zc9xB^z};<dBpbFOxy<&@3~$9ZdCO`NO?f$H@yn>POH|)X|4e#(7`{&;pe!}Z-3=vM z(&BB#aO0G}9I}l3Y)=vx;g>@`BtwDiky*?L<ai?xl{UR3?yLj8PJEp*3_xc@=Q2~U zm!vL6STAH{vue4q2#2jMr03%OUB{)YeKQ~5wR={O6@|#nx0WC=r+3H3M$}QKe_flT zK_)+Se&|RUpP2!0B<H=nCt)E5_qDd+@}cvLDgtRa-}NkYX7#^j(?_anD`wI92>0M1 zk8#;JDjtE_`0)#$cr6el-m9#OE3ZP8PK!?#f{uz3WwLq*k#PNau7h}FX{Kwk_+S%m z1&l3bVBd1KU{;#NJpWQyfCJ^eHmPfNg7jB~q@HEDhm1=Q10tegtSiwh4r@83ha8L{ zaUCQfYtdGdB_Ua0h!NRJLc};53u;J~0KHm#yBwL3R#`s9SQiDxA@6tR&aUl#VK#wH zs8u*5MQ#}9W))w@7*DQ%p=G))S~J{LBYl*J)l@$`9kb^9V9apb+e?lDUVU79cPWRT zS<%2xqG?vMO?y`WxgY;Yx7JSrTn2%}(N3A79V4f^!1t2?-^~#xC=rPh#6~Y8GN%p# z;x)UqQGqyC(lk0_<Wk@=c)cnzYeM~XnncqK7|9e3u0MXtLd#_k7N_()4qnStZzqp2 zqEK1@8b`YYjj@h}QH6gb;FOX5tiOgbgXlj76~4@LSG{hh7}o{7Gmo>oO1l0*F11Z- zoZ@cyU+nPxpV^6_{maf48O)9szSH;N3-*7&Om}~}#JLv#1!Rwd4|lm7snGh5QS6Kt z7d{DD_fLS|CtMW>Ng8xR0RRUG008=bCR~}hx?0&={F-OgsIl(6!i&|lS!~#{ZI007 zB>$Q2h_Yrwf!Y?l_yC~p9l-92CmuOZag$;-d?eXC_4EY$;&!sT%VprBB1<O9n_QG6 zV?jg~{~3Td9Rof0<;-HS?Y$8vYjS8fE1jVrzc2o~w#U=Yf?S47)@cx)Xw-Xq-Ytz9 zMG0Ya%{^hQv53(AHl=*EH>Et7pevWkp`zVzEgdDvQzHx*5%!a{;oF)8@>AVNKBh)a zXS*yJ`b8TZ(I20guK1i@CloQ-0Q<PnX$bK4NrRSE%sO};4LZqMzJuO;!>;{2etg=$ z73us#MY9^`pzr`WF_{W40CylwF*aP#@jjqnaS0c%ouSq#Xy0f$^XE9qOPV+w&)gvD zpN^R@EOYHqvFTXI`(oqf?op9Hsi{a#=7>cOVDK`OUuN<GBI1tMFp%I0VIuiFfpW0W zS*LSR)I@1}RN#dQY*aYz#KYHQ_tq!#bA!g1W(pf}T_j|$nqvE`M>_PWo|51AF@9Pa zSm=u-7O-AYUJ{y}T_DhrLP=UL<&J$g7Y5cfNl9-+Bk<z{n$$X!bjM@2Kw2wIb;sl> zVys?+NW)YstzZ<ZMp|F74%ZB2z7HT&USAwixmrT*VKF_^ha}bAosy0y4)8|~Jx;sL z@#n%#W-HTBvnl;19+Yrf-b?*)F@m9v{qamf_E<e&i#vPb192_U3Pmp`sBf1t9(u&p z2@!!MK-=33F{~>7=+gxjdsO_%7gJasp6!c%nT#BZ&K8t-O2Hu0h+XS+x6UK}KGo*C z7mY%E=_u|twWJ->iO3rEFF~1-c{vWHXYLR!EL(UZ$9(~xosjTAcw>&buh1GU!U-A! z-CWEW@_9NpL1H<HJJu~e-_^+#g`Y~zDs^0|!>o@!tM~DKWh|{hP4sFljaYk)u;wVG z#8fu*a{a2*VM?%3y4xI?M7n=n5_jOn=4&Y2z!IU@i1!UD{-)3yR}22P6+zg_WoF*B z2nS-4Jg*7)IU)?OyUHh|YQy*$t!qjK(@|C0jFsD8UeyhcMPGf;=RP&7O&RPwnH^*H z(%vx(OR#gg=c+of$Q1K&y=XL*|C;x+{G$(=+t`}I4+Xm-gOPPu?o;y(44APun-nBw z42LfO97{!K#<dgJf;3PCmV+Zm`qpR@nU|mpF0Z?&s{YJ6&*MDu<eP`Hp_a5TpS_(8 zaGk>E5{l*L!CpR7gmOTH{I;BF(KR@<#2M0Z^#~~lDVBUOD1reabA#9{@|AbFM`cFe zi2!=t_h+}|;wHrJt^H7EPZa>(YN`qRkM&P$e`{u}`d<w`YP#3DpA{J}?bX${8?XA= zG?Gm;o*j?4JyE0y)scvbtr0vKYU1Y)4sC9{_A6?#BeK#BAA$+yEtx#KI(8OzSdE?B zlX+8;Zz1a@L$vTO;iGWUPY@GNR}1yaV15EcL!$=BjZ4|<WDd}D2l+<uBXfy}m$Zmx z2+@td(AWnGWvzqqoybe{t+1=^t7u=$Mx8g)@<z+{ag5eO5H5Ip6_G+`ni!plX<Ex? zI)pn=hE(qOc<3+%Z|0MB@knBtcqPpnEbyY<u0h4AgK=T_FcDw76NJ-w$i-<DILmFs z_+H5}YHam9$l=yk3OTE#2h+_71qH5%49Dj2VAbvd=68?X@#VUhKmq`Fh~Q15{_&W( zgT1Sok+H4W^PW-b$-VX~tVBcK-i6bqWXjqmG-9FC4+S@j0SUbTm_#)=t8u5ot+RKN zFKuI3Pith_uk2zyPZ`E3A^hR-UB#rK$oQV7ZjGO=9*A|C-!j%wi__D$eCr`_WbA`t zEA-xdN%FIEEZBrD>K0APA9FJeeQ&LaMQ6^OwnZbkHLIK&$=j>KZOv5=I})M4jM}&_ z3J-ClhC9W1mz`0F`rM3EE0T+NI`m6g1fZqPh0r530$z+DR+C&BZ=1h!Gl`Nt+P|DF z>eHy5XXvWhuo0t#a3cF_bLIVyta~<uho^XK1k!opB{t?9x8W)bE-#^vLM568{r5A_ zaWPQIcUFgn?G3avCMVOm`OH5&z8``mmn~9@WWx;?E>LvbUCrG!X+K`>#gmh`^5mws zBs1qsucXYhR7Nu?3pkg}FqBCtXFLp#j!Z9sSgPoqq@QlyegktlUh1IfKVMv{=NH6x z6!6-9=mWB94tst@v#E6_Ci3M&N9?V>AHO?<z@?b^uKF2d@z(d16L>ezPL%4ZQe7T_ zmj2@p;hLTZ962T)Zfu095*kiA7bCn(9(k9JkR`(-uQfW{_Ih;9GVQYmkB+ui=lW5P z&o7HVvKTxm<8oim#ap@a(BrnTV9w?Ynxgi{Zp0b6ne?FuPXiXsq7a^-(OxyPwe#jv zTd9j27e$564rkF=)-YOT*~?4=Er+`iSBxKB*q$0&x(&v=a|G?>k;Cq%E~qhN!YW1y zSv_9Hu3KFs3pMF)sTn%%_9kBnCaZp|sM&FVfn=Ge{kHcmY|`yMrHlHw*i)fOvh3YC z;qSL(q6m4*5G(-DP7eTJ|8q;aTAJCJvHUu+{o2V)N5Ns08?Wn!CR*F>{LfXl@HUB) zrPGBEC>YIRF%9CcZE2NBk0`rPPMvyujn-@W5l@=>Ybu42PG%p{?TMR!hX#0b%i*-j z&ijK3D^zeF$!`U`yA4#%J{^a=fVc~Fl7G0?*;DaAA>I4{y#9FF=oRodxInN4`4dPv zFLl~Tw_~$dt=EEB4Ug*qFWoX#-P5#(h^|YQ)WQ@()-4XTJJyPB_5$l7E=mp<02jYN zQDoh^#^`}|hjW$}va+x~1HTH7j8)`z3JGv|d=W>|wPnpn)QGU!*q@ak&5(|&$?KJL zt9mnD+#!dz6jEq6@<8m#Djd=W3GGZqv!cOwUoXohB|{O1CC->HKmEO5D2rJHO;+pE z(ULpfrTWkB-^Vhntjf|kVb1ic7f;n^+iA9^n~EeDZn3~iV9(0P#6k{o!o<6dMLDla zCnSAHR;=GRawRyXPS*+kkgYgJ&vQ(uj;ZiUY>u`@!J=T*F5Qv<KiynV;pxJ&-oDU^ z+EUm5q0<`!yYWbPjueA6f7hH%POh#&Wv04dUOMxQ6M1wA3yx_%9Er+8%5lRw0@f0o zF+nbR1636g!V7k9N|6@C-hue}%!$i`L@nP{WVn30cSOS4#?cF1?`}V@rcCvgOR?y` z`nmV%^T+BJ54zCZ2oFvekNbBlkMDod-5lS(b;$N^1PmwiZ2J5>X*THA^+l5zIIhll zINw=5b`X5J455)|L4ggCSp#^52E053oG2;M8#GW6)KTR@;qFkn_LI0Aior1Ze-AA0 z{VGr9C*cd6C3egiF@7D|!sbyz*LYkPCjBuvxMk0iz&Ju>Hm_m{(42@Tv#M}^4%xay ziLm77*sWAyPao)+AiPh&yjFNju8?F$UVs=K7Xz*2;)<F_eh(GN^<-<5p$w9O6zIu? za;hbRbEh3Y%vef@&c+#K7L%<%TzEH~SLoQU&`>RA+9s;ff<T^*Yu^**t6!@zO(8RE zN4q8}@uqluf#_op@&`6@tSh1c53(EP3~T!J_(mrYQ@gm*VzJa7ub|k~*jY-`gs*OT zhcJVlMw<%$f;(R9s-#OF^0frZ4r)A;qk|rILfc~EwCY6H`%M;!BCXB)5M8j9JA>s% z;PVIGl#!WxW_)?OB&5-OY1%HgYMb-`YKmLj6nyC6Uk`3DGO8=STNM><D|e%FISZMY z=iVOJlw|jt@>=mweEH4FqNJFqLbm*yspn$&SD(xF^%=LR*RHFCMxt*96LVK}9ej1u zhmOXU3uY_@b|YBw{TRBfR}v$&!iYHVfRK`dG+Hg~%*ciE*bI&RsWx>31*EyU8#zqx z=nYD)+72JEl9PeBTvtD&_(`x{&xb918r=`d%fEc5x|5vP(Mmr*zd%zYd3s@0svL%= zC><mFIn{Ib=G5Q_J*u!zKI`XhnqsmvokBf<6-_+`#%>9#0?e&I=9F`!rgP>C^tNe{ zkJ>3U?~C3v6_l`+rcD26$gk|6UFq|8C|M6*;K6a-2?%Iv+$iB@H0;l{i`pfYdjB1= zl|-`=J-&9c$+mZP#`*;2<BvG|J2#j7VyH@6on)Kq%qq-T*vJTl;q_2n-J~YwbCeHd z++Rki${W?KKIzQsB>hm2I2=LSw&WCTVwLnTrf+oPG@);IB}ZFz%YtI1#B72pa{Hpp z&!0L5rS10EW9nBr#jehBwhx*7{+mOI%eJUAB<>_4h@pE!+;urJbWhQ%H}_)3aa6Lr zPVba>g}JycbM@<$9Zxy&uoM@K3H)n)5FT_HZv)e4pqyDdlCJHKw2rw{wL}d-T`C<E zK=k2ENbIh3h%bd^XUbM?S6%*Oi_K7r+i06TuxW67<H>$e_LF-qJkpYFf_%{Uil{a% zYpO-)H9CHY@rIgxo$pHr&~jV(whJUTSH<GOcFR+DC&t3&Ua695-uw8FEzcQsdMC8k zLQHmwJL5S@g`V=w>Vz;7U%OxIL(%Hy3hsI!a%7M)6m;=dqaH?&zeF=o_pPADeO*8M z0u@*krK4RrzhNEm;~TlP($Hak{cSQix((ltP1}mBKyrrA#$d8}<lA>0(yAsO-lOY- zItRw2xngh4Qu!fJNMsRsX5c~6I|s%Bv0`D_0|9m#vI3~39G|4%V?4E;BQ1bd1Dhfg zzFA^o<c0Z0*K?&Od!`4{bxYH1Hg<dq_#xZE_#B!kCehliX?vyg=D<x#7DF1?G>T|b zT1w;>SrW!9!A%COiYW~<kU7bk`*Tz#XMx`Iz?3tqlfkq5oDuz^&v}M5FQ2y47!jOf zcps|uZs2}@@MZh*FBXFzc(CB;6%+h!;^yM&V5eng%VOnfX7}r{R~cE0*vE<m`0-w1 zB!1)re<;?R3K|W19%7Zu&lTACj`%|3r;X6=E%&#MANTDP9wA9M`9aMRGA@pYUZ;{E zk{_4*!k1`4$3Y@+nN?_(suoh(ZY}OGWQtI@^dlo!pLN-A=AktLAuSwBQ4T9Z5gvve z@f!Rw%Le@;Y&Da;s2e#6q*(T?sJfSZG>Gw((0?*l!SWiNRx)L;Qw3GGAZD042Yy=q zgO}jw-ltA59~v;P7r*iPYZuu6u=}+IY}8aOI6g;%-Re{%Yp^Ucv!Kn>y2gN97Ih>m zkDY1?Ey8S*liujcSB1i7-ynYa;bP~5THYc;9Wzt%p)ASw(DBNN`-{brk9`tplT!nO zB<mN5!k=FYUiDhHnp&QRiHnfqx3U&HRE{vAF{-DMCXWU3X)&8*yVZR)wwTUndz`)P zCTpirC6c7LG_ozkdx5Y~>y+LRK-^~IJIq4g(2ilk8}dE1mNUF}AAd=D3hhxtL0!JI zT|?+$@d5Jp3)<K&udod!Yz!uj_Z#tN%1X`-jxN8hYF2EmeE=(##M`dEi=7d+Txd(I zP-y_2%_mtsVs~FETZ&cjsIo^d1%57*k8i77zkYg2B(xW3Z&)!+PcuVJ39nZEGA}r@ zqnnG9Vwk^*U(gDMuQh<`BRQ|@n@y*Wm2<GnK+203dubErhie@c<4qLO_B@`jd;R)^ zBUwTdJ+7M(IP~kx7>OoMc+?eo9dhr75h%XlfM=CD->dUDrJE^>9=ps=voC)=luOm% zwA<0s&QVluWa@T8Cv5=zJoOIRsM-yy+!zdf0(OKSQ(hU~2yC!<an==&!rfdZli&Q2 zrNvH%gxP>=GGXB8?msz)y_=n}nKQU5^BmnxXsz4t@ZxoD77NEo>NbWYa6)r7<O^%) zMM+XLUIM7`YQE$Qv$3vyl{+<=V5rCvDhcP1$9OXqh3T`&_vj6}hq<{w3y9wUhGDsI ze+_tDyeY=GFE_sM`SC}}A@#`*6@+CBjLGR1A7QJf?Z=p@JM`6A7!3OAVp=W!6`J#< z73s7{+m}&1p?z1$J6dOn)w_1%r4yPAL=;|AgR^glw@fv|w%BA<2RVXl;Z)0W3+~yu z5kFtdx@9V@n`k(7REN@pmTrY3Ar`e^+XSIiukK1ZQ7i4hP;0&5)Kr=|8CuY&QSNn8 z{glT>q26ODC6hLO?0hL2ktZH17d}qL@xp=5n4Mt?C$z+$PmYI|@5@4>0t^q8%xSP9 zce|qR=HykHuNM~)0t=l_S^(_`L$w9;BuunrIHHpbg2N&s1K+!w>xn`>?6>QnRUw#7 zYANou=+m%H4c#)#d!Xj_%+bQ^n?V5w!+aoGl$4^)5R5LGdYno}agOw{ej`nbbdhiE zeOpl{&s)W@+C5V`#z8skRT1{4{1c8;ou=q%rnnoV6|xIwX{EFRTx!~k$vH{|Y5Yvj zo;1aGJTb9ZKtry?h*D~L(=_A@#!sRYItt9nk#v;Uy+}Wm2L=6BtskqwGjJ`bE{}FI zh7ncn7>C%UDve5y0~rQI4te=E1<j2=P)&<VXsz3kzCM0*@DNGMK3`qFSa#1pU>Ss^ zuC<67%)SHhy6QfOMV#I>8Xr6UFm9)=J%}2Ne&I)m+$kevOfz*MT$a?mrIu3FCF8z$ z)rVlyIlcY0^Gse}JgW0dbQ7*uPg8GbH^@tZ1=c<bn}4Mfv#U1@jq?p8a*yd31)YZ6 z!DKYbDf)-YZ$6Jtzy^{q<51p&QVp)C+MhV*-nmF1_Cs;2pXQJpr8%|OYT@8$H^}KN z_2;$y)A0<&{AKTzD5r0V(<V5tHYfNm8ufVY;JaX{4LbKLonZHsIgby8)4gA9;<ukS zs$1N{R{`a#f}`}Z_!4^Fjo&R{8@OwFZxX-bKPZcS^VEf;Ac;&!xDAAZP}0}^;jVY? z+IX=o#IMK5k&yLh@A=ubwYnzP#KN!&A7{Qtfa9bJ_WJ9`YxMoap0DFIA}EZk{T&2L zJ;*LLhI#m~)`nsn<9tEOG*5XOPe{M_Soy;X7M(BvKp-gqfD7)iz#DY`>ZZV%7r1@e z8(G;hTRWOroNCxOVya^awtMl=*-fC)GGW2>wX^pWGKn%ka$Bu@NaQYLs&)#NNl?H{ zr#5;!mki2^`w93SBH;a0i>2n8>7+E{&#xjOECq*#goQUmZhg)rfMo!6zP4V7pcEKq zFnO}R{Lqzox$ZK>=XGG8|1sa7fBFK$3xO3G%FJEls-bJlzOl-Yw_NrFo7)v|*xcH; z4X}Z<L>|qrpEuTNvu%~xL}HBe=Nt~^OnHrn%SVLm8(_WN=5z$liiN63vi4L>F#+xc z0n{)a!xXKAam|1;h__ma5Uv)}`qPXdamU}pv&oG8hbIyj%R5pp_>)-FIk`X#&gl`o zr!F3`&15+mDW=tmJJ!I_VOnf||HHQ#mMlcZ!^f!bGDjN&?;;A=(88*DV86MR=6OOG z11r~QlKD(J`cLcBDRM%+eYBdC?o4-uBnYe{VijQwLp%oKb=0ReR=<>M`wL*Ueq>p- zF}_|f)H6%sPdT-PXL20kyjzdQ_JaK!mvez|jmSj;6cDYAx<<OiGbm)l$ze!IFS9%6 ztrWB=A_&3MY#SvWu8*t66q}^yvZzvpPvrk}v$j!l61vy(9S$l9+gjcjjj~8`konGf zbiUOzIg&*=OD@^#f$BQ7i-}0-ntWffDIDFxPTpmjYDM-F@2)>uMbzM~l&9`sLo-M0 zRKQ>)5;VLBvEVvd;FuvKO`<;x8#1|P$5seH2l@)p@|@$XFw_d{A|F=NC}y~)*&Cs( zsB+{gWldhfxj<<gwgm5vgIGlN6s%ZL*0|eULG~D|g2vc!{M2J<ot!GSH>8vFp@wQ@ zuaT3#FUMsLc%JSccHi|R98{$fw=r9gWh*+v3HJd9lI9YQHy5uThs7gb%(^{oU%j(a za;ty5u()~O1((x#KN9FK)SEL<jvLO}>>IC4vGJY?m5wf8m!R!Dxi@G^a6TyBRkQLC zrD_V~Zb=i^n3O`F_fDtJTG0A&dCg?QcEkGdJe?VJg~}eYIpEHBb>wtM5Dv3B<9J_i z_Zs~4{zlbVPWUax?``!wVRf=6>~SLiB4Chpg|%177SATtv_yJ<!xY~c#VnZa5dYQL zM*N~&)gbEF8UD8~TQ;R_I#)mQv`d9m_=y5PEuK6r*|G=kV%%}HH~D_uYZ0}W!#)o0 zDa<HinBV&{N*8lboZ;Uk>b+96;`WtLjg+!9{y4MhguhMm=!ANZ#t*z7G5aga%Dgk{ z8G~;L3LfT{wyNmw%b*ip9cHsl>>@(Ths&{t`BR1}_*=kEf6Mh0H*lrj<e|P4?xSXb zPQ63Q)`r7u+C%qUr1GsuLwF6$4x`YAK1|<>4ppe-$T8{y#CF_ze5rOt%2p`OpNVkS zKjMMZ-9U?aT*r^n-|#<IgggO?-w8K0-gE+xaA#FXRD4;Nd*+tir%WVK&oHRrP#s6L zeH-7CvU!==p4KfnU@rr?s|>Rbw{|ks3uiD5i=JNC+f0)-69$BRg*8EppmUBy*P`gh zPdj{p7twlAM*3)ErWJk0|86vPo)hZ?50SCoiE-OK+t6wp>JZL&HzaC|X%@v`cQ(c# zDX!VPe?Ji+`E;EkGh`CHzBlG-NTIoT6j9PEtBb_U_79UCUJEDweO?uVP4TnrULI}= zr#n<Pp6f4qUw^x-PTZANbs77z5c1;v?o8wH3oq}_ABwZz?=K%@`{cQdEu`qbH(M>Q z?z~-CeYxS$Y;#Z*Be-!nvhnUt=;36n-{3nDpJ&An_st`RN$<P$9}gBjygu7I)!lkO zA7X@_^4-{aP!a_#^_HN6sRAiHb#VK+RsG`5K0CYhhvOXSl?TU6y-tWXTCVZ%`Vot8 zJLy94Wk9Sn4b$RUfnQNhmA<{PZ{<nV3@V7~3qSCP$ex$NBJkD`yx_8H4eAGfSOUvj zzyEo7;t0L&6H=0cfW-P$9)|}UkZ<T#suaZ~fg{vWaLw&G?fB=<7yKOn@LCL93^ucO zHBj?#G;`5=j?VrB@Sv6xz5$=Q0j?t>{UZ&2_k|b&L;Mxv{|fE@6bubUQndn~G6NQT z9-RB<&llnV{LO!!Jthv$zbesxieIg+;vR#=ZJ_}Gw0~s5@4gT|;BWpTZsh3rzpU2Q zGCO!;0RV~uApTIE3U<U>2WL|jFoc^OnEU?{xA!H)U<WH71FItXP5BV_-{OBU|8J20 z=>@p?>NFRyAsFf~0My@**m?g(a&h&rHTyMO`KJ&5Tu|~Kd0&WGi{FI*SYGl^oIl4& z|HTo@`UmH~22uVL|8wBwUvaj)zs3I;hWQiW&w+@45lr&`M)+-5;!pWMtH%GzUl;x@ z|7Q*PPl!MB?|&h_mH*Fd{7-;C6WxCSK34oU!2e2n|HS$;cl;Ntr}DqCe$OQTMENt- w`WI!f>Tj?7mU#Us|7Se-ue@{Je>Jh+qeCS*7;p=EZqn!gqvw{`^8D%l0O5l><NyEw literal 0 HcmV?d00001 diff --git "a/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" "b/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" new file mode 100644 index 0000000..8fb09dc --- /dev/null +++ "b/Experiment5/\350\277\255\344\273\243\346\200\273\347\273\223.md" @@ -0,0 +1,89 @@ +# 实验5迭代总结(CampusTask v1.1.0 · 成员3) + +> 对应实验5:"迭代开发与变更管理——处理用户的新需求" +> 迭代周期:2026-06-30(约 4 小时编码 + 2 小时文档) + +--- + +## 一、迭代目标 vs 完成情况 + +| 目标项 | 计划 | 实际完成 | 偏差说明 | +|--------|------|---------|---------| +| 采集用户变更请求 | ≥3 条 | ✅ 3 条(CR-001 逾期 / CR-002 搜索 / CR-003 CSV导出) | 无偏差 | +| 选择并实现功能 | 2 条 | ✅ CR-001 + CR-002(暂缓 CR-003) | CR-003 因跨成员 schema 风险推迟到 v1.2.0 | +| 核心 API 代码 | 2 个函数 | ✅ `get_overdue_tasks()` + `search_tasks()` | 分别约 25 / 22 行 | +| CLI + 交互模式接入 | 2 条命令 + 2 个菜单 | ✅ `overdue`、`search`;菜单 5、6 | 菜单编号从 1..5 → 1..7 | +| 单元测试覆盖率 | ≥5 条 / 功能 | ✅ 6 + 6 = 12 条用例,100% 通过 | 每条用例独立临时 cwd | +| CHANGELOG + 文档 | 同步更新 | ✅ CHANGELOG v1.1.0 + README 实验5章节 | Keep a Changelog 格式 | +| 交付 Word 报告 | 1 份 | ✅ 实验五实验报告.docx(见 Experiment5/) | 约 20 页 | + +**整体完成度**:95%(CR-003 暂缓入 Backlog,其余 100%) + +--- + +## 二、用户反馈闭环(验证交付) + +迭代后期把实现交给了 3 位原始受访者,获得反馈: + +| 用户 | 反馈要点 | 我们的回应 | +|------|---------|-----------| +| 小李(CR-001) | "过了零点昨天没做的任务一下就出现在 overdue 里,真准!要是能把明天要交的也列出来就更好。" | ✅ 日期判定正确;💡 入 Backlog:`due_soon(days=N)` 函数 | +| 小王(CR-002) | "用 `search 实验` 能同时看到还没做和已做完的,方便。大小写不敏感也试了 OK。" | ✅ 子串匹配 + 大小写不敏感均有效,无修改需求 | +| 小张(CR-003) | "CSV这次没上?好吧,目前任务还不多,等下次也行。" | ✅ 已放入 Backlog,解释:需和成员1对齐 priority 值域;小张理解 | + +**满意度自评**: +- CR-001 ⭐⭐⭐⭐⭐(5/5,精准命中) +- CR-002 ⭐⭐⭐⭐½(4.5/5,建议后续支持"日期区间 + 关键字"组合) +- CR-003 ⭐⭐⭐(3/5,功能延期) + +--- + +## 三、变更管理的关键经验 + +### 经验 1:用户原话 ≠ 系统需求,需要"翻译"为验收标准 +小李的原话是"只想看今天已经过期但还没做的作业",如果直译可能做成 `deadline <= today`,但拆分后正确理解为: +1. **"还没做的"** → `status == "待完成"`(已完成任务即使过期也不提醒,避免噪音) +2. **"已经过期"** → `deadline.date() < date.today()`(**严格小于**,"今天到期"的不算逾期,给今天留缓冲) +3. **"没数据别吓我"** → 空列表显示 🎉 emoji,而不是一张空表格 + +没有"翻译"这一步,用户会看到"今天到期 + 半年前就过期"的混合列表,体验反而更差。→ 这就是课后反思题"为什么用户说什么 ≠ 系统该做什么"的真实场景。 + +### 经验 2:迭代选择必须"避免跨成员责任边界" +CR-003 CSV 导出代码 ≤40 行就能写完,但我们主动放弃本轮做。原因是 CSV 要输出"优先级"列——这个列的值域(普通/低/中/高?有没有紧急?)是成员1在实验4中定义的。**成员3擅自输出 schema = 越界**,一旦成员1后续加了"紧急"而我们没同步,导出结果就会变成"看起来正确但和 UI 不一致"的隐形 Bug。 +→ 多人协作黄金法则:**"读别人的字段但不写"是安全;对外输出别人定义的语义 schema 必须先对齐。** + +### 经验 3:TDD 给"回归"买了保险 +改 `main.py` 时为了显示优先级列,我不小心把 `task.deadline` 写成 `str(task.deadline)` 导致 None 显示成 "None"。幸好每次改完都跑 12 条测试 + 手动冒烟,这个 Bug 在 1 分钟内就被发现,没留到交付现场。→ 实际效果:**TDD 是"改完立刻放心跑",比"上线后再发现"省掉大量返工。** + +### 经验 4:语义化版本 = 让用户一眼知道"敢不敢升级" +CHANGELOG 标 v1.1.0 而非随便写日期: +- 首位 "1" → 主版本没变,现有脚本不会崩(Backward Compatible) +- 中位 "0→1" → 新增功能,升级是纯收益 +- 如果未来把 `search_tasks(kw)` 改成 `search_tasks(*, keyword, status=None)`,必须标 2.0.0,警告用户有 Breaking Change + +--- + +## 四、暴露的问题与下一轮改进方向 + +| # | 当前问题 | 改进方向 | 版本 | +|---|---------|---------|------| +| 1 | `main.py` 命令分派靠手写 if-else,新增命令改 2 处,易漏 | 引入 `argparse` 或命令→处理函数映射表 | v1.2.0 | +| 2 | 多处显示逻辑重复(priority 兜底、deadline 格式化) | 抽 `safe_priority()` / `format_task_row()` | v1.1.1 | +| 3 | 搜索只按 title,复合查询(status + deadline 区间)要手动拼接 | 引入 `filter_tasks(status?, priority?, dl_before?, keyword?)` | v1.2.0 | +| 4 | CR-003 CSV 导出未实现(小张需求 pending) | 先和成员1对齐 priority → UTF-8 BOM CSV | v1.2.0 | +| 5 | 单元测试只覆盖 service 层,没测 CLI(main.py) | 用 `subprocess.run(..., capture=True)` 写 CLI 冒烟 | v1.1.1 | +| 6 | 交互模式"回车空输入"提示不友好 | 每个输入循环给出明确示例提示 | v1.1.1 | + +--- + +## 五、结论 + +本次 v1.1.0 是一次"教科书式的小版本迭代": + +1. ✅ **从真实用户来的需求**:不是拍脑袋加功能,而是访谈了 3 位实际试用者。 +2. ✅ **基于优先级与边界做克制取舍**:虽然 CR-003 简单但仍推迟,守住责任边界。 +3. ✅ **TDD 驱动实现**:12 条测试先红后绿,实现没做多余功能。 +4. ✅ **测试-文档-代码同步交付**:CHANGELOG / README / 测试文件一起提交,不留后手。 +5. ✅ **交付后回访用户**,反向沉淀 Backlog。 + +最大的体会是:**小版本迭代的价值不在于"一次做完所有功能",而在于把"用户 → 需求 → 实现 → 测试 → 反馈 → 下一轮"这个闭环跑稳**。只要每一轮闭环都扎实,一个学期下来 CampusTask 会真正变成好用的工具。 diff --git "a/Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..09964028c03a39496330c33886184f4b0f106c0b GIT binary patch literal 43722 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 O08GKnL!<ELum1sI6AKjp literal 0 HcmV?d00001 diff --git a/Experiment8/blackbox_results.json b/Experiment8/blackbox_results.json new file mode 100644 index 0000000..8a3b578 --- /dev/null +++ b/Experiment8/blackbox_results.json @@ -0,0 +1,213 @@ +{ + "total": 23, + "passed": 23, + "rows": [ + { + "category": "等价类划分", + "case_id": "EQ1", + "title": "标题有效等价类", + "input_desc": "add ' 提交 实验8 报告 ' 2026-07-20", + "expected": "exit=0,写入 tasks.json 标题被 strip,priority=普通", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ2", + "title": "标题无效等价类(全空白)", + "input_desc": "add ''", + "expected": "exit=2,打印「标题不能为空」类错误", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ3", + "title": "priority 有效等价类=紧急", + "input_desc": "edit 1 priority 紧急", + "expected": "exit=0,list 输出含「紧急」", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ4", + "title": "priority 无效等价类=P0", + "input_desc": "edit 1 priority P0", + "expected": "exit=2,stdout 含 priority 非法", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ5", + "title": "status 有效等价类=已完成", + "input_desc": "filter 已完成", + "expected": "exit=0,不报错", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ6", + "title": "status 无效等价类=进行中", + "input_desc": "filter 进行中", + "expected": "exit=2,stdout 含 status 非法", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ7", + "title": "deadline 有效等价类", + "input_desc": "add 学习软件工程 2026-08-01", + "expected": "exit=0,tasks.json deadline=2026-08-01", + "actual": "通过断言", + "passed": true + }, + { + "category": "等价类划分", + "case_id": "EQ8", + "title": "deadline 无效等价类(斜杠)", + "input_desc": "add 标题 2026/08/01", + "expected": "exit=2,报截止日期格式错误或 ValueError 信息", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV1", + "title": "标题边界:单字符标题", + "input_desc": "add '交'", + "expected": "exit=0,成功添加", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV2", + "title": "标题边界:空字符串", + "input_desc": "add ''", + "expected": "exit=2", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV3", + "title": "priority 边界:合法值『低』", + "input_desc": "edit 1 priority 低", + "expected": "exit=0,生效", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV4", + "title": "priority 边界:合法值『紧急』", + "input_desc": "edit 1 priority 紧急", + "expected": "exit=0,生效", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV5", + "title": "deadline 边界:今天 (2026-06-30)", + "input_desc": "add T 2026-06-30", + "expected": "exit=0,成功", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV6", + "title": "deadline 边界:昨天 (2026-06-29) → 应出现在逾期列表", + "input_desc": "add T 2026-06-29", + "expected": "overdue 命令能返回该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV7", + "title": "deadline 边界:明天 (2026-07-01) → 不出现在逾期列表", + "input_desc": "add T 2026-07-01", + "expected": "overdue 命令结果不包含该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV8", + "title": "search 边界:单字符关键字", + "input_desc": "search '实'", + "expected": "子串命中即可", + "actual": "通过断言", + "passed": true + }, + { + "category": "边界值分析", + "case_id": "BV9", + "title": "search 边界:空白关键字(无效)", + "input_desc": "search ' '", + "expected": "exit=2,抛关键字不能为空", + "actual": "通过断言", + "passed": true + }, + { + "category": "场景法", + "case_id": "SC1", + "title": "Happy Path 全流程主线(add→list→edit→done→filter→search→export_csv→overdue)", + "input_desc": "连续 8 条命令,每一步 exit=0 且状态正确传递", + "expected": "所有断言均通过;CSV 文件头=EF BB BF;逾期列表不包含未到截止日期任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R1", + "title": "CR001 空标题不应被接受", + "input_desc": "add ''", + "expected": "exit=2 且 tasks.json 不写入该任务", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R2", + "title": "CR002 CSV 必须带 UTF-8 BOM", + "input_desc": "export_csv a.csv", + "expected": "file bytes[:3] = EF BB BF", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R3", + "title": "CR003 priority=P99 应被拒绝", + "input_desc": "edit 1 priority P99", + "expected": "exit=2,status 未变仍=待完成", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R4", + "title": "CR004 overdue/search 输出不包含 emoji,GBK 不抛错", + "input_desc": "先建昨天截止+待完成任务,再 overdue → search", + "expected": "stdout 无 🎉/⚠️/🔍;子进程 exit=0(不因编码失败)", + "actual": "通过断言", + "passed": true + }, + { + "category": "历史缺陷回归", + "case_id": "R5", + "title": "CR005 未知命令 exit=2(CI 可判定失败)", + "input_desc": "python main.py xyz", + "expected": "exit=2 + 打印可用命令列表", + "actual": "通过断言", + "passed": true + } + ] +} \ No newline at end of file diff --git a/Experiment8/coverage.json b/Experiment8/coverage.json new file mode 100644 index 0000000..3a19c65 --- /dev/null +++ b/Experiment8/coverage.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.14.3", "timestamp": "2026-06-30T05:36:11.249218", "branch_coverage": true, "show_contexts": false}, "files": {"campus_task\\__init__.py": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 11, 12, 29], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "campus_task\\task_model.py": {"executed_lines": [1, 3, 4, 7, 8, 9, 10, 11, 12, 14, 15, 24, 25, 26], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"Task.__init__": {"executed_lines": [7, 8, 9, 10, 11, 12], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 4, "executed_branches": [], "missing_branches": []}, "Task.to_dict": {"executed_lines": [15], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 14, "executed_branches": [], "missing_branches": []}, "Task.from_dict": {"executed_lines": [26], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 14, 24, 25], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Task": {"executed_lines": [7, 8, 9, 10, 11, 12, 15, 26], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 3, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 14, 24, 25], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "campus_task\\task_printer.py": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 27, "percent_covered": 20.0, "percent_covered_display": "20", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 25.925925925925927, "percent_statements_covered_display": "26", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 8, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65, 73, 74], "excluded_lines": [], "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64], [73, -68], [73, 74]], "functions": {"format_task_lines": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65], "excluded_lines": [], "start_line": 29, "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64]]}, "print_tasks": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [73, 74], "excluded_lines": [], "start_line": 68, "executed_branches": [], "missing_branches": [[73, -68], [73, 74]]}, "": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [13, 14, 15, 18, 19, 29, 68], "summary": {"covered_lines": 7, "num_statements": 27, "percent_covered": 20.0, "percent_covered_display": "20", "missing_lines": 20, "excluded_lines": 0, "percent_statements_covered": 25.925925925925927, "percent_statements_covered_display": "26", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 8, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [35, 36, 37, 38, 40, 41, 42, 43, 47, 48, 49, 51, 52, 53, 55, 63, 64, 65, 73, 74], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": [[35, 36], [35, 37], [37, 38], [37, 40], [51, 52], [51, 64], [73, -68], [73, 74]]}}}, "campus_task\\task_service.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 25, 26, 27, 28, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 52, 53, 54, 55, 56, 59, 61, 62, 63, 67, 74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90, 94, 102, 103, 104, 105, 106, 109, 110, 111, 115, 120, 121, 122, 123, 124, 125, 126, 129, 134, 138, 139, 143, 149, 150, 152, 153, 154, 155, 156, 164, 168, 182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 95, "num_statements": 100, "percent_covered": 93.47826086956522, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 95.0, "percent_statements_covered_display": "95", "num_branches": 38, "num_partial_branches": 4, "covered_branches": 34, "missing_branches": 4, "percent_branches_covered": 89.47368421052632, "percent_branches_covered_display": "89"}, "missing_lines": [57, 86, 87, 107, 135], "excluded_lines": [], "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47], [54, 55], [55, 56], [76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89], [105, 106], [105, 111], [106, 109], [109, 105], [109, 110], [122, 123], [122, 124], [134, 138], [155, 156], [155, 164], [184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": [[54, 57], [55, 54], [106, 107], [134, 135]], "functions": {"add_task": {"executed_lines": [25, 26, 27, 28, 36, 37, 38], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24, "executed_branches": [], "missing_branches": []}, "get_all_tasks": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40, "executed_branches": [], "missing_branches": []}, "mark_task_done": {"executed_lines": [44, 45, 46, 47, 48, 49, 50], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43, "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47]], "missing_branches": []}, "get_task_by_id": {"executed_lines": [53, 54, 55, 56], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [57], "excluded_lines": [], "start_line": 52, "executed_branches": [[54, 55], [55, 56]], "missing_branches": [[54, 57], [55, 54]]}, "get_tasks_by_status": {"executed_lines": [61, 62, 63], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59, "executed_branches": [], "missing_branches": []}, "get_overdue_tasks": {"executed_lines": [74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [86, 87], "excluded_lines": [], "start_line": 67, "executed_branches": [[76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89]], "missing_branches": []}, "search_tasks": {"executed_lines": [102, 103, 104, 105, 106, 109, 110, 111], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [107], "excluded_lines": [], "start_line": 94, "executed_branches": [[105, 106], [105, 111], [106, 109], [109, 105], [109, 110]], "missing_branches": [[106, 107]]}, "delete_task": {"executed_lines": [120, 121, 122, 123, 124, 125, 126], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 115, "executed_branches": [[122, 123], [122, 124]], "missing_branches": []}, "get_tasks_by_priority": {"executed_lines": [134, 138, 139], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [135], "excluded_lines": [], "start_line": 129, "executed_branches": [[134, 138]], "missing_branches": [[134, 135]]}, "export_tasks_csv": {"executed_lines": [149, 150, 152, 153, 154, 155, 156, 164], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 143, "executed_branches": [[155, 156], [155, 164]], "missing_branches": []}, "edit_task": {"executed_lines": [182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 168, "executed_branches": [[184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 40, 43, 52, 59, 67, 94, 115, 129, 143, 168], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 18, 19, 21, 24, 25, 26, 27, 28, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 52, 53, 54, 55, 56, 59, 61, 62, 63, 67, 74, 75, 76, 78, 79, 81, 82, 84, 85, 88, 89, 90, 94, 102, 103, 104, 105, 106, 109, 110, 111, 115, 120, 121, 122, 123, 124, 125, 126, 129, 134, 138, 139, 143, 149, 150, 152, 153, 154, 155, 156, 164, 168, 182, 183, 184, 185, 188, 189, 192, 193, 196, 197, 200, 201, 203, 204], "summary": {"covered_lines": 95, "num_statements": 100, "percent_covered": 93.47826086956522, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 95.0, "percent_statements_covered_display": "95", "num_branches": 38, "num_partial_branches": 4, "covered_branches": 34, "missing_branches": 4, "percent_branches_covered": 89.47368421052632, "percent_branches_covered_display": "89"}, "missing_lines": [57, 86, 87, 107, 135], "excluded_lines": [], "start_line": 1, "executed_branches": [[45, 46], [45, 50], [46, 45], [46, 47], [54, 55], [55, 56], [76, 78], [76, 90], [78, 79], [78, 81], [81, 82], [81, 84], [88, 76], [88, 89], [105, 106], [105, 111], [106, 109], [109, 105], [109, 110], [122, 123], [122, 124], [134, 138], [155, 156], [155, 164], [184, 185], [184, 188], [188, 189], [188, 192], [192, 193], [192, 196], [196, 197], [196, 200], [200, 201], [200, 203]], "missing_branches": [[54, 57], [55, 54], [106, 107], [134, 135]]}}}, "campus_task\\task_storage.py": {"executed_lines": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "executed_branches": [[9, 10], [9, 16]], "missing_branches": [], "functions": {"load_tasks": {"executed_lines": [9, 10, 11, 12, 13, 14, 15, 16], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 8, "executed_branches": [[9, 10], [9, 16]], "missing_branches": []}, "save_tasks": {"executed_lines": [19, 20, 21, 22], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "start_line": 18, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 6, 8, 18], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [23, 24], "excluded_lines": [], "start_line": 1, "executed_branches": [[9, 10], [9, 16]], "missing_branches": []}}}, "campus_task\\task_validator.py": {"executed_lines": [10, 11, 17, 19, 21, 27, 32, 34, 35, 36, 37, 40, 45, 46, 47, 48, 51, 54, 59, 60, 63, 66, 73, 74, 75, 79, 82, 84, 86, 87, 88, 89], "summary": {"covered_lines": 32, "num_statements": 35, "percent_covered": 88.67924528301887, "percent_covered_display": "89", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 91.42857142857143, "percent_statements_covered_display": "91", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [33, 76, 85], "excluded_lines": [], "executed_branches": [[32, 34], [35, 36], [35, 37], [45, 46], [45, 47], [47, 48], [47, 51], [59, 60], [59, 63], [73, 74], [73, 75], [75, 79], [84, 86], [87, 88], [87, 89]], "missing_branches": [[32, 33], [75, 76], [84, 85]], "functions": {"validate_title": {"executed_lines": [32, 34, 35, 36, 37], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [33], "excluded_lines": [], "start_line": 27, "executed_branches": [[32, 34], [35, 36], [35, 37]], "missing_branches": [[32, 33]]}, "validate_priority": {"executed_lines": [45, 46, 47, 48, 51], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40, "executed_branches": [[45, 46], [45, 47], [47, 48], [47, 51]], "missing_branches": []}, "validate_status": {"executed_lines": [59, 60, 63], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 54, "executed_branches": [[59, 60], [59, 63]], "missing_branches": []}, "validate_deadline": {"executed_lines": [73, 74, 75, 79], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [76], "excluded_lines": [], "start_line": 66, "executed_branches": [[73, 74], [73, 75], [75, 79]], "missing_branches": [[75, 76]]}, "validate_keyword": {"executed_lines": [84, 86, 87, 88, 89], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [85], "excluded_lines": [], "start_line": 82, "executed_branches": [[84, 86], [87, 88], [87, 89]], "missing_branches": [[84, 85]]}, "": {"executed_lines": [10, 11, 17, 19, 21, 27, 40, 54, 66, 82], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [10, 11, 17, 19, 21, 27, 32, 34, 35, 36, 37, 40, 45, 46, 47, 48, 51, 54, 59, 60, 63, 66, 73, 74, 75, 79, 82, 84, 86, 87, 88, 89], "summary": {"covered_lines": 32, "num_statements": 35, "percent_covered": 88.67924528301887, "percent_covered_display": "89", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 91.42857142857143, "percent_statements_covered_display": "91", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [33, 76, 85], "excluded_lines": [], "start_line": 1, "executed_branches": [[32, 34], [35, 36], [35, 37], [45, 46], [45, 47], [47, 48], [47, 51], [59, 60], [59, 63], [73, 74], [73, 75], [75, 79], [84, 86], [87, 88], [87, 89]], "missing_branches": [[32, 33], [75, 76], [84, 85]]}}}}, "totals": {"covered_lines": 172, "num_statements": 202, "percent_covered": 83.2089552238806, "percent_covered_display": "83", "missing_lines": 30, "excluded_lines": 0, "percent_statements_covered": 85.14851485148515, "percent_statements_covered_display": "85", "num_branches": 66, "num_partial_branches": 7, "covered_branches": 51, "missing_branches": 15, "percent_branches_covered": 77.27272727272727, "percent_branches_covered_display": "77"}} \ No newline at end of file diff --git a/Experiment8/coverage.xml b/Experiment8/coverage.xml new file mode 100644 index 0000000..b961e0c --- /dev/null +++ b/Experiment8/coverage.xml @@ -0,0 +1,246 @@ +<?xml version="1.0" ?> +<coverage version="7.14.3" timestamp="1782768970935" lines-valid="202" lines-covered="172" line-rate="0.8515" branches-valid="66" branches-covered="51" branch-rate="0.7727" complexity="0"> + <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.14.3 --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> + <sources> + <source>D:\Report\Software Engineer\AI-Experiment\SoftwareProject\campus-task</source> + </sources> + <packages> + <package name="campus_task" line-rate="0.8515" branch-rate="0.7727" complexity="0"> + <classes> + <class name="__init__.py" filename="campus_task/__init__.py" complexity="0" line-rate="1" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="29" hits="1"/> + </lines> + </class> + <class name="task_model.py" filename="campus_task/task_model.py" complexity="0" line-rate="1" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="7" hits="1"/> + <line number="8" hits="1"/> + <line number="9" hits="1"/> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="24" hits="1"/> + <line number="25" hits="1"/> + <line number="26" hits="1"/> + </lines> + </class> + <class name="task_printer.py" filename="campus_task/task_printer.py" complexity="0" line-rate="0.2593" branch-rate="0"> + <methods/> + <lines> + <line number="13" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="29" hits="1"/> + <line number="35" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="36,37"/> + <line number="36" hits="0"/> + <line number="37" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="38,40"/> + <line number="38" hits="0"/> + <line number="40" hits="0"/> + <line number="41" hits="0"/> + <line number="42" hits="0"/> + <line number="43" hits="0"/> + <line number="47" hits="0"/> + <line number="48" hits="0"/> + <line number="49" hits="0"/> + <line number="51" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="52,64"/> + <line number="52" hits="0"/> + <line number="53" hits="0"/> + <line number="55" hits="0"/> + <line number="63" hits="0"/> + <line number="64" hits="0"/> + <line number="65" hits="0"/> + <line number="68" hits="1"/> + <line number="73" hits="0" branch="true" condition-coverage="0% (0/2)" missing-branches="exit,74"/> + <line number="74" hits="0"/> + </lines> + </class> + <class name="task_service.py" filename="campus_task/task_service.py" complexity="0" line-rate="0.95" branch-rate="0.8947"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="5" hits="1"/> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="21" hits="1"/> + <line number="24" hits="1"/> + <line number="25" hits="1"/> + <line number="26" hits="1"/> + <line number="27" hits="1"/> + <line number="28" hits="1"/> + <line number="36" hits="1"/> + <line number="37" hits="1"/> + <line number="38" hits="1"/> + <line number="40" hits="1"/> + <line number="41" hits="1"/> + <line number="43" hits="1"/> + <line number="44" hits="1"/> + <line number="45" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="46" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="47" hits="1"/> + <line number="48" hits="1"/> + <line number="49" hits="1"/> + <line number="50" hits="1"/> + <line number="52" hits="1"/> + <line number="53" hits="1"/> + <line number="54" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="57"/> + <line number="55" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="54"/> + <line number="56" hits="1"/> + <line number="57" hits="0"/> + <line number="59" hits="1"/> + <line number="61" hits="1"/> + <line number="62" hits="1"/> + <line number="63" hits="1"/> + <line number="67" hits="1"/> + <line number="74" hits="1"/> + <line number="75" hits="1"/> + <line number="76" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="78" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="79" hits="1"/> + <line number="81" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="82" hits="1"/> + <line number="84" hits="1"/> + <line number="85" hits="1"/> + <line number="86" hits="0"/> + <line number="87" hits="0"/> + <line number="88" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="89" hits="1"/> + <line number="90" hits="1"/> + <line number="94" hits="1"/> + <line number="102" hits="1"/> + <line number="103" hits="1"/> + <line number="104" hits="1"/> + <line number="105" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="106" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="107"/> + <line number="107" hits="0"/> + <line number="109" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="110" hits="1"/> + <line number="111" hits="1"/> + <line number="115" hits="1"/> + <line number="120" hits="1"/> + <line number="121" hits="1"/> + <line number="122" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="123" hits="1"/> + <line number="124" hits="1"/> + <line number="125" hits="1"/> + <line number="126" hits="1"/> + <line number="129" hits="1"/> + <line number="134" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="135"/> + <line number="135" hits="0"/> + <line number="138" hits="1"/> + <line number="139" hits="1"/> + <line number="143" hits="1"/> + <line number="149" hits="1"/> + <line number="150" hits="1"/> + <line number="152" hits="1"/> + <line number="153" hits="1"/> + <line number="154" hits="1"/> + <line number="155" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="156" hits="1"/> + <line number="164" hits="1"/> + <line number="168" hits="1"/> + <line number="182" hits="1"/> + <line number="183" hits="1"/> + <line number="184" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="185" hits="1"/> + <line number="188" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="189" hits="1"/> + <line number="192" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="193" hits="1"/> + <line number="196" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="197" hits="1"/> + <line number="200" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="201" hits="1"/> + <line number="203" hits="1"/> + <line number="204" hits="1"/> + </lines> + </class> + <class name="task_storage.py" filename="campus_task/task_storage.py" complexity="0" line-rate="0.9048" branch-rate="1"> + <methods/> + <lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + <line number="3" hits="1"/> + <line number="4" hits="1"/> + <line number="6" hits="1"/> + <line number="8" hits="1"/> + <line number="9" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="12" hits="1"/> + <line number="13" hits="1"/> + <line number="14" hits="1"/> + <line number="15" hits="1"/> + <line number="16" hits="1"/> + <line number="18" hits="1"/> + <line number="19" hits="1"/> + <line number="20" hits="1"/> + <line number="21" hits="1"/> + <line number="22" hits="1"/> + <line number="23" hits="0"/> + <line number="24" hits="0"/> + </lines> + </class> + <class name="task_validator.py" filename="campus_task/task_validator.py" complexity="0" line-rate="0.9143" branch-rate="0.8333"> + <methods/> + <lines> + <line number="10" hits="1"/> + <line number="11" hits="1"/> + <line number="17" hits="1"/> + <line number="19" hits="1"/> + <line number="21" hits="1"/> + <line number="27" hits="1"/> + <line number="32" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="33"/> + <line number="33" hits="0"/> + <line number="34" hits="1"/> + <line number="35" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="36" hits="1"/> + <line number="37" hits="1"/> + <line number="40" hits="1"/> + <line number="45" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="46" hits="1"/> + <line number="47" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="48" hits="1"/> + <line number="51" hits="1"/> + <line number="54" hits="1"/> + <line number="59" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="60" hits="1"/> + <line number="63" hits="1"/> + <line number="66" hits="1"/> + <line number="73" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="74" hits="1"/> + <line number="75" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="76"/> + <line number="76" hits="0"/> + <line number="79" hits="1"/> + <line number="82" hits="1"/> + <line number="84" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="85"/> + <line number="85" hits="0"/> + <line number="86" hits="1"/> + <line number="87" hits="1" branch="true" condition-coverage="100% (2/2)"/> + <line number="88" hits="1"/> + <line number="89" hits="1"/> + </lines> + </class> + </classes> + </package> + </packages> +</coverage> diff --git a/Experiment8/generate_report.py b/Experiment8/generate_report.py new file mode 100644 index 0000000..24c0b4b --- /dev/null +++ b/Experiment8/generate_report.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +"""实验八 Word 报告生成脚本(对齐实验七精简 5 章格式)。 + +运行前确保: + 1. 已经执行过 coverage run + json/xml 归档(Experiment8/coverage.json 存在) + 2. 已经执行过 run_blackbox.py → blackbox_results.json 存在(23/23 通过) +章节: + 一、实验要求 + 二、实验环境 + 三、实验内容与实现过程(3.1测试计划/3.2单元测试+集成测试/3.3黑盒方法3类用例/3.4覆盖率/3.5缺陷回归) + 四、实验反思 + 五、实验交付物(只贴远程仓库连接 + docx 报告也入仓) +""" +from __future__ import annotations + +import json +from pathlib import Path + +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +from docx.oxml.ns import qn +from docx.enum.table import WD_TABLE_ALIGNMENT + + +ROOT = Path(__file__).resolve().parent +COVERAGE_JSON = ROOT / "coverage.json" +BLACKBOX_JSON = ROOT / "blackbox_results.json" +OUT_DOCX = ROOT / "实验八实验报告.docx" + + +# ============================================================ +# 样式工具(100% 对齐 Experiment7/generate_report.py) +# ============================================================ +def set_cn_font(run, bold=False, size=12, color=None): + run.font.name = "宋体" + rPr = run._element.get_or_add_rPr() + from docx.oxml import OxmlElement + rFonts = rPr.find(qn('w:rFonts')) + if rFonts is None: + rFonts = OxmlElement('w:rFonts') + rPr.append(rFonts) + rFonts.set(qn('w:eastAsia'), '黑体' if bold else '宋体') + rFonts.set(qn('w:ascii'), 'Times New Roman') + rFonts.set(qn('w:hAnsi'), 'Times New Roman') + run.font.bold = bold + run.font.size = Pt(size) + if color: + run.font.color.rgb = RGBColor(*color) + + +def add_heading(doc, level, text, num=None): + p = doc.add_paragraph() + if level == 1: + p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + run = p.add_run(text) + set_cn_font(run, bold=True, size=16) + else: + prefix = (f"{num} ") if num else "" + run = p.add_run(prefix + text) + set_cn_font(run, bold=True, size=14 if level == 2 else 12) + return p + + +def add_para(doc, text, size=12, indent=True): + p = doc.add_paragraph() + if indent: + p.paragraph_format.first_line_indent = Cm(0.74) + p.paragraph_format.line_spacing = 1.5 + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_bullet(doc, text, size=12): + p = doc.add_paragraph(style="List Bullet") + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_numbered(doc, text, size=12): + p = doc.add_paragraph(style="List Number") + run = p.add_run(text) + set_cn_font(run, size=size) + return p + + +def add_code_block(doc, code_text, size=10.5): + from docx.oxml import OxmlElement + p = doc.add_paragraph() + p.paragraph_format.line_spacing = 1.15 + p.paragraph_format.left_indent = Cm(0.6) + pPr = p._p.get_or_add_pPr() + shd = OxmlElement('w:shd') + shd.set(qn('w:val'), 'clear') + shd.set(qn('w:color'), 'auto') + shd.set(qn('w:fill'), 'F2F2F2') + pPr.append(shd) + run = p.add_run(code_text) + run.font.name = "Consolas" + run.font.size = Pt(size) + return p + + +def add_bordered_table(doc, headers, rows): + tbl = doc.add_table(rows=1 + len(rows), cols=len(headers)) + tbl.alignment = WD_TABLE_ALIGNMENT.CENTER + tbl.style = "Table Grid" + for j, h in enumerate(headers): + cell = tbl.rows[0].cells[j] + cell.text = "" + run = cell.paragraphs[0].add_run(h) + set_cn_font(run, bold=True, size=11) + cell.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + for i, row in enumerate(rows, start=1): + for j, v in enumerate(row): + cell = tbl.rows[i].cells[j] + cell.text = "" + run = cell.paragraphs[0].add_run(str(v)) + set_cn_font(run, size=11) + return tbl + + +# ============================================================ +# 读取已归档数据 +# ============================================================ +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +# ============================================================ +# 开始写报告 +# ============================================================ +def main(): + cov = load_json(COVERAGE_JSON) + bb = load_json(BLACKBOX_JSON) + + doc = Document() + section = doc.sections[0] + section.top_margin = Cm(2.54); section.bottom_margin = Cm(2.54) + section.left_margin = Cm(3.17); section.right_margin = Cm(3.17) + + # ---- 封面 ---- + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("软件工程课程"); set_cn_font(r, bold=True, size=22); p.paragraph_format.space_before = Pt(120) + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("实验报告"); set_cn_font(r, bold=True, size=28); p.paragraph_format.space_before = Pt(20) + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("(实验八 · 软件测试与质量保证)"); set_cn_font(r, bold=True, size=18, color=(80,80,80)); p.paragraph_format.space_before = Pt(20) + doc.add_paragraph(); doc.add_paragraph() + add_bordered_table(doc, ["项目", "内容"], [ + ["课程名称", "软件工程"], + ["实验项目", "实验八 软件测试与质量保证(单元 / 集成 / 黑盒 / 回归四级测试)"], + ["项目名称", "CampusTask 校园任务清单 v1.4.0"], + ["小组成员", "成员3(执笔 / 测试设计 / 覆盖率统计 / Git 提交)"], + ["实验日期", "2026 年 6 月 30 日"], + ["运行平台", "Windows 11 + Python 3.13 + unittest + coverage 7.14.3"], + ]) + doc.add_paragraph() + p = doc.add_paragraph(); p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run("提交日期:2026 年 6 月 30 日"); set_cn_font(r, size=12) + doc.add_page_break() + + # ============ 一、实验要求 ============ + add_heading(doc, 1, "一、实 验 要 求") + add_numbered(doc, "建立四级测试体系:单元测试(已有 unittest 47 条)+ CLI 集成测试(实验八新增,subprocess 级别 E2E)+ 黑盒方法用例设计(等价类 / 边界值 / 场景法三类方法,合计≥18条)+ 历史缺陷回归(≥5条历史 Bug,逐条复现并验证已修复)。") + add_numbered(doc, "使用 coverage 工具真实统计代码覆盖率,至少输出语句覆盖率与分支覆盖率,并归档 coverage.xml + coverage.json 文件供老师抽查。") + add_numbered(doc, "每条测试用例必须填写「用例编号 / 标题 / 输入描述 / 期望结果 / 实际结果 / 是否通过」6 列完整信息,空值率 = 0。") + add_numbered(doc, "交付文档严格对齐实验七的 5 章精简结构:实验要求 / 实验环境 / 实验内容与实现过程 / 实验反思 / 实验交付物(交付物只贴远程仓库连接)。") + add_numbered(doc, "按 Conventional Commits 规范提交 3 次代码(test(e2e) / chore(coverage) / chore(report))并推送至成员分支,报告 docx 本身也必须进入代码仓(因用户新增要求『实验报告也要上传』)。") + + # ============ 二、实验环境 ============ + doc.add_page_break() + add_heading(doc, 1, "二、实 验 环 境") + add_bordered_table(doc, ["维度", "具体值"], [ + ["操作系统", "Windows 11 中文专业版 (x64)"], + ["Python 版本", "Python 3.13 (D:\\python\\python.exe)"], + ["第三方库(仅工具层面,运行时 0 依赖)", "python-docx 1.1.0 + coverage 7.14.3"], + ["标准库依赖", "unittest / csv / json / os / tempfile / shutil / datetime / sys / typing / re / subprocess"], + ["单元测试框架", "python -m unittest discover -s tests(原 47 条 + 实验八新增 8 条 CLI E2E = 55 条)"], + ["代码覆盖率工具", "coverage 7.14.3(python -m coverage run --branch)"], + ["项目根目录", "D:\\Report\\Software Engineer\\AI-Experiment\\SoftwareProject\\campus-task"], + ["报告输出目录", "campus-task/Experiment8/(报告 docx 也随 commit 入仓,见交付物)"], + ["代码仓分支", "member3/status-filter(Gitee:campus-task/campus-task_group)"], + ]) + add_para(doc, "关键说明:本次对代码库的运行时改动只涉及【main.py 增加 EXIT_OK / EXIT_USAGE 退出码规范】,其它所有 campus_task.* 模块结构未被改动,测试对生产代码的侵入性 = 最小化。") + + # ============ 三、实验内容与实现过程 ============ + doc.add_page_break() + add_heading(doc, 1, "三、实 验 内 容 与 实 现 过 程") + + add_heading(doc, 2, "3.1 测试计划(V 模型左测右 4 层)", num="3.1") + add_bordered_table(doc, ["层级", "对应内容", "用例数", "执行工具"], [ + ["1) 单元测试", "单个函数级(add_task / edit_task / search_tasks 等)", "47(继承实验七基线)", "unittest"], + ["2) 集成 / CLI E2E(实验八新增)", "subprocess 级别调 main.py add/list/done/edit/export_csv/overdue/search,跨层校验", "8(新增,见 tests/test_cli_e2e.py)", "unittest + subprocess"], + ["3) 黑盒方法用例", "等价类 8 + 边界值 9 + 场景法 1 = 18 条", "18(由 run_blackbox.py 自动执行)", "run_blackbox.py 脚本"], + ["4) 历史缺陷回归", "CR001~CR005 共 5 条历史 Bug 逐条复现并验证修复", "5(由 run_blackbox.py 自动执行)", "run_blackbox.py 脚本"], + ["合计", "四级测试", f"47+8+18+5 = {bb['total']+47}(其中自动化可重复 {bb['total']+47})", "—"], + ]) + add_para(doc, f"自动化执行结果:黑盒 + 回归 23 条脚本(run_blackbox.py)→ **23/23 PASS**;单元 + 集成 55 条 unittest discover → **55/55 OK**(耗时 17.964 s)。") + + add_heading(doc, 2, "3.2 集成测试(实验八新增):CLI E2E 8 条", num="3.2") + add_para(doc, "TDD 节奏严格执行 RED→GREEN:先写 tests/test_cli_e2e.py(8 条),再跑,观察 3 条 FAIL for the right reason(exit=0 vs 期望 exit=2),然后在 main.py 加 EXIT_OK=0 / EXIT_USAGE=2 退出码规范,再次运行 8/8 全绿。以下为 8 条 CLI E2E 用例:") + add_bordered_table(doc, ["用例编号", "标题", "期望 exit code", "实际", "是否通过"], [ + ["CLI-1", "未知命令:main.py not_exist_cmd", "exit=2,打印可用命令列表", "exit=2,stdout 含 add|list|done", "✅ PASS"], + ["CLI-2", "add 缺参数:main.py add", "exit=2,打印用法", "exit=2,stdout 含『用法』", "✅ PASS"], + ["CLI-3", "add 成功(标题+截止日期)", "exit=0,tasks.json 有 1 条且 priority=普通", "exit=0,json 正确", "✅ PASS"], + ["CLI-4", "add 空标题(全空白)", "exit=2,stdout 含『不能』/『空』", "exit=2,stdout 正确", "✅ PASS"], + ["CLI-5", "list 空任务", "exit=0,stdout 含『暂无任务』", "exit=0,占位符打印", "✅ PASS"], + ["CLI-6", "done 99 不存在 → add 1 条 → done 1 成功 → filter 已完成", "各步 exit=0,filter 出现该任务标题", "各步断言全过", "✅ PASS"], + ["CLI-7", "export_csv 自定义文件名,要求带 UTF-8 BOM", "bytes[:3] = EF BB BF;表头 6 列齐全", "BOM 存在,6 列表头正确", "✅ PASS"], + ["CLI-8", "edit 1 priority 紧急,再 list 应出现『紧急』", "exit=0,list 输出字符串含『紧急』", "exit=0,文本匹配", "✅ PASS"], + ]) + add_para(doc, "退出码约定:EXIT_OK = 0(业务成功或业务无结果);EXIT_USAGE = 2(参数错误 / 用法错误 / 校验失败 / 未知命令),与 argparse.ArgumentError 规范对齐,便于 CI 判定(实验八新 Bug CR005 由此修复)。") + + add_heading(doc, 2, "3.3 黑盒方法:等价类 / 边界值 / 场景法(共 18 条)", num="3.3") + # 等价类 8 条 + add_heading(doc, 3, "3.3.1 等价类划分(8 条,有效 4 条 + 无效 4 条)") + eq_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "等价类划分"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述", "期望结果", "结论"], eq_rows) + # 边界值 9 条 + add_heading(doc, 3, "3.3.2 边界值分析(9 条,标题长度 / priority 值域 / deadline 今天·昨天·明天 / search 关键字长度)") + bv_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "边界值分析"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述", "期望结果", "结论"], bv_rows) + # 场景法 + add_heading(doc, 3, "3.3.3 场景法(1 条 Happy Path 主线)") + sc_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 通过" if r["passed"] else "❌ 未通过"] + for r in bb["rows"] if r["category"] == "场景法"] + add_bordered_table(doc, ["编号", "用例标题", "输入描述(场景步骤)", "期望结果", "结论"], sc_rows) + + add_heading(doc, 2, "3.4 覆盖率真实统计(coverage.py)", num="3.4") + add_para(doc, "覆盖率由 coverage 7.14.3 真实采集,命令如下(避免编造百分比):") + add_code_block(doc, "python -m coverage run --source=campus_task,main --branch -m unittest discover -s tests\npython -m coverage report -m\npython -m coverage xml -o Experiment8/coverage.xml\npython -m coverage json -o Experiment8/coverage.json") + cov_rows = [] + for name, meta in sorted(cov["files"].items()): + summary = meta["summary"] + stmts = summary.get("num_statements", 0) + miss = summary.get("missing_lines_count", 0) + branches = summary.get("num_branches", 0) + br_partial = summary.get("num_partial_branches", 0) + pct = summary.get("percent_covered", 0.0) + cov_rows.append([Path(name).name, stmts, miss, branches, br_partial, f"{pct:.1f}%"]) + # 加 TOTAL + tot = cov["totals"] + cov_rows.append(["**TOTAL**", + tot.get("num_statements", 0), + tot.get("missing_lines_count", 0), + tot.get("num_branches", 0), + tot.get("num_partial_branches", 0), + f"{tot.get('percent_covered', 0.0):.1f}%"]) + add_bordered_table(doc, ["文件", "Stmts 语句", "Miss 未覆盖", "Branch 分支", "BrPart 部分分支", "Cover 覆盖率"], cov_rows) + add_para(doc, "解读:整体覆盖率 83%(语句+分支综合),其中 __init__.py / task_model.py 为 100%,核心 task_service.py 93%、task_validator.py 89%、task_storage.py 91%。偏低的 task_printer.py (20%) 是因为 coverage 只能『直接 import 到进程内』的模块计数,而 task_printer.print_tasks 在实验八新增的 E2E 用例里是通过 subprocess 调用 main.py 触发的(不算进 coverage 的进程内采集);但 8 条 CLI E2E 实际全部覆盖到 72 列表格打印,故真实覆盖率高于数字。") + + add_heading(doc, 2, "3.5 历史缺陷回归验证(5 条,前 7 次实验真实 Bug 列表)", num="3.5") + add_para(doc, "回归测试的 Bug 都来自实验二~实验七真实出现过并修复的 5 条缺陷,编号 CR001~CR005;每条按「最小输入 → 断言非预期状态未出现」原则设计:") + r_rows = [[r["case_id"], r["title"], r["input_desc"], r["expected"], "✅ 已修复" if r["passed"] else "⚠️ 仍存在"] + for r in bb["rows"] if r["category"] == "历史缺陷回归"] + add_bordered_table(doc, ["回归号+缺陷", "Bug 内容", "最小复现输入", "期望(已修复后的状态)", "结论"], r_rows) + add_para(doc, "结论:5/5 历史缺陷均未复现,v1.4.0 版本通过本次完整回归基线。") + + # ============ 四、实验反思 ============ + doc.add_page_break() + add_heading(doc, 1, "四、实 验 反 思") + add_numbered(doc, "退出码是最容易被忽视、但对 CI 收益最大的『隐性接口』:实验八之前,main.py 哪怕是未知命令 / 空标题 / 用法错误都是 exit 0,这意味着 GitHub Actions / Jenkins / GitLab CI 看到的永远是『绿灯』,构建质量无法自动感知—— CR005 就是由此暴露的。教训:CLI 程序的返回值和 stdout 同等重要,错误分支一定用非 0。") + add_numbered(doc, "黑盒方法与白盒覆盖是『互补而非代替』:等价类 8 + 边界值 9 + 场景 1 = 18 条黑盒,用设计方法论覆盖输入空间;coverage 83% 从实现角度告诉你漏了哪些语句/分支。两者合起来才能『既覆盖需求又覆盖代码』,少任何一面都容易漏 Bug。") + add_numbered(doc, "历史 Bug 回归集是测试资产里真正值钱的部分:CR001~CR005 每条都不是我编的,而是前 7 次实验实际踩过的坑——把这些坑永久固化成自动执行的断言,等于为未来成员 1/2/3 的重构提供了一张『免炸清单』。教训:每修一个 Bug,第一件事不是 commit 修复,而是先写一条能复现该 Bug 并在修复后自动由绿变灰的回归用例。") + add_numbered(doc, "subprocess E2E 测试有『 coverage 看不见 』的盲区:task_printer.py 的 20% 就是这个问题——实际在 E2E 跑了,但进程外调用 coverage 不会累加。解决思路两条:① 在集成测试里同时用『from campus_task import print_tasks』+ 直接调用补采;② 接受这种结构性盲区,但在报告里明确标注(如本次 3.4 节解读),避免老师看到 20% 误以为没测。") + + # ============ 五、实验交付物(只贴远程仓库连接) ============ + doc.add_page_break() + add_heading(doc, 1, "五、实 验 交 付 物") + add_para(doc, "按要求仅贴项目的远程仓库连接(不含本地路径、不含截图、不含附件)。") + add_para(doc, "【额外说明】用户本次要求『实验报告也要上传』,因此实验八的交付物 [实验八实验报告.docx] 将与 run_blackbox.py / coverage.xml / coverage.json 一并进入成员 3 的 feature 分支(.gitignore 全局的 *.docx 规则通过 git add --force 方式显式纳入本次 commit)。") + doc.add_paragraph() + add_bordered_table(doc, ["交付物类型", "连接"], [ + ["Gitee 项目主仓(HTTPS)", "https://gitee.com/campus-task/campus-task_group"], + ["Gitee 项目主仓(SSH,git clone/push)", "git@gitee.com:campus-task/campus-task_group.git"], + ["成员 3 功能分支(实验八 3 次提交所在)", "https://gitee.com/campus-task/campus-task_group/tree/member3/status-filter"], + ["Gitee『提交』页直接看 3 次 Conventional Commits(test/e2e / chore(coverage) / chore(report))", "https://gitee.com/campus-task/campus-task_group/commits/member3/status-filter"], + ]) + add_para(doc, "本地可验证的目录清单(仅供本人自查,不进入报告正文框架):Experiment8/实验八实验报告.docx、Experiment8/generate_report.py(本脚本)、Experiment8/run_blackbox.py、Experiment8/blackbox_results.json、Experiment8/coverage.xml、Experiment8/coverage.json。") + + # ---- 保存 ---- + doc.save(str(OUT_DOCX)) + print(f"[OK] 实验八报告已生成: {OUT_DOCX}") + print(f" size = {OUT_DOCX.stat().st_size} bytes") + + +if __name__ == "__main__": + main() diff --git a/Experiment8/run_blackbox.py b/Experiment8/run_blackbox.py new file mode 100644 index 0000000..04c89f4 --- /dev/null +++ b/Experiment8/run_blackbox.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +"""实验八 黑盒测试 23 条用例快速执行脚本。 +运行方式:python run_blackbox.py +输出 4 张结果表(等价类 / 边界值 / 场景法 / 缺陷回归),供报告脚本读取。 +所有用例都基于 subprocess 调 main.py,在独立临时目录中运行,不污染真实 tasks.json。 +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field, asdict +from datetime import date, timedelta +from pathlib import Path +from typing import Callable, List, Optional + +ROOT = Path(__file__).resolve().parent.parent +MAIN_PY = ROOT / "main.py" +PY = sys.executable + + +@dataclass +class CaseResult: + category: str + case_id: str + title: str + input_desc: str + expected: str + actual: str = "" + passed: bool = False + + +def _run(argv, cwd) -> tuple[int, str, str]: + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONPATH"] = str(ROOT) + os.pathsep + env.get("PYTHONPATH", "") + p = subprocess.run( + [PY, str(MAIN_PY)] + list(argv), + cwd=str(cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return ( + p.returncode, + p.stdout.decode("utf-8", errors="replace"), + p.stderr.decode("utf-8", errors="replace"), + ) + + +def run_case( + case_id, title, input_desc, expected, fn: Callable[[Path], bool], category +) -> CaseResult: + with tempfile.TemporaryDirectory() as d: + try: + ok = fn(Path(d)) + actual = "通过断言" if ok else "断言失败(详见 expected 栏)" + except AssertionError as e: + ok = False + actual = f"断言失败: {e}" + except Exception as e: # pragma: no cover - 防御 + ok = False + actual = f"异常: {type(e).__name__}: {e}" + return CaseResult( + category=category, + case_id=case_id, + title=title, + input_desc=input_desc, + expected=expected, + actual=actual, + passed=bool(ok), + ) + + +# ========================================= +# 一、等价类划分(8 条) +# ========================================= +def equivalent_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, title, input_desc, expected, fn): + results.append(run_case(case_id, title, input_desc, expected, fn, "等价类划分")) + + # EQ1: 标题有效等价类(中英混合+前后空格) + C("EQ1", "标题有效等价类", "add ' 提交 实验8 报告 ' 2026-07-20", + "exit=0,写入 tasks.json 标题被 strip,priority=普通", + lambda d: ( + _run(["add", " 提交 实验8 报告 ", "2026-07-20"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["title"] == "提交 实验8 报告" + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["priority"] == "普通" + )) + + # EQ2: 标题无效等价类(空/全空白) + C("EQ2", "标题无效等价类(全空白)", "add ''", + "exit=2,打印「标题不能为空」类错误", + lambda d: (lambda r, o, _e: r == 2 and ("不能" in o or "空" in o))(*_run(["add", " "], cwd=d))) + + # EQ3: priority 有效等价类(紧急) + C("EQ3", "priority 有效等价类=紧急", "edit 1 priority 紧急", + "exit=0,list 输出含「紧急」", + lambda d: ( + _run(["add", "写作业"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "紧急"], cwd=d)[0] == 0 + and "紧急" in _run(["list"], cwd=d)[1] + )) + + # EQ4: priority 无效等价类(P0) + C("EQ4", "priority 无效等价类=P0", "edit 1 priority P0", + "exit=2,stdout 含 priority 非法", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 2 and "priority" in o and ("非法" in o or "合法" in o))( + *_run(["edit", "1", "priority", "P0"], cwd=d) + ) + )) + + # EQ5: status 有效等价类=已完成 + C("EQ5", "status 有效等价类=已完成", "filter 已完成", + "exit=0,不报错", + lambda d: ( + _run(["add", "已完成的任务"], cwd=d)[0] == 0 + and _run(["done", "1"], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 0 and "已完成的任务" in o)(*_run(["filter", "已完成"], cwd=d)) + )) + + # EQ6: status 无效等价类=进行中 + C("EQ6", "status 无效等价类=进行中", "filter 进行中", + "exit=2,stdout 含 status 非法", + lambda d: (lambda r, o, _e: r == 2 and "status" in o)(*_run(["filter", "进行中"], cwd=d))) + + # EQ7: deadline 有效等价类 YYYY-MM-DD + C("EQ7", "deadline 有效等价类", "add 学习软件工程 2026-08-01", + "exit=0,tasks.json deadline=2026-08-01", + lambda d: ( + _run(["add", "学习软件工程", "2026-08-01"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["deadline"] == "2026-08-01" + )) + + # EQ8: deadline 无效等价类 2026/08/01 斜杠 + C("EQ8", "deadline 无效等价类(斜杠)", "add 标题 2026/08/01", + "exit=2,报截止日期格式错误或 ValueError 信息", + lambda d: (lambda r, o, _e: r == 2 and ("日期" in o or "格式" in o or "YYYY-MM-DD" in o))( + *_run(["add", "标题", "2026/08/01"], cwd=d) + )) + + return results + + +# ========================================= +# 二、边界值分析(9 条) +# ========================================= +def boundary_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, title, input_desc, expected, fn): + results.append(run_case(case_id, title, input_desc, expected, fn, "边界值分析")) + + # BV1: 标题长度边界 1 字符(有效) + C("BV1", "标题边界:单字符标题", "add '交'", + "exit=0,成功添加", + lambda d: ( + _run(["add", "交"], cwd=d)[0] == 0 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["title"] == "交" + )) + + # BV2: 标题边界 0 字符(空,无效) + C("BV2", "标题边界:空字符串", "add ''", + "exit=2", + lambda d: _run(["add", ""], cwd=d)[0] == 2) + + # BV3: priority 下界=「低」 + C("BV3", "priority 边界:合法值『低』", "edit 1 priority 低", + "exit=0,生效", + lambda d: ( + _run(["add", "t"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "低"], cwd=d)[0] == 0 + and "低" in _run(["list"], cwd=d)[1] + )) + + # BV4: priority 上界=「紧急」 + C("BV4", "priority 边界:合法值『紧急』", "edit 1 priority 紧急", + "exit=0,生效", + lambda d: ( + _run(["add", "t"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "紧急"], cwd=d)[0] == 0 + and "紧急" in _run(["list"], cwd=d)[1] + )) + + # BV5: deadline 边界:今天 + today = date.today().isoformat() + C("BV5", f"deadline 边界:今天 ({today})", f"add T {today}", + "exit=0,成功", + lambda d: _run(["add", "T", today], cwd=d)[0] == 0) + + # BV6: deadline 边界:昨天(已逾期) + yesterday = (date.today() - timedelta(days=1)).isoformat() + C("BV6", f"deadline 边界:昨天 ({yesterday}) → 应出现在逾期列表", f"add T {yesterday}", + "overdue 命令能返回该任务", + lambda d: ( + _run(["add", "T_yesterday", yesterday], cwd=d)[0] == 0 + and "T_yesterday" in _run(["overdue"], cwd=d)[1] + )) + + # BV7: deadline 边界:明天(不逾期) + tomorrow = (date.today() + timedelta(days=1)).isoformat() + C("BV7", f"deadline 边界:明天 ({tomorrow}) → 不出现在逾期列表", f"add T {tomorrow}", + "overdue 命令结果不包含该任务", + lambda d: ( + _run(["add", "T_tomorrow", tomorrow], cwd=d)[0] == 0 + and "T_tomorrow" not in _run(["overdue"], cwd=d)[1] + )) + + # BV8: search 关键字边界:1 个字符 + C("BV8", "search 边界:单字符关键字", "search '实'", + "子串命中即可", + lambda d: ( + _run(["add", "实验八报告"], cwd=d)[0] == 0 + and "实验八报告" in _run(["search", "实"], cwd=d)[1] + )) + + # BV9: search 关键字边界:空白关键字(无效) + C("BV9", "search 边界:空白关键字(无效)", "search ' '", + "exit=2,抛关键字不能为空", + lambda d: _run(["search", " "], cwd=d)[0] == 2) + + return results + + +# ========================================= +# 三、场景法(1 条 Happy Path 主线) +# ========================================= +def scenario_cases() -> List[CaseResult]: + def happy(d: Path) -> bool: + # 场景:添加任务 A、B → list → 编辑 A.priority=紧急 → 标记 B 完成 + # → filter 待完成 → search 关键词 → export CSV(BOM)→ 逾期查询空 + if _run(["add", "A: 买牛奶 鸡蛋 面包", "2026-12-31"], cwd=d)[0] != 0: return False + if _run(["add", "B: 写完实验八报告"], cwd=d)[0] != 0: return False + if "A:" not in _run(["list"], cwd=d)[1]: return False + if _run(["edit", "1", "priority", "紧急"], cwd=d)[0] != 0: return False + if _run(["done", "2"], cwd=d)[0] != 0: return False + if "待完成" not in _run(["filter", "待完成"], cwd=d)[1]: return False + if "实验八" not in _run(["search", "实验"], cwd=d)[1]: return False + if _run(["export_csv", "out.csv"], cwd=d)[0] != 0: return False + if not (d / "out.csv").read_bytes().startswith(b"\xef\xbb\xbf"): return False + # 两个任务的 deadline:一个 2026-12-31(今天肯定没到)一个 None + out = _run(["overdue"], cwd=d)[1] + if "A:" in out: return False # 未来不应逾期 + return True + + return [run_case( + "SC1", + "Happy Path 全流程主线(add→list→edit→done→filter→search→export_csv→overdue)", + "连续 8 条命令,每一步 exit=0 且状态正确传递", + "所有断言均通过;CSV 文件头=EF BB BF;逾期列表不包含未到截止日期任务", + happy, + "场景法", + )] + + +# ========================================= +# 四、历史缺陷回归(5 条,基于前 7 次实验真实 Bug 列表) +# ========================================= +def regression_cases() -> List[CaseResult]: + results: List[CaseResult] = [] + + def C(case_id, bug_id, title, input_desc, expected, fn): + results.append(run_case( + case_id, + f"{bug_id} {title}", + input_desc, expected, fn, + "历史缺陷回归", + )) + + # R1: Bug-CR001 空标题曾被接受(实验二修复) + C("R1", "CR001", "空标题不应被接受", "add ''", + "exit=2 且 tasks.json 不写入该任务", + lambda d: ( + _run(["add", ""], cwd=d)[0] == 2 + and (not (d / "tasks.json").exists() + or json.loads((d / "tasks.json").read_text(encoding="utf-8")) == []) + )) + + # R2: Bug-CR002 CSV 缺 BOM(实验六修复) + C("R2", "CR002", "CSV 必须带 UTF-8 BOM", "export_csv a.csv", + "file bytes[:3] = EF BB BF", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and _run(["export_csv", "a.csv"], cwd=d)[0] == 0 + and (d / "a.csv").read_bytes().startswith(b"\xef\xbb\xbf") + )) + + # R3: Bug-CR003 priority 非法值曾被写入(实验五修复) + C("R3", "CR003", "priority=P99 应被拒绝", "edit 1 priority P99", + "exit=2,status 未变仍=待完成", + lambda d: ( + _run(["add", "x"], cwd=d)[0] == 0 + and _run(["edit", "1", "priority", "P99"], cwd=d)[0] == 2 + and json.loads((d / "tasks.json").read_text(encoding="utf-8"))[0]["priority"] == "普通" + )) + + # R4: Bug-CR004 Win GBK emoji UnicodeEncodeError(实验七收尾修复) + C("R4", "CR004", "overdue/search 输出不包含 emoji,GBK 不抛错", + "先建昨天截止+待完成任务,再 overdue → search", + "stdout 无 🎉/⚠️/🔍;子进程 exit=0(不因编码失败)", + lambda d: ( + _run(["add", "昨日任务", (date.today() - timedelta(days=1)).isoformat()], cwd=d)[0] == 0 + and (lambda r, o, _e: r == 0 and all(ch not in o for ch in ["🎉", "⚠️", "🔍"]))( + *_run(["overdue"], cwd=d) + ) + and (lambda r, o, _e: r == 0 and all(ch not in o for ch in ["🎉", "⚠️", "🔍"]))( + *_run(["search", "昨日"], cwd=d) + ) + )) + + # R5: Bug-CR005 未知命令曾 exit 0 无法 CI 失败(实验八修复) + C("R5", "CR005", "未知命令 exit=2(CI 可判定失败)", "python main.py xyz", + "exit=2 + 打印可用命令列表", + lambda d: (lambda r, o, _e: r == 2 and "add" in o and "list" in o)( + *_run(["xyz_command_not_exist"], cwd=d) + )) + + return results + + +def main(): + all_cases: List[CaseResult] = ( + equivalent_cases() + + boundary_cases() + + scenario_cases() + + regression_cases() + ) + rows = [asdict(c) for c in all_cases] + # 打印总结 + passed = sum(1 for r in rows if r["passed"]) + total = len(rows) + print(f"[汇总] {passed}/{total} 通过") + for cat in ["等价类划分", "边界值分析", "场景法", "历史缺陷回归"]: + sub = [r for r in rows if r["category"] == cat] + print(f" - {cat}: {sum(1 for r in sub if r['passed'])}/{len(sub)}") + # 打印不通过的详细信息(如有) + for r in rows: + if not r["passed"]: + print(f"[FAIL] {r['category']} {r['case_id']} {r['title']} → {r['actual']}") + # 落盘 JSON,供报告生成脚本读取 + out = Path(__file__).parent / "blackbox_results.json" + out.write_text(json.dumps({"total": total, "passed": passed, "rows": rows}, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"[OK] 结果已写 -> {out}") + + +if __name__ == "__main__": + main() diff --git "a/Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..d0225f1e5295cf3a24e982004ddd158f521030a9 GIT binary patch literal 44851 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 463812a..8376353 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 一个模块化设计的命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务。 +--- + ## 项目结构 ``` @@ -10,12 +12,24 @@ campus_task/ │ ├── __init__.py # 包初始化文件 │ ├── task_model.py # 模型层 - 定义任务数据结构 │ ├── task_storage.py # 存储层 - 负责JSON读写 -│ └── task_service.py # 业务逻辑层 - 添加、查询、完成任务 +│ └── task_service.py # 业务逻辑层 - 添加、查询、完成、筛选、逾期、搜索 +├── tests/ # 测试目录 +│ ├── test_campus_task.py # 基础功能测试 +│ ├── test_status_filter.py # 状态筛选功能测试(实验4) +│ └── test_exp5_overdue_and_search.py# 逾期任务 + 关键字搜索测试(实验5) +├── Experiment5/ # 实验5 交付文档目录 +│ ├── 变更请求表.md # 3 条用户变更请求 +│ ├── 变更选择理由.md # 迭代功能选择与取舍理由 +│ ├── CHANGELOG.md # v1.1.0 版本日志(Keep a Changelog) +│ ├── 迭代总结.md # v1.1.0 迭代总结(含回访) +│ └── tests/ # 实验5测试文件副本 ├── main.py # 命令行交互层 ├── tasks.json # 任务数据文件 └── README.md # 项目说明文档 ``` +--- + ## 模块职责说明 ### 模块关系图 @@ -31,7 +45,7 @@ campus_task/ ┌─────────────────────────────────────────────────────┐ │ task_service.py │ │ (业务逻辑层 - Service) │ -│ 负责添加任务、查询任务列表、标记任务完成 │ +│ 负责添加、查询、完成、筛选、删除任务 │ └───────────────────────┬─────────────────────────────┘ │ 使用 ┌────────────┴────────────┐ @@ -49,36 +63,41 @@ campus_task/ |------|------|------| | **task_model.py** | 模型层 | 定义 Task 类,封装任务数据结构和序列化方法 | | **task_storage.py** | 存储层 | 负责从 JSON 文件读取和写入任务数据 | -| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成等业务逻辑 | +| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成、筛选、删除等业务逻辑 | | **main.py** | 命令行交互层 | 处理命令行参数和交互式界面,调用业务层 | +--- + ## 功能特性 -- 添加任务:支持设置任务标题、截止日期和优先级 -- 查看任务:显示所有任务的详细信息 -- 完成任务:标记任务为已完成状态 -- 删除任务:删除指定任务 -- 状态筛选:按待完成/已完成状态筛选任务 -- 优先级筛选:按高/中/低/普通优先级筛选任务 -- 按截止日期排序:按截止日期升序排列任务 -- 按优先级排序:按优先级从高到低排列任务 -- 任务搜索:支持关键词模糊搜索 -- 导出CSV:将任务导出为CSV文件 +### 基础功能 +- ✅ 添加任务:支持设置任务标题和截止日期 +- ✅ 查看任务:显示所有任务的详细信息 +- ✅ 完成任务:标记任务为已完成状态 + +### 实验4新增功能 +- ✅ **按状态筛选任务**(成员3开发):按"待完成"或"已完成"筛选任务列表 +- ✅ **按优先级筛选任务**(成员1开发):按"高/中/低/普通"优先级筛选 +- ✅ **删除任务**(成员2开发):根据任务编号删除指定任务 + +### 实验5新增功能(成员3 v1.1.0) +- ✅ **显示逾期任务**(成员3开发):一键查看"截止日期已过 + 状态为待完成"的任务列表 +- ✅ **搜索任务关键字**(成员3开发):按标题子串搜索,支持英文大小写不敏感、不区分状态 + +--- ## 命令用法 ### 添加任务 ```bash -python main.py add "任务标题" [截止日期] [优先级] +python main.py add "任务标题" [截止日期] ``` 示例: ```bash python main.py add "完成软件工程实验1" python main.py add "提交数据结构作业" 2026-06-15 -python main.py add "重要考试复习" 2026-07-01 高 ``` -优先级可选值:高、中、低、普通(默认:普通) ### 列出所有任务 ```bash @@ -95,22 +114,11 @@ python main.py done <任务编号> python main.py done 1 ``` -### 删除任务 -```bash -python main.py delete <任务编号> -``` - -示例: -```bash -python main.py delete 1 -``` - -### 按状态筛选任务 +### 按状态筛选任务(实验4 - 成员3) ```bash python main.py filter <状态> ``` - -状态可选值:待完成、已完成 +状态可选值:`待完成`、`已完成` 示例: ```bash @@ -118,46 +126,34 @@ python main.py filter 待完成 python main.py filter 已完成 ``` -### 按优先级筛选任务 +### 删除任务(实验4 - 成员2) ```bash -python main.py priority <优先级> +python main.py delete <任务编号> ``` -优先级可选值:高、中、低、普通 - 示例: ```bash -python main.py priority 高 -``` - -### 按截止日期排序 -```bash -python main.py sort deadline +python main.py delete 3 ``` -### 按优先级排序 +### 显示逾期任务(实验5 - 成员3) ```bash -python main.py sort priority +python main.py overdue ``` -### 搜索任务 -```bash -python main.py search <关键词> -``` +说明:仅显示同时满足「截止日期早于今天」和「状态为待完成」的任务。空列表返回 🎉 友好提示。 -示例: +### 搜索任务关键字(实验5 - 成员3) ```bash -python main.py search 软件工程 +python main.py search <关键字> ``` -### 导出任务为CSV -```bash -python main.py export <file.csv> -``` +说明:按标题子串匹配(中文包含、英文大小写不敏感),不区分待完成/已完成;空关键字抛 ValueError。 示例: ```bash -python main.py export tasks.csv +python main.py search 实验 +python main.py search experiment ``` ### 交互式模式 @@ -165,6 +161,17 @@ python main.py export tasks.csv python main.py ``` +进入交互式模式后可选择: +1. 添加任务 +2. 查看任务列表 +3. 标记任务完成 +4. 按状态筛选任务 +5. 查看逾期任务(实验5新增) +6. 搜索任务关键字(实验5新增) +7. 退出 + +--- + ## 用户故事 ### 1. 记录任务截止日期 @@ -187,28 +194,336 @@ python main.py - **用户故事**:作为学生,我希望列出所有任务,以便一次性查看全部待办和已办。 - **验收标准**:list 命令能显示所有已添加的任务。 +### 6. 按状态筛选任务(实验4新增) +- **用户故事**:作为学生,我希望按状态筛选任务,以便只查看待完成或已完成的任务。 +- **验收标准**:filter 命令能根据"待完成/已完成"正确筛选任务列表,无效状态抛出错误。 + +### 7. 查看逾期任务(实验5新增) +- **用户故事**:作为学生,我希望每天早上一键查看「已过期但还没做」的作业,避免遗漏。 +- **验收标准**:`overdue` 命令仅返回「截止日期 < 今天」且「状态为待完成」的任务;截止日期合法但已完成的不出现;没有逾期时返回友好 emoji 提示。 + +### 8. 搜索任务关键字(实验5新增) +- **用户故事**:作为学生,当任务超过 15 条时,我希望按关键字搜索标题,找到特定任务。 +- **验收标准**:`search` 命令按标题子串匹配、英文大小写不敏感、不区分状态返回;空关键字抛 ValueError;无命中返回空。 + +--- + ## 数据存储 任务数据保存在 `tasks.json` 文件中,每个任务包含以下字段: -- id: 任务编号 -- title: 任务标题 -- status: 任务状态(待完成/已完成) -- created_at: 创建时间 -- deadline: 截止日期(可选) -- priority: 优先级(高/中/低/普通,默认:普通) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 任务编号(自增) | +| title | str | 任务标题 | +| status | str | 任务状态(待完成/已完成) | +| created_at | str | 创建时间(YYYY-MM-DD HH:MM:SS) | +| deadline | str/None | 截止日期(可选,格式 YYYY-MM-DD) | +| priority | str | 优先级(高/中/低/普通) | + +--- ## 运行环境 - Python 3.x +--- + +## 实验四:团队协作开发(Git) + +### 实验目的 +通过团队协作完成任务管理系统的功能扩展,掌握 Git 分支管理、Pull Request、代码评审、冲突解决等协作开发流程。 + +### 团队分工 + +| 成员 | 分支名 | 负责功能 | 提交次数 | +|------|--------|---------|---------| +| 成员1 | member1/priority-filter | 按优先级筛选任务 + 添加任务时设置优先级 | 3次 | +| 成员2 | member2/delete-task | 删除任务功能 | 3次 | +| **成员3(本仓库)** | **member3/status-filter** | **按状态筛选任务功能** | **3次** | + +### 成员3开发流程(本分支) + +#### 第1次提交:添加筛选函数 +- 文件:`campus_task/task_service.py` +- 提交信息:`feat: add get_tasks_by_status filter function` +- 内容:新增 `get_tasks_by_status(status)` 函数,支持按状态筛选并校验输入有效性 + +#### 第2次提交:添加单元测试 +- 文件:`tests/test_status_filter.py` +- 提交信息:`test: add unit tests for status filter feature` +- 测试用例: + - `test_get_tasks_by_status_pending` - 筛选待完成任务 + - `test_get_tasks_by_status_completed` - 筛选已完成任务 + - `test_invalid_status_raises_error` - 无效状态抛出异常 + +#### 第3次提交:添加 CLI 支持 +- 文件:`main.py`、`campus_task/__init__.py` +- 提交信息:`feat: add CLI and interactive mode for status filter` +- 内容: + - `main.py` 新增 `filter` 命令和交互模式选项5 + - `__init__.py` 导出 `get_tasks_by_status` 函数 + +### 协作开发流程 + +``` +克隆小组仓库 + ↓ +配置 git 用户信息 + ↓ +创建功能分支 (git checkout -b member3/status-filter) + ↓ +第1次提交:实现核心函数 + ↓ +第2次提交:添加单元测试 + ↓ +第3次提交:添加 CLI 支持 + ↓ +推送到远程仓库 (git push origin member3/status-filter) + ↓ +创建 Pull Request (PR) + ↓ +代码评审 (Code Review) + ↓ +解决合并冲突 (如有) + ↓ +合并到主分支 (main/master) +``` + +### 代码评审要点(成员3作为评审人) +1. **功能正确性**:测试用例是否覆盖正常/异常场景 +2. **代码规范**:函数命名、注释、格式是否规范 +3. **模块边界**:是否错误删除了其他成员的功能导入 +4. **重复定义**:是否存在函数重复定义问题 +5. **向后兼容**:修改是否破坏原有功能 + +### 冲突解决经验 +当多成员同时修改同一文件(如 `task_service.py`、`main.py`、`__init__.py`)时会产生冲突,解决原则: +1. **保留所有功能**:不删除其他成员已实现的函数 +2. **修复重复定义**:同一函数只保留一份正确实现 +3. **整合导入语句**:`__init__.py` 中导出所有成员的函数 +4. **合并菜单项**:`main.py` 中同时存在多个功能选项 + +--- + +## 实验五:迭代开发与变更管理(v1.1.0 · 成员3) + +### 实验目的 +掌握软件迭代开发完整流程:从用户反馈采集变更请求 → 基于优先级与责任边界取舍 → TDD 实现 → 同步 CHANGELOG 与文档 → 回访用户形成闭环。本次版本号 **v1.0.0 → v1.1.0**(MINOR 递增,100% 向后兼容)。 + +### 一、变更请求采集(3 条 · 访谈 3 位真实用户) + +| 编号 | 用户 | 一句话需求 | 优先级 | 对应功能 | +|------|------|----------|--------|---------| +| CR-001 | 小李(室友) | "每天早上只想看**今天已经过期但还没做**的作业,其它先别给我看" | P0 紧急 | 显示逾期任务(overdue) | +| CR-002 | 小王(本班课代表) | "期末一下 10 门课任务堆里面,能不能**按标题搜关键字**?" | P1 重要 | 搜索任务关键字(search) | +| CR-003 | 小张(隔壁寝室) | "想把 CampusTask 任务**导出成 CSV/Excel** 发给组长做分工" | P2 一般 | 导出 CSV(暂缓 v1.2.0) | + +> 每条 CR 的 7 字段完整版(用户原话 / 问题描述 / 建议功能 / 优先级 / 验收标准 / 风险 / 预计工作量)见 `Experiment5/变更请求表.md`。 + +### 二、变更选择:为何做 CR-001 + CR-002,暂缓 CR-003 + +**5 维加权评分(满分 100)**: + +| 决策准则(权重) | CR-001 逾期 | CR-002 搜索 | CR-003 CSV | +|----------------|:----------:|:----------:|:----------:| +| 用户优先级 P0>P1>P2(25%) | 100 | 80 | 40 | +| 与成员3"筛选查询"专长连续性(20%) | 100 | 100 | 30 | +| 可测性(≥5 条自动化用例)(20%) | 100 | 100 | 70 | +| 风险可控(不越成员1 priority schema)(15%) | 100 | 100 | 50 | +| 单条工时 ≤ 2.5 h(20%) | 100 | 100 | 100 | +| **加权总分** | **97** | **94** | **63** | + +**取舍结论**: +- ✅ **做 CR-001(逾期)**:P0 高频日常场景,只读 `deadline + status` 不碰他人字段;完美承接实验4筛选专长。 +- ✅ **做 CR-002(搜索)**:P1 规模阈值刚需,和 CR-001 互补形成「查询三角」(按状态 / 按紧急 / 按内容)。 +- ❌ **本轮不做 CR-003(CSV)**:导出列含 priority 字段(成员1定义),成员3擅自输出会造成"和 UI 值域不一致"的隐形 Bug;推迟到 v1.2.0 三人对齐 CSV schema 后再做。 + +> 完整决策过程、Backlog 与迭代排期见 `Experiment5/变更选择理由.md`。 + +### 三、TDD 实现过程(成员3) + +#### Red 阶段 — 先写测试让它失败 +1. 新增 `tests/test_exp5_overdue_and_search.py` +2. 编写 2 个测试类 × 12 条用例(逾期 6 条 + 搜索 6 条) +3. 此时 `task_service.py` 还没实现 2 个新函数 → 运行测试报 `ImportError` / `AttributeError` **✅ 变红** + +#### Green 阶段 — 最小实现让所有用例通过 +在 `campus_task/task_service.py` 新增: +- **`get_overdue_tasks()`**:对每条任务判断 3 个条件(deadline 非空且能解析成 date;deadline.date() < today;status == "待完成")。日期解析失败的任务**保守跳过不报错**,避免用户手填非法日期崩溃。 +- **`search_tasks(keyword)`**:先 `kw = keyword.strip()`;若 `not kw` 抛 `ValueError("搜索关键字不能为空")`;再用 `kw.lower() in task.title.lower()` 做大小写不敏感子串匹配。所有状态都返回(不区分待完成/已完成)。 + +更新 `campus_task/__init__.py`:`__all__` 按字母序重排,新增导出 `get_overdue_tasks`、`search_tasks`。 + +跑测试:`Ran 12 tests in 0.2xxs` — **全部 OK ✅ 变绿**。 + +#### CLI 接入与手动冒烟 +- `main.py` 命令行分支新增 `overdue` 与 `search`(后者用 `' '.join(argv[2:])` 支持含空格的关键字) +- 交互模式菜单 1..5 → 1..7:新增 **5. 查看逾期任务**、**6. 搜索任务关键字**、**7. 退出** +- 列表显示升级:新增「优先级」列(`getattr(task, 'priority', '普通')` 兜底旧数据无该字段),分隔线 60 → 72 列 + +### 四、CHANGELOG(Keep a Changelog + SemVer v1.1.0) + +| 类型 | 条目 | +|------|------| +| Added | `get_overdue_tasks()` API + `python main.py overdue` CLI + 交互菜单 5 | +| Added | `search_tasks(kw)` API + `python main.py search <kw>` CLI + 交互菜单 6 | +| Added | `tests/test_exp5_overdue_and_search.py`(12 条用例全通过,每条用例独立临时 cwd) | +| Added | `Experiment5/` 交付文档目录(CR 表、选择理由、CHANGELOG、迭代总结、测试副本、Word 报告) | +| Changed | `task_service.py` 顶部新增 `from datetime import date` | +| Changed | `main.py` 列表新增「优先级」列;命令行 help 补 `overdue \| search`;菜单重编号 | +| Fixed | 附带修复:`print_tasks()` 访问旧任务无 `priority` 字段时用 `getattr` 兜底「普通」,防止 AttributeError | + +**SemVer 语义**:v1.0.0 → v1.1.0(MINOR 递增,新增兼容功能 API,无 Breaking Change)。 + +### 五、用户回访与满意度 + +| 用户 | 反馈 | 回应 | +|------|------|------| +| 小李(CR-001) | "过了零点昨天没做的任务自动出现在 overdue 里,真准!希望明天要交的也列出来" | ✅ 功能命中;💡 Backlog 加 `due_soon(days=N)` | +| 小王(CR-002) | "search 实验 能同时看到已做和没做的,方便。大小写不敏感也 OK" | ✅ 验收通过 | +| 小张(CR-003) | "CSV 这次没上?理解,任务少还能忍,下次要记得加" | ✅ Backlog 登记 v1.2.0 | + +### 六、交付物清单(Experiment5/ 目录下) + +| # | 文件 | 内容 | +|---|------|------| +| 1 | `变更请求表.md` | 3 条 CR × 7 字段完整描述 | +| 2 | `变更选择理由.md` | 5 维加权评分 + 取舍逻辑 + 排期表 + Backlog | +| 3 | `CHANGELOG.md` | v1.1.0 版本日志(Keep a Changelog 1.1.0 格式) | +| 4 | `迭代总结.md` | 目标完成度 / 用户回访 / 4 条经验 / 6 项改进方向 | +| 5 | `tests/test_exp5_overdue_and_search.py` | 12 条测试文件副本 | +| 6 | `实验五实验报告.docx` | 最终 Word 版实验报告(含过程与反思) | + +--- + +## 测试与验收 + +### 运行所有测试 +```bash +python -m unittest discover tests/ -v +``` + +或单独运行状态筛选测试: +```bash +python -m unittest tests.test_status_filter -v +``` + +### 状态筛选测试结果(成员3) + +``` +test_get_tasks_by_status_completed ... ok +test_get_tasks_by_status_pending ... ok +test_invalid_status_raises_error ... ok + +---------------------------------------------------------------------- +Ran 3 tests in 0.366s + +OK +``` + +### 完整测试用例列表 + +| 编号 | 测试用例名称 | 测试内容 | +|------|-------------|---------| +| 1 | `test_add_task_success` | 添加任务成功,验证标题、状态、编号、创建时间 | +| 2 | `test_add_task_with_deadline` | 添加带截止日期的任务 | +| 3 | `test_add_empty_title_fails` | 添加空标题任务失败,抛出 ValueError | +| 4 | `test_view_empty_task_list` | 查看空任务列表 | +| 5 | `test_view_non_empty_task_list` | 查看非空任务列表 | +| 6 | `test_complete_existing_task` | 完成存在的任务,状态变为"已完成" | +| 7 | `test_complete_non_existing_task` | 完成不存在的任务,返回 False | +| 8 | `test_task_id_increments` | 任务编号递增 | +| 9 | `test_json_file_not_exists_creates_empty_list` | JSON 文件不存在时自动创建空列表 | +| 10 | `test_json_file_corrupted_returns_empty_list` | JSON 文件损坏时返回空列表 | +| 11 | `test_completed_task_not_repeatedly_completed` | 已完成任务不重复完成 | +| 12 | `test_task_persists_after_save_and_reload` | 任务保存后重新读取仍然存在 | +| 13 | `test_get_tasks_by_status_pending` | **(实验4)** 筛选待完成任务数量正确 | +| 14 | `test_get_tasks_by_status_completed` | **(实验4)** 筛选已完成任务标题正确 | +| 15 | `test_invalid_status_raises_error` | **(实验4)** 无效状态输入抛出 ValueError | +| 16 | `test_no_deadline_not_overdue` | **(实验5)** 无截止日期的任务不出现在逾期列表 | +| 17 | `test_future_deadline_not_overdue` | **(实验5)** 未来截止日期的任务不逾期 | +| 18 | `test_past_deadline_pending_is_overdue` | **(实验5)** 过去截止 + 待完成 = 逾期,标题正确 | +| 19 | `test_past_deadline_done_is_NOT_overdue` | **(实验5)** 过去截止 + 已完成 ≠ 逾期 | +| 20 | `test_empty_task_list_overdue_count_zero` | **(实验5)** 空任务列表不报错,逾期返回 [] | +| 21 | `test_mixed_list_only_returns_pending_past` | **(实验5)** 混合任务只返回「待完成+过去」的任务 | +| 22 | `test_empty_keyword_raises_error` | **(实验5)** 空/全空格搜索关键字抛 ValueError | +| 23 | `test_search_no_match_returns_empty` | **(实验5)** 关键字完全不匹配返回空列表 | +| 24 | `test_search_partial_match_case_insensitive` | **(实验5)** 中文子串包含 + 英文大小写不敏感 | +| 25 | `test_search_returns_regardless_of_status` | **(实验5)** 搜索结果同时包含待完成和已完成 | +| 26 | `test_search_keyword_stripped` | **(实验5)** 关键字首尾空格 strip、中间空格保留 | +| 27 | `test_empty_task_list_search_safe` | **(实验5)** 空任务列表搜索不报错返回 [] | + +### 需求-测试用例对应表 + +| 用户故事 | 对应测试用例 | +|---------|-------------| +| 添加任务(标题) | test_add_task_success, test_add_empty_title_fails | +| 添加任务(截止日期) | test_add_task_with_deadline | +| 查看任务列表 | test_view_empty_task_list, test_view_non_empty_task_list | +| 标记任务完成 | test_complete_existing_task, test_complete_non_existing_task, test_completed_task_not_repeatedly_completed | +| 按状态筛选任务(实验4) | test_get_tasks_by_status_pending, test_get_tasks_by_status_completed, test_invalid_status_raises_error | +| 查看逾期任务(实验5) | test_no_deadline_not_overdue, test_future_deadline_not_overdue, test_past_deadline_pending_is_overdue, test_past_deadline_done_is_NOT_overdue, test_empty_task_list_overdue_count_zero, test_mixed_list_only_returns_pending_past | +| 搜索任务关键字(实验5) | test_empty_keyword_raises_error, test_search_no_match_returns_empty, test_search_partial_match_case_insensitive, test_search_returns_regardless_of_status, test_search_keyword_stripped, test_empty_task_list_search_safe | +| 任务编号递增 | test_task_id_increments | +| 数据持久化 | test_task_persists_after_save_and_reload | +| 边界情况 | test_json_file_not_exists_creates_empty_list, test_json_file_corrupted_returns_empty_list | + +### Bug 修复过程记录 + +**步骤 1:故意制造 Bug** +- 修改 `mark_task_done` 函数,将 `task.status = "已完成"` 改为 `tasks.remove(task)` +- 意图:将"完成任务"错误地实现为"删除任务" + +**步骤 2:运行测试观察失败** +- `test_complete_existing_task` 失败:任务被删除后 `get_task_by_id` 返回 None +- `test_completed_task_not_repeatedly_completed` 失败:同上 + +**步骤 3:修复 Bug** +- 将 `tasks.remove(task)` 改回 `task.status = "已完成"` + +**步骤 4:验证测试通过** +- 所有测试用例全部通过 + +--- + ## 课后反思问题 +### 实验一反思 +为什么"会写 Python"还不等于"会做软件"? + +答:编写单个 Python 代码片段与开发完整软件系统存在本质区别。软件不仅需要实现功能,还需要考虑需求分析、架构设计、错误处理、数据持久化、用户体验等多个方面。会写 Python 只是掌握了编程语言本身,而会做软件需要理解软件工程的完整流程,包括需求转化、模块化设计、测试验证、文档编写等能力。 + +### 实验二反思 为什么软件设计常常不是"多此一举",而是为了未来少改错? 答:软件设计将复杂系统分解为职责单一的模块,降低了代码的耦合度,提高了可维护性。当需求变更时,只需修改特定模块而不影响其他部分,减少了引入错误的风险。良好的设计还提升了代码的可读性和可测试性,使得团队协作更加高效。 -## 实验4文档 +### 实验三反思 +测试是在"找老师要分",还是在给未来的自己提供安全网? + +答:测试的真正价值在于为未来的自己和团队提供安全网。通过自动化测试,可以快速验证代码修改是否破坏了现有功能,防止回归错误。测试用例也是文档的一种形式,清晰地说明了代码应该如何工作。当需求变更或代码重构时,测试用例确保系统行为的一致性,减少了维护成本。 + +### 实验四反思 +为什么在团队协作中,"先自己分支写好测过"再提 PR,比直接改主分支更靠谱? + +答:分支开发是团队协作的最佳实践,原因如下: +1. **隔离风险**:功能开发不影响主分支的稳定性,即使代码有问题也不会阻碍其他成员 +2. **代码评审**:PR 机制强制代码经过他人审查,提前发现 Bug 和设计问题 +3. **可追溯性**:每个功能对应一个分支和 PR,历史记录清晰,便于回溯和版本回退 +4. **并行开发**:多成员可同时在不同分支开发不同功能,互不干扰 +5. **渐进式合并**:功能完整且测试通过后才合并,保证主分支始终可运行 + +### 实验五反思 +为什么"用户说的"和"系统该做的"不是一回事?请结合本次实验中「逾期任务」判定里"今天到期"算不算逾期这个细节回答。 + +答:「用户说的」是场景化、情绪化的自然语言(带上下文和潜台词);「系统该做的」是经过「需求翻译」之后可执行、可测试的精确验收标准。两者之间差了一个关键的**去模糊化**步骤——这正是软件工程师的核心价值,而不是"把用户的话直接写成 if-else"。 + +本次实验小李的原话是「每天早上只想看今天已经过期但还没做的作业」。如果直接死译为 `deadline <= today`,会把「今天到期」和「半年前已过期」的任务混在一起列出来,小李看到会更焦虑(因为真正紧急的和不那么紧急的没有层次感)。 + +正确的「翻译」过程拆解了三层: +1. **「还没做的」→ `status == "待完成"`**:已完成的任务即使过了截止日期也不显示,排除噪音; +2. **「已经过期」→ `deadline.date() 严格 < today`(不是 ≤)**:给"今天到期"的任务留足一整天的缓冲时间,避免一早就给用户造成不必要的压力; +3. **「都没有的别吓我」→ 空结果展示 🎉 emoji** 而不是冷冰冰的空列表。 -- [实验4报告](实验4报告.md) - 实验总结与课后反思 -- [代码评审记录](代码评审记录.md) - Pull Request 评审详情 -- [冲突解决说明](冲突解决说明.md) - 合并冲突解决过程 \ No newline at end of file +如果跳过这一步"翻译"会导致两个具体坏处:① 今天到期的任务被过早地当成逾期,用户一早产生不必要的压力;② 已完成的任务也出现在列表里干扰视线。所以做需求 ≠ 逐字执行用户的话,而是把模糊的「用户原话」拆解为可验证的验收标准。这正是迭代开发(实验5)相对于瀑布式开发最大的进步。 diff --git a/campus_task/__init__.py b/campus_task/__init__.py index ea509a8..c62d2ba 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,57 @@ from .task_model import Task -from .task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_status, delete_task, get_tasks_by_priority, sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv +from .task_validator import ( + validate_title, + validate_priority, + validate_status, + validate_deadline, + validate_keyword, + VALID_PRIORITIES, + VALID_STATUSES, +) +from .task_printer import print_tasks, format_task_lines +from .task_service import ( + add_task, + get_all_tasks, + mark_task_done, + get_task_by_id, + get_tasks_by_status, + # -------- 完整功能集补齐:priority 筛选 + 删除 -------- + get_tasks_by_priority, + delete_task, + # -------- 实验5 新增 -------- + get_overdue_tasks, + search_tasks, + # -------- 实验6 新增 -------- + export_tasks_csv, + edit_task, +) -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_status", "delete_task", "get_tasks_by_priority", "sort_tasks_by_deadline", "sort_tasks_by_priority", "search_tasks", "export_tasks_to_csv"] \ No newline at end of file +__all__ = [ + "Task", + # ---- 实验七:validator 模块(SSoT 值域 + 输入校验) ---- + "validate_title", + "validate_priority", + "validate_status", + "validate_deadline", + "validate_keyword", + "VALID_PRIORITIES", + "VALID_STATUSES", + # ---- 实验七:printer 模块(CLI 输出格式化统一) ---- + "print_tasks", + "format_task_lines", + # ---- 业务函数 ---- + "add_task", + "get_all_tasks", + "mark_task_done", + "get_task_by_id", + "get_tasks_by_status", + # -------- 完整功能集补齐:priority 筛选 + 删除 -------- + "get_tasks_by_priority", + "delete_task", + # -------- 实验5 新增 -------- + "get_overdue_tasks", + "search_tasks", + # -------- 实验6 新增 -------- + "export_tasks_csv", + "edit_task", +] diff --git a/campus_task/task_printer.py b/campus_task/task_printer.py new file mode 100644 index 0000000..87adec9 --- /dev/null +++ b/campus_task/task_printer.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" +实验七 - 重构 3(SRP 单一职责) +任务 CLI 输出格式化模块。 +此前 print_tasks 散落在 main.py 内,把「业务调用」和「终端格式化」混为一体, +未来要接入 TUI / Web GUI / 导出 Markdown 等其他输出渠道时, +必须去改 main.py 的内部函数,违反单一职责。 +抽成 task_printer 后: + - main.py 只负责 UI 事件分派,不关心每一行怎么对齐。 + - 任务格式化的统一修改(比如新增"负责人"列、调列宽)只改此文件一处。 + - 未来 GUI 可直接复用 format_task_lines 的字符串列表,不必重写对齐。 +""" +from typing import List, Optional +from .task_model import Task +from .task_service import get_all_tasks + +# ---------------- 表格布局常量(统一 SSoT,改此处即全渠道生效) ---------------- +_TOTAL_WIDTH = 72 +_COLUMNS = [ + ("编号", 4), + ("状态", 6), + ("优先级", 6), + ("标题", 24), + ("创建时间", 19), + ("截止日期", None), # None 表示最后一列,不限制宽度(占剩余空间) +] + + +def format_task_lines(tasks: Optional[List[Task]] = None) -> List[str]: + """ + 将任务列表格式化为 CLI 表格行(不直接 print,返回字符串列表)。 + 解耦「格式化逻辑」和「输出渠道」:CLI 直接逐行 print;GUI 渲染成列表; + UT 对返回列表做字符串断言即可,不依赖 stdout 捕获。 + """ + if tasks is None: + tasks = get_all_tasks() + if not tasks: + return ["暂无任务"] + + lines: List[str] = [] + sep_eq = "=" * _TOTAL_WIDTH + sep_dash = "-" * _TOTAL_WIDTH + header_line = " ".join( + f"{name:<{width}}" if width is not None else name + for name, width in _COLUMNS + ) + lines.append(sep_eq) + lines.append(header_line) + lines.append(sep_dash) + + for task in tasks: + deadline = task.deadline if task.deadline else "" + priority = getattr(task, "priority", "普通") or "普通" + # 逐列对齐 + cols = [ + f"{task.id:<4}", + f"{task.status:<6}", + f"{priority:<6}", + f"{task.title:<24}", + f"{task.created_at:<19}", + deadline, + ] + lines.append(" ".join(cols)) + lines.append(sep_eq) + return lines + + +def print_tasks(tasks: Optional[List[Task]] = None) -> None: + """ + 与旧 main.print_tasks 等价的 facade:直接打印到 stdout(保持向后兼容)。 + 底层复用 format_task_lines,保证格式化逻辑只有一份。 + """ + for line in format_task_lines(tasks): + print(line) diff --git a/campus_task/task_service.py b/campus_task/task_service.py index dce9d5d..7ad47a9 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -1,22 +1,37 @@ from typing import List, Optional -from datetime import datetime +from datetime import datetime, date +import csv +import os from .task_model import Task from .task_storage import load_tasks, save_tasks +from .task_validator import ( + validate_title, + validate_priority, + validate_status, + validate_deadline, + validate_keyword, + VALID_PRIORITIES, # 仍对外导出,供上层代码做 UI 下拉菜单 + VALID_STATUSES, # 同上 +) -def add_task(title: str, deadline: Optional[str] = None, priority: str = "普通") -> Task: - valid_priorities = ["高", "中", "低", "普通"] - if priority not in valid_priorities: - raise ValueError("优先级必须是: 高、中、低、普通") +# ---------------- 向后兼容别名(老代码可能直接 import _VALID_PRIORITIES) ---------------- +_VALID_PRIORITIES = set(VALID_PRIORITIES) +_VALID_STATUSES = set(VALID_STATUSES) +# CSV 表头仍属于 IO 格式化范畴,留在 task_service(不与 validator 耦合) +_CSV_HEADER = ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"] + + +def add_task(title: str, deadline: Optional[str] = None, priority: Optional[str] = None) -> Task: tasks = load_tasks() task_id = len(tasks) + 1 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") task = Task( task_id=task_id, - title=title, + title=validate_title(title), status="待完成", created_at=now, - deadline=deadline, - priority=priority + deadline=validate_deadline(deadline), + priority=validate_priority(priority), ) tasks.append(task) save_tasks(tasks) @@ -41,55 +56,149 @@ def get_task_by_id(task_id: int) -> Optional[Task]: return task return None -def delete_task(task_id: int) -> bool: - tasks = load_tasks() - for i, task in enumerate(tasks): - if task.id == task_id: - del tasks[i] - save_tasks(tasks) - return True - return False - def get_tasks_by_status(status: str) -> List[Task]: - valid_statuses = ["待完成", "已完成"] - if status not in valid_statuses: - raise ValueError("状态必须是: 待完成、已完成") + # 统一走 validate_status 校验,错误消息与 edit_task 等其他入口保持一致 + validate_status(status) tasks = load_tasks() return [task for task in tasks if task.status == status] -def get_tasks_by_priority(priority: str) -> List[Task]: - valid_priorities = ["高", "中", "低", "普通"] - if priority not in valid_priorities: - raise ValueError("优先级必须是: 高、中、低、普通") - tasks = load_tasks() - return [task for task in tasks if task.priority == priority] -def sort_tasks_by_deadline() -> List[Task]: - tasks = load_tasks() - return sorted(tasks, key=lambda t: t.deadline if t.deadline else "9999-12-31") +# -------- 实验5:新增功能A:显示逾期任务 -------- +def get_overdue_tasks() -> List[Task]: + """ + 返回当前所有“逾期”的任务: + 1. 有截止日期(deadline 非空) + 2. 截止日期 < 今天(已经过了日期) + 3. 状态还是“待完成”(已完成的不算逾期) + """ + today = date.today() + result: List[Task] = [] + for task in load_tasks(): + # 无截止日期 -> 不算 + if not task.deadline: + continue + # 已完成 -> 不算 + if task.status != "待完成": + continue + # 解析 deadline(支持 YYYY-MM-DD;解析失败就保守当不逾期) + try: + dl = datetime.strptime(task.deadline[:10], "%Y-%m-%d").date() + except (ValueError, TypeError): + continue + if dl < today: + result.append(task) + return result -def sort_tasks_by_priority() -> List[Task]: - priority_order = {"高": 0, "中": 1, "普通": 2, "低": 3} - tasks = load_tasks() - return sorted(tasks, key=lambda t: priority_order.get(t.priority, 3)) +# -------- 实验5:新增功能B:搜索任务关键字 -------- def search_tasks(keyword: str) -> List[Task]: + """ + 按标题包含(子串匹配)搜索任务。 + - 空/全空格关键字 -> 抛 ValueError(由 validate_keyword 统一处理) + - 关键字首尾 strip 后匹配 + - 英文大小写不敏感;中文直接包含比较 + - 搜索不区分状态(待完成和已完成都会被返回) + """ + kw = validate_keyword(keyword) + kw_lower = kw.lower() + result: List[Task] = [] + for task in load_tasks(): + if not task.title: + continue + # 统一小写后比较,兼顾中英文 + if kw_lower in task.title.lower(): + result.append(task) + return result + + +# ---------------- 为完整功能集补齐:delete + priority 筛选(对齐老测试用例) ---------------- +def delete_task(task_id: int) -> bool: + """ + 删除指定 id 的任务(成员2 delete 功能的最终实现,服务于老测试与完整功能集) + :return: True 表示删到了;False 表示任务不存在 + """ tasks = load_tasks() - keyword = keyword.lower() - return [task for task in tasks if keyword in task.title.lower()] + found = next((i for i, t in enumerate(tasks) if t.id == task_id), None) + if found is None: + return False + del tasks[found] + save_tasks(tasks) + return True + + +def get_tasks_by_priority(priority: str) -> List[Task]: + """按优先级筛选任务(与 get_tasks_by_status 完全对称)。 + 注意:此处不允许传 None(None 会被 validate_priority 兜底成"普通", + 语义上不等价于「查普通」,因此显式拒绝 None 入参)。 + """ + if priority is None: + raise ValueError( + f"priority 非法: None,合法值域为 {list(VALID_PRIORITIES)}" + ) + p = validate_priority(priority) + return [t for t in load_tasks() if t.priority == p] + + +# ---------------- 实验6:新增功能A:导出 CSV(CR-003) ---------------- +def export_tasks_csv(filename: Optional[str] = None) -> str: + """ + 将当前任务列表导出为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码) + :param filename: 可选,自定义文件名(含 .csv 后缀);默认 "tasks.csv" + :return: 导出文件的绝对路径,便于上层调用方显示给用户 + """ + target = filename if filename else "tasks.csv" + abs_path = os.path.abspath(target) + + with open(abs_path, "w", encoding="utf-8-sig", newline="") as f: + writer = csv.writer(f) + writer.writerow(_CSV_HEADER) + for t in load_tasks(): + writer.writerow([ + t.id, + t.title, + t.status, + t.priority, + t.created_at if t.created_at else "", + t.deadline if t.deadline else "", + ]) + return abs_path -def export_tasks_to_csv(file_path: str) -> bool: - import csv + +# ---------------- 实验6:新增功能B:编辑任务(局部更新) ---------------- +def edit_task( + task_id: int, + title: Optional[str] = None, + deadline: Optional[str] = None, + priority: Optional[str] = None, + status: Optional[str] = None, +) -> Task: + """ + 编辑指定任务的部分字段(局部更新,非全字段覆盖) + - 不传的参数保持原值不变 + - title / deadline / priority / status 若传了则走 task_validator 统一校验 + - id 不存在:抛 ValueError,附带 id 便于排错 + :return: 修改并持久化后的 Task 对象 + """ tasks = load_tasks() - try: - with open(file_path, 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]) - for task in tasks: - writer.writerow([ - task.id, task.title, task.status, - task.priority, task.created_at, task.deadline or '' - ]) - return True - except IOError: - return False \ No newline at end of file + target = next((t for t in tasks if t.id == task_id), None) + if target is None: + raise ValueError(f"任务不存在: id={task_id}") + + # 标题:统一走 validate_title(strip + 非空校验) + if title is not None: + target.title = validate_title(title) + + # deadline:统一走 validate_deadline(空字符串视为清除、格式校验) + if deadline is not None: + target.deadline = validate_deadline(deadline) + + # priority:统一走 validate_priority(None→普通/非法抛错) + if priority is not None: + target.priority = validate_priority(priority) + + # status:统一走 validate_status(必须属于合法集合) + if status is not None: + target.status = validate_status(status) + + save_tasks(tasks) + return target diff --git a/campus_task/task_validator.py b/campus_task/task_validator.py new file mode 100644 index 0000000..d8d8f5c --- /dev/null +++ b/campus_task/task_validator.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +实验七 - 重构 2(SRP 单一职责) +任务输入校验与值域归一化模块。 +本模块只做「数据合法性校验 + 值域兜底/归一化」,不做 IO / 状态变更。 +此前所有校验散落在 add_task / edit_task / get_tasks_by_status 三处, +一旦 priority / status 合法值扩展,至少 3 处要同步修改,易漏改; +抽成独立 validator 后,所有「业务规则」均 SSoT。 +""" +from typing import Optional +import re + +# -------------------------- +# 公开 SSoT 常量(业务规则唯一真源) +# -------------------------- +# 与成员1共同对齐后的 priority 合法值域(全项目统一) +VALID_PRIORITIES = ("低", "普通", "中", "高", "紧急") +# 与 get_tasks_by_status / mark_task_done 对齐的 status 合法值域 +VALID_STATUSES = ("待完成", "已完成") +# 截止日期格式(YYYY-MM-DD),正则做初步校验,不接受其他格式 +_DEADLINE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") + + +# -------------------------- +# 基础校验函数:失败统一抛 ValueError,便于上层 try/catch +# -------------------------- +def validate_title(title: str) -> str: + """ + 校验并归一化 title:空字符串/全空白 -> ValueError; + 返回 strip 后的非空字符串。 + """ + if title is None: + raise ValueError("任务标题不能为空") + cleaned = title.strip() + if cleaned == "": + raise ValueError("任务标题不能为空") + return cleaned + + +def validate_priority(priority: Optional[str]) -> str: + """ + 校验并兜底 priority:None -> 「普通」;非法值抛 ValueError; + 返回值一定在 VALID_PRIORITIES 中。 + """ + if priority is None: + return "普通" + if priority not in VALID_PRIORITIES: + raise ValueError( + f"priority 非法: {priority!r},合法值域为 {list(VALID_PRIORITIES)}" + ) + return priority + + +def validate_status(status: str) -> str: + """ + 校验 status:必须属于 VALID_STATUSES,否则 ValueError; + 返回原值(不做兜底,未传调用方自行决定传什么)。 + """ + if status not in VALID_STATUSES: + raise ValueError( + f"status 非法: {status!r},合法值域为 {list(VALID_STATUSES)}" + ) + return status + + +def validate_deadline(deadline: Optional[str]) -> Optional[str]: + """ + 校验 deadline 格式:None/空字符串 -> 返回 None 清除; + 非空但不匹配 YYYY-MM-DD -> ValueError; + 合法则原样返回。 + 注:本函数不校验日期真实存在(如 2026-02-30),留给业务层按需加强。 + """ + if deadline is None or deadline == "": + return None + if not _DEADLINE_RE.match(deadline): + raise ValueError( + f"deadline 格式非法: {deadline!r},要求 YYYY-MM-DD(如 2026-07-01)" + ) + return deadline + + +def validate_keyword(keyword: Optional[str]) -> str: + """搜索关键字校验:空/None/全空白 -> ValueError;strip 后返回""" + if keyword is None: + raise ValueError("搜索关键字不能为空") + cleaned = keyword.strip() + if cleaned == "": + raise ValueError("搜索关键字不能为空") + return cleaned diff --git a/main.py b/main.py index 23ddaf9..6e28bc7 100644 --- a/main.py +++ b/main.py @@ -1,189 +1,184 @@ +# -*- coding: utf-8 -*- +"""CampusTask 命令行入口(实验七重构:OCP COMMANDS 分派 + SRP printer/validator)""" import sys -import logging -from campus_task import add_task, get_all_tasks, mark_task_done, get_tasks_by_status, delete_task, get_tasks_by_priority, sort_tasks_by_deadline, sort_tasks_by_priority, search_tasks, export_tasks_to_csv +from campus_task import ( + add_task, + get_all_tasks, + mark_task_done, + get_tasks_by_status, + get_overdue_tasks, # 实验5 新增 + search_tasks, # 实验5 新增 + export_tasks_csv, # 实验6 新增 + edit_task, # 实验6 新增 + print_tasks, # 实验7 重构:task_printer 模块统一格式化输出 +) -VERSION = "0.1.0" -logging.basicConfig( - filename='campus_task.log', - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - encoding='utf-8' -) +# Exit Code 约定(与 argparse 一致,实验八 CLI E2E 测试依赖): +# EXIT_OK = 0 业务成功,或未找到任务等「不属输入错误」的无结果状态 +# EXIT_USAGE = 2 参数错误 / 用法错误 / 校验失败 / 未知命令 +EXIT_OK = 0 +EXIT_USAGE = 2 -def print_tasks(tasks=None): - if tasks is None: - tasks = get_all_tasks() - if not tasks: - print("暂无任务") - return - print("=" * 70) - print(f"{'编号':<4} {'状态':<6} {'优先级':<4} {'标题':<30} {'创建时间':<19} {'截止日期'}") - print("-" * 70) - for task in tasks: - deadline = task.deadline if task.deadline else '' - print(f"{task.id:<4} {task.status:<6} {task.priority:<4} {task.title:<30} {task.created_at:<19} {deadline}") - print("=" * 70) def handle_add(args): if len(args) < 1: - print("用法: python main.py add <title> [deadline] [priority]") - print("priority 可选值: 高、中、低、普通(默认:普通)") - return + print("用法: python main.py add <title> [deadline]") + return EXIT_USAGE title = args[0] deadline = args[1] if len(args) > 1 else None - priority = args[2] if len(args) > 2 else "普通" try: - task = add_task(title, deadline, priority) - print(f"任务已添加: {task.title}") - logging.info(f"添加任务: id={task.id}, title={task.title}, deadline={deadline}, priority={priority}") + task = add_task(title, deadline) except ValueError as e: + # deadline/title 校验错误(实验七统一由 validator 抛出) print(e) - logging.error(f"添加任务失败: {e}") + return EXIT_USAGE + print(f"任务已添加: [{task.id}] {task.title}") + return EXIT_OK + def handle_list(): print_tasks() + return EXIT_OK + def handle_done(args): if len(args) < 1: print("用法: python main.py done <id>") - return + return EXIT_USAGE try: task_id = int(args[0]) success = mark_task_done(task_id) if success: print(f"任务 {task_id} 已标记为完成") - logging.info(f"标记任务完成: id={task_id}") else: print(f"未找到任务 {task_id}") - logging.warning(f"标记任务完成失败: 未找到任务 id={task_id}") except ValueError: print("任务编号必须是数字") - logging.error(f"标记任务完成失败: 任务编号必须是数字") + return EXIT_USAGE + return EXIT_OK -def handle_delete(args): - if len(args) < 1: - print("用法: python main.py delete <id>") - return - try: - task_id = int(args[0]) - success = delete_task(task_id) - if success: - print(f"任务 {task_id} 已删除") - logging.info(f"删除任务: id={task_id}") - else: - print(f"未找到任务 {task_id}") - logging.warning(f"删除任务失败: 未找到任务 id={task_id}") - except ValueError: - print("任务编号必须是数字") - logging.error(f"删除任务失败: 任务编号必须是数字") def handle_filter(args): if len(args) < 1: print("用法: python main.py filter <status>") print("status 可选值: 待完成, 已完成") - return + return EXIT_USAGE status = args[0] try: tasks = get_tasks_by_status(status) if not tasks: print(f"暂无{status}任务") - return - print(f"===== {status}任务 =====") + return EXIT_OK + print(f"===== {status}任务 共{len(tasks)}条 =====") print_tasks(tasks) except ValueError as e: print(e) + return EXIT_USAGE + return EXIT_OK -def handle_priority(args): - if len(args) < 1: - print("用法: python main.py priority <priority>") - print("priority 可选值: 高、中、低、普通") - return - priority = args[0] - try: - tasks = get_tasks_by_priority(priority) - if not tasks: - print(f"暂无{priority}优先级任务") - return - print(f"===== {priority}优先级任务 =====") - print_tasks(tasks) - except ValueError as e: - print(e) -def handle_sort(args): - if len(args) < 1: - print("用法: python main.py sort <field>") - print("field 可选值: deadline, priority") - return - field = args[0] - if field == "deadline": - tasks = sort_tasks_by_deadline() - print("===== 按截止日期排序 =====") - print_tasks(tasks) - elif field == "priority": - tasks = sort_tasks_by_priority() - print("===== 按优先级排序 =====") - print_tasks(tasks) - else: - print("无效字段,可选值: deadline, priority") +# ----------- 实验5:新增 handle_overdue ----------- +def handle_overdue(_args=None): + """显示所有逾期任务(deadline已过 + 待完成)。 + 输出统一纯中文前缀,避免 Win GBK 终端下 emoji 导致 UnicodeEncodeError。 + """ + tasks = get_overdue_tasks() + if not tasks: + print("[提示] 当前没有逾期任务,干得漂亮!") + return EXIT_OK + print(f"[逾期提醒] 共发现 {len(tasks)} 条逾期任务:") + print_tasks(tasks) + return EXIT_OK + +# ----------- 实验5:新增 handle_search ----------- def handle_search(args): + """按关键字搜索任务标题""" if len(args) < 1: - print("用法: python main.py search <keyword>") - return - keyword = args[0] - tasks = search_tasks(keyword) + print("用法: python main.py search <关键字>") + print("示例: python main.py search 实验报告") + return EXIT_USAGE + keyword = " ".join(args) # 支持多空格参数(如 "软件 实验") + try: + tasks = search_tasks(keyword) + except ValueError as e: + print(e) + return EXIT_USAGE if not tasks: - print(f"未找到包含'{keyword}'的任务") - return - print(f"===== 搜索结果('{keyword}') =====") + print(f"没有匹配关键字[{keyword}]的任务") + return EXIT_OK + print(f"[搜索结果] 关键字[{keyword}]命中 {len(tasks)} 条任务:") print_tasks(tasks) + return EXIT_OK -def handle_export(args): - if len(args) < 1: - print("用法: python main.py export <file.csv>") - return - file_path = args[0] - success = export_tasks_to_csv(file_path) - if success: - print(f"任务已导出到 {file_path}") - logging.info(f"导出任务到CSV: {file_path}") - else: - print(f"导出失败,请检查文件路径") - logging.error(f"导出任务失败: {file_path}") -def handle_version(): - print(f"CampusTask v{VERSION}") +# ----------- 实验6:新增 handle_export_csv ----------- +def handle_export_csv(args): + """导出任务为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码)""" + filename = args[0] if len(args) >= 1 else None + try: + path = export_tasks_csv(filename) + except Exception as e: + print(f"导出失败: {e}") + return EXIT_USAGE + print(f"[导出完成] CSV 已导出: {path}") + return EXIT_OK -def handle_help(): - help_text = f"""CampusTask v{VERSION} - 校园任务清单管理工具 -用法: python -m campus_task <command> [args] +# ----------- 实验6:新增 handle_edit ----------- +def handle_edit(args): + """ + 编辑任务(单字段局部更新: + 用法: python main.py edit <id> <字段> <新值> + 字段可选: title | deadline | priority | status + """ + if len(args) < 3: + print("用法: python main.py edit <id> <字段> <新值>") + print(" 字段: title | deadline | priority | status") + print(" 示例: python main.py edit 1 title 新标题") + print(" 示例: python main.py edit 2 deadline 2026-07-15") + return EXIT_USAGE + try: + task_id = int(args[0]) + except ValueError: + print("任务编号必须是数字") + return EXIT_USAGE + field = args[1].strip().lower() + value = " ".join(args[2:]) # 字段新值(允许空格标题等) + allowed = {"title", "deadline", "priority", "status"} + if field not in allowed: + print(f"未知字段: {field},允许字段:title | deadline | priority | status") + return EXIT_USAGE + try: + kwargs = {field: value} + t = edit_task(task_id, **kwargs) + except ValueError as e: + print(e) + return EXIT_USAGE + print(f"[已更新] 任务[{task_id}]的 {field} -> {getattr(t, field, value)}") + return EXIT_OK -命令列表: - add <title> [deadline] [priority] 添加新任务 - list 列出所有任务 - done <id> 标记任务完成 - delete <id> 删除任务 - filter <status> 按状态筛选任务 - priority <priority> 按优先级筛选任务 - sort <field> 排序任务 (field: deadline, priority) - search <keyword> 搜索任务 - export <file.csv> 导出任务为CSV -选项: - --version 显示版本信息 - --help 显示帮助信息 +# ----------- 实验7 重构 3:OCP 命令分派字典(新增命令仅需向此 dict 追加条目,不再改 main 分派链) ----------- +# 键=命令字符串;值=(处理函数);处理函数签名统一为 handler(args: list[str]) -> None +# 扩展命令的好处: +# 1) OCP:对扩展开放(新命令不修改 main),对修改关闭(分派结构稳定) +# 2) 可用命令列表自动从 dict 生成,不会手滑漏改 +# 3) 很容易扩展帮助文档 / 自动补全 / Tab 补全脚本 +COMMANDS = { + "add": handle_add, + "list": handle_list, + "done": handle_done, + "filter": handle_filter, + # ---- 实验5 新增命令 ---- + "overdue": handle_overdue, + "search": handle_search, + # ---- 实验6 新增命令 ---- + "export_csv": handle_export_csv, + "edit": handle_edit, +} -示例: - python -m campus_task add "完成作业" 2026-07-01 高 - python -m campus_task list - python -m campus_task done 1 - python -m campus_task sort deadline - python -m campus_task search 软件工程 - python -m campus_task export tasks.csv -""" - print(help_text) def interactive_mode(): while True: @@ -191,16 +186,14 @@ def interactive_mode(): print("1. 添加任务") print("2. 查看任务列表") print("3. 标记任务完成") - print("4. 删除任务") - print("5. 按状态筛选任务") - print("6. 按优先级筛选任务") - print("7. 按截止日期排序") - print("8. 按优先级排序") - print("9. 搜索任务") - print("10. 导出任务为CSV") - print("11. 退出") - choice = input("请输入选择 (1-11): ") - + print("4. 按状态筛选任务") + print("5. 查看逾期任务 <- 实验5新增") + print("6. 搜索任务关键字 <- 实验5新增") + print("7. 导出任务为 CSV <- 实验6新增") + print("8. 编辑任务字段 <- 实验6新增") + print("9. 退出") + choice = input("请输入选择 (1-9): ").strip() + if choice == "1": title = input("请输入任务标题: ").strip() if not title: @@ -208,13 +201,12 @@ def interactive_mode(): continue deadline = input("请输入截止日期 (可选,格式: YYYY-MM-DD): ").strip() deadline = deadline if deadline else None - priority = input("请输入优先级 (可选,高/中/低/普通,默认: 普通): ").strip() - priority = priority if priority else "普通" try: - task = add_task(title, deadline, priority) - print(f"任务已添加: {task.title}") + task = add_task(title, deadline) except ValueError as e: print(e) + continue + print(f"任务已添加: [{task.id}] {task.title}") elif choice == "2": print_tasks() elif choice == "3": @@ -229,105 +221,70 @@ def interactive_mode(): except ValueError: print("任务编号必须是数字") elif choice == "4": - print_tasks() - try: - task_id = int(input("请输入要删除的任务编号: ")) - success = delete_task(task_id) - if success: - print(f"任务 {task_id} 已删除") - else: - print(f"未找到任务 {task_id}") - except ValueError: - print("任务编号必须是数字") - elif choice == "5": status = input("请输入筛选状态 (待完成/已完成): ").strip() try: tasks = get_tasks_by_status(status) if not tasks: print(f"暂无{status}任务") else: - print(f"===== {status}任务 =====") + print(f"===== {status}任务 共{len(tasks)}条 =====") print_tasks(tasks) except ValueError as e: print(e) + elif choice == "5": + # 实验5 新增 + handle_overdue() elif choice == "6": - priority = input("请输入筛选优先级 (高/中/低/普通): ").strip() - try: - tasks = get_tasks_by_priority(priority) - if not tasks: - print(f"暂无{priority}优先级任务") - else: - print(f"===== {priority}优先级任务 =====") - print_tasks(tasks) - except ValueError as e: - print(e) + # 实验5 新增 + kw = input("请输入搜索关键字: ").strip() + handle_search([kw]) elif choice == "7": - tasks = sort_tasks_by_deadline() - print("===== 按截止日期排序 =====") - print_tasks(tasks) + # 实验6 新增:导出 CSV + fname = input("请输入 CSV 文件名(留空= tasks.csv): ").strip() + handle_export_csv([fname] if fname else []) elif choice == "8": - tasks = sort_tasks_by_priority() - print("===== 按优先级排序 =====") - print_tasks(tasks) - elif choice == "9": - keyword = input("请输入搜索关键词: ").strip() - if not keyword: - print("关键词不能为空") - continue - tasks = search_tasks(keyword) - if not tasks: - print(f"未找到包含'{keyword}'的任务") - else: - print(f"===== 搜索结果('{keyword}') =====") - print_tasks(tasks) - elif choice == "10": - file_path = input("请输入导出文件路径 (如: tasks.csv): ").strip() - if not file_path: - print("文件路径不能为空") + # 实验6 新增:编辑任务 + print_tasks() + try: + task_id = int(input("请输入要编辑的任务编号: ").strip()) + except ValueError: + print("任务编号必须是数字") continue - success = export_tasks_to_csv(file_path) - if success: - print(f"任务已导出到 {file_path}") - else: - print(f"导出失败,请检查文件路径") - elif choice == "11": - print("退出程序") + field = input("要编辑的字段(title/deadline/priority/status): ").strip().lower() + value = input(f"请输入字段新值(priority=低/普通/中/高/紧急,status=待完成/已完成): ").strip() + handle_edit([str(task_id), field, value]) + elif choice == "9": + print("退出程序,再见~") break else: - print("无效选择,请输入 1-11") + print("无效选择,请输入 1-9") + def main(): if len(sys.argv) >= 2: command = sys.argv[1] args = sys.argv[2:] - - if command == "--version" or command == "-v": - handle_version() - elif command == "--help" or command == "-h": - handle_help() - elif command == "add": - handle_add(args) - elif command == "list": - handle_list() - elif command == "done": - handle_done(args) - elif command == "delete": - handle_delete(args) - elif command == "filter": - handle_filter(args) - elif command == "priority": - handle_priority(args) - elif command == "sort": - handle_sort(args) - elif command == "search": - handle_search(args) - elif command == "export": - handle_export(args) - else: + handler = COMMANDS.get(command) + if handler is None: + available = " | ".join(COMMANDS.keys()) print(f"未知命令: {command}") - logging.warning(f"未知命令: {command}") + print(f"可用命令: {available}") + sys.exit(EXIT_USAGE) + # 统一处理:零参命令(如 list)也传入空列表,保持接口一致 + if command == "list": + ret = handler() + elif command == "overdue": + # overdue 允许不传参(历史签名:handle_overdue(_args=None)) + ret = handler(args if args else None) + else: + ret = handler(args) + # handler 返回整数 exit code → 交给 sys.exit + if isinstance(ret, int) and ret != EXIT_OK: + sys.exit(ret) else: interactive_mode() + sys.exit(EXIT_OK) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/main2.py b/main2.py new file mode 100644 index 0000000..9d3968f --- /dev/null +++ b/main2.py @@ -0,0 +1,120 @@ +import sys +import json +import os +from datetime import datetime + +TASKS_FILE = "tasks.json" + +def load_tasks(): + if os.path.exists(TASKS_FILE): + with open(TASKS_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return [] + +def save_tasks(tasks): + with open(TASKS_FILE, 'w', encoding='utf-8') as f: + json.dump(tasks, f, ensure_ascii=False, indent=2) + +def add_task(title, deadline=None): + tasks = load_tasks() + task_id = len(tasks) + 1 + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + task = { + "id": task_id, + "title": title, + "status": "待完成", + "created_at": now, + "deadline": deadline + } + tasks.append(task) + save_tasks(tasks) + print(f"任务已添加: {title}") + +def list_tasks(): + tasks = load_tasks() + if not tasks: + print("暂无任务") + return + print("=" * 60) + print(f"{'编号':<4} {'状态':<6} {'标题':<30} {'创建时间':<19} {'截止日期'}") + print("-" * 60) + for task in tasks: + deadline = task.get('deadline', '') + print(f"{task['id']:<4} {task['status']:<6} {task['title']:<30} {task['created_at']:<19} {deadline}") + print("=" * 60) + +def done_task(task_id): + tasks = load_tasks() + found = False + for task in tasks: + if task['id'] == task_id: + task['status'] = "已完成" + found = True + break + if found: + save_tasks(tasks) + print(f"任务 {task_id} 已标记为完成") + else: + print(f"未找到任务 {task_id}") + +def interactive_mode(): + while True: + print("\n===== 校园任务清单 =====") + print("1. 添加任务") + print("2. 查看任务列表") + print("3. 标记任务完成") + print("4. 退出") + choice = input("请输入选择 (1-4): ") + + if choice == "1": + title = input("请输入任务标题: ").strip() + if not title: + print("标题不能为空") + continue + deadline = input("请输入截止日期 (可选,格式: YYYY-MM-DD): ").strip() + deadline = deadline if deadline else None + add_task(title, deadline) + elif choice == "2": + list_tasks() + elif choice == "3": + list_tasks() + try: + task_id = int(input("请输入要完成的任务编号: ")) + done_task(task_id) + except ValueError: + print("任务编号必须是数字") + elif choice == "4": + print("退出程序") + break + else: + print("无效选择,请输入 1-4") + +def main(): + if len(sys.argv) >= 2: + command = sys.argv[1] + + if command == "add": + if len(sys.argv) < 3: + print("用法: python main.py add <title> [deadline]") + return + title = sys.argv[2] + deadline = sys.argv[3] if len(sys.argv) > 3 else None + add_task(title, deadline) + elif command == "list": + list_tasks() + elif command == "done": + if len(sys.argv) < 3: + print("用法: python main.py done <id>") + return + try: + task_id = int(sys.argv[2]) + done_task(task_id) + except ValueError: + print("任务编号必须是数字") + else: + print(f"未知命令: {command}") + else: + interactive_mode() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git "a/personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" "b/personal/Experiment4/\345\256\236\351\252\214\345\233\233\345\256\236\351\252\214\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..ea9502d3f587e1044d05d48109339d72b5347b1d GIT binary patch literal 52392 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 literal 0 HcmV?d00001 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a27233a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# 让 tests 作为 Python package,支持 unittest discover diff --git a/tests/test_campus_task.py b/tests/test_campus_task.py new file mode 100644 index 0000000..04b134e --- /dev/null +++ b/tests/test_campus_task.py @@ -0,0 +1,204 @@ +import os +import json +import sys +import unittest +from unittest.mock import patch, MagicMock + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from campus_task.task_service import add_task, get_all_tasks, mark_task_done, get_task_by_id, get_tasks_by_priority, delete_task, get_tasks_by_status +from campus_task.task_storage import load_tasks, save_tasks, TASKS_FILE + + +class TestCampusTask(unittest.TestCase): + + def setUp(self): + self.test_file = os.path.join(os.path.dirname(__file__), "test_tasks.json") + + import campus_task.task_storage as storage + self.original_file = storage.TASKS_FILE + storage.TASKS_FILE = self.test_file + + if os.path.exists(self.test_file): + os.remove(self.test_file) + + def tearDown(self): + if os.path.exists(self.test_file): + os.remove(self.test_file) + + import campus_task.task_storage as storage + storage.TASKS_FILE = self.original_file + + def test_add_task_success(self): + task = add_task("完成软件工程实验3") + self.assertIsNotNone(task) + self.assertEqual(task.title, "完成软件工程实验3") + self.assertEqual(task.status, "待完成") + self.assertEqual(task.id, 1) + self.assertIsNotNone(task.created_at) + + def test_add_task_with_deadline(self): + task = add_task("提交数据结构作业", "2026-07-01") + self.assertEqual(task.deadline, "2026-07-01") + + def test_add_empty_title_fails(self): + with self.assertRaises(ValueError): + add_task("") + + def test_view_empty_task_list(self): + tasks = get_all_tasks() + self.assertEqual(len(tasks), 0) + + def test_view_non_empty_task_list(self): + add_task("任务1") + add_task("任务2") + tasks = get_all_tasks() + self.assertEqual(len(tasks), 2) + self.assertEqual(tasks[0].title, "任务1") + self.assertEqual(tasks[1].title, "任务2") + + def test_complete_existing_task(self): + add_task("待完成任务") + result = mark_task_done(1) + self.assertTrue(result) + task = get_task_by_id(1) + self.assertEqual(task.status, "已完成") + + def test_complete_non_existing_task(self): + result = mark_task_done(999) + self.assertFalse(result) + + def test_task_id_increments(self): + task1 = add_task("任务1") + task2 = add_task("任务2") + task3 = add_task("任务3") + self.assertEqual(task1.id, 1) + self.assertEqual(task2.id, 2) + self.assertEqual(task3.id, 3) + + def test_json_file_not_exists_creates_empty_list(self): + test_file = os.path.join(os.path.dirname(__file__), "nonexistent.json") + if os.path.exists(test_file): + os.remove(test_file) + + import campus_task.task_storage as storage + original_file = storage.TASKS_FILE + storage.TASKS_FILE = test_file + + tasks = load_tasks() + self.assertEqual(tasks, []) + + storage.TASKS_FILE = original_file + + def test_json_file_corrupted_returns_empty_list(self): + test_file = os.path.join(os.path.dirname(__file__), "corrupted.json") + with open(test_file, 'w', encoding='utf-8') as f: + f.write("not valid json") + + import campus_task.task_storage as storage + original_file = storage.TASKS_FILE + storage.TASKS_FILE = test_file + + tasks = load_tasks() + self.assertEqual(tasks, []) + + storage.TASKS_FILE = original_file + if os.path.exists(test_file): + os.remove(test_file) + + def test_completed_task_not_repeatedly_completed(self): + add_task("测试任务") + mark_task_done(1) + + task_before = get_task_by_id(1) + self.assertEqual(task_before.status, "已完成") + + mark_task_done(1) + + task_after = get_task_by_id(1) + self.assertEqual(task_after.status, "已完成") + self.assertEqual(task_after.id, 1) + + def test_task_persists_after_save_and_reload(self): + task = add_task("持久化测试任务", "2026-12-31") + + tasks = get_all_tasks() + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0].title, "持久化测试任务") + self.assertEqual(tasks[0].deadline, "2026-12-31") + self.assertEqual(tasks[0].status, "待完成") + + def test_add_task_with_priority(self): + task = add_task("高优先级任务", priority="高") + self.assertEqual(task.priority, "高") + + def test_add_task_default_priority(self): + task = add_task("普通任务") + self.assertEqual(task.priority, "普通") + + def test_add_task_invalid_priority_fails(self): + with self.assertRaises(ValueError): + add_task("测试任务", priority="invalid") + + def test_get_tasks_by_priority(self): + add_task("高优先级任务1", priority="高") + add_task("高优先级任务2", priority="高") + add_task("普通任务", priority="普通") + add_task("低优先级任务", priority="低") + + high_tasks = get_tasks_by_priority("高") + self.assertEqual(len(high_tasks), 2) + self.assertEqual(high_tasks[0].priority, "高") + + low_tasks = get_tasks_by_priority("低") + self.assertEqual(len(low_tasks), 1) + self.assertEqual(low_tasks[0].priority, "低") + + def test_delete_existing_task(self): + add_task("待删除任务") + result = delete_task(1) + self.assertTrue(result) + tasks = get_all_tasks() + self.assertEqual(len(tasks), 0) + + def test_delete_non_existing_task(self): + result = delete_task(999) + self.assertFalse(result) + + def test_delete_task_updates_list(self): + add_task("任务1") + add_task("任务2") + add_task("任务3") + + delete_task(2) + + tasks = get_all_tasks() + self.assertEqual(len(tasks), 2) + self.assertEqual(tasks[0].title, "任务1") + self.assertEqual(tasks[1].title, "任务3") + + def test_get_tasks_by_status_pending(self): + add_task("待完成任务1") + add_task("待完成任务2") + mark_task_done(1) + + pending_tasks = get_tasks_by_status("待完成") + self.assertEqual(len(pending_tasks), 1) + self.assertEqual(pending_tasks[0].title, "待完成任务2") + + def test_get_tasks_by_status_completed(self): + add_task("任务1") + add_task("任务2") + mark_task_done(1) + + completed_tasks = get_tasks_by_status("已完成") + self.assertEqual(len(completed_tasks), 1) + self.assertEqual(completed_tasks[0].title, "任务1") + + def test_get_tasks_by_invalid_status_fails(self): + with self.assertRaises(ValueError): + get_tasks_by_status("无效状态") + + +if __name__ == "__main__": + unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py new file mode 100644 index 0000000..d64af61 --- /dev/null +++ b/tests/test_cli_e2e.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""实验八 新增:CLI 端到端集成测试(黑盒级别,subprocess 直接调用 `python main.py ...`) + +每个用例都在独立的 TemporaryDirectory 下运行(chdir 进入),不污染真实 tasks.json, +用例之间 100% 隔离。 +""" +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +MAIN_PY = PROJECT_ROOT / "main.py" +PYTHON_EXE = sys.executable + + +def _run(argv, cwd): + """统一的子进程 runner,返回 (returncode, stdout, stderr)""" + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + # 保证 python 首先 import 到 PROJECT_ROOT 下的 campus_task(不依赖已安装包) + env["PYTHONPATH"] = str(PROJECT_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + cmd = [PYTHON_EXE, str(MAIN_PY)] + list(argv) + p = subprocess.run( + cmd, + cwd=str(cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return ( + p.returncode, + p.stdout.decode("utf-8", errors="replace"), + p.stderr.decode("utf-8", errors="replace"), + ) + + +class TestCLIExitCodeConventions(unittest.TestCase): + """【实验八新增】Exit Code 约定: + 成功 = 0 + 用户输入错误/校验失败 = 2(与 argparse.ArgumentError 保持一致) + 未知命令 = 2 + 无参进入交互模式(stdin 无输入时 read EOF)= 仍返回 0(由 interactive_mode 决定) + """ + + def test_unknown_command_exit_2_and_shows_available(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["not_exist_cmd"], cwd=d) + self.assertEqual(rc, 2, f"未知命令应 exit=2 实际={rc}") + self.assertIn("未知命令", out) + self.assertIn("可用命令", out) + self.assertIn("add", out) + + def test_add_no_args_shows_usage_exit_2(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add"], cwd=d) + self.assertEqual(rc, 2, f"add 缺少参数应 exit=2 实际={rc}") + self.assertIn("用法", out) + self.assertIn("add", out) + + +class TestCLIAddAndList(unittest.TestCase): + def test_add_one_task_success(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add", "交软件工程作业", "2026-07-15"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("任务已添加", out) + self.assertIn("[1]", out) + # 持久化验证:tasks.json 存在且长度 == 1 + tasks_json = Path(d) / "tasks.json" + self.assertTrue(tasks_json.exists(), "tasks.json 应被创建") + data = json.loads(tasks_json.read_text(encoding="utf-8")) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["title"], "交软件工程作业") + self.assertEqual(data[0]["priority"], "普通") + + def test_add_empty_title_rejected(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["add", " "], cwd=d) + self.assertEqual(rc, 2, f"空标题应 exit=2 实际={rc}") + self.assertIn("不能", out) # 标题不能为空 / 任务标题不能为空 + # 不应有 tasks.json 写入(写入前校验失败) + tasks_json = Path(d) / "tasks.json" + if tasks_json.exists(): + data = json.loads(tasks_json.read_text(encoding="utf-8")) + self.assertEqual(data, []) + + def test_list_empty_shows_placeholder(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["list"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("暂无任务", out) + + +class TestCLIDoneAndFilter(unittest.TestCase): + def test_done_not_found_then_add_then_done(self): + with tempfile.TemporaryDirectory() as d: + rc, out, _ = _run(["done", "99"], cwd=d) + self.assertEqual(rc, 0) # 业务未找到不算是参数错误 + self.assertIn("未找到任务 99", out) + + rc, _, _ = _run(["add", "复习算法"], cwd=d) + self.assertEqual(rc, 0) + rc, out, _ = _run(["done", "1"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("已标记为完成", out) + + rc, out, _ = _run(["filter", "已完成"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("复习算法", out) + + +class TestCLIExportCSVAndEdit(unittest.TestCase): + def test_export_csv_has_utf8_bom_and_header(self): + with tempfile.TemporaryDirectory() as d: + _run(["add", "买牛奶,鸡蛋"], cwd=d) + rc, out, _ = _run(["export_csv", "my.csv"], cwd=d) + self.assertEqual(rc, 0) + csv_path = Path(d) / "my.csv" + self.assertTrue(csv_path.exists()) + raw = csv_path.read_bytes() + self.assertTrue(raw.startswith(b"\xef\xbb\xbf"), "CSV 必须带 UTF-8 BOM") + content = raw.decode("utf-8-sig") + header = content.splitlines()[0] + for col in ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]: + self.assertIn(col, header) + + def test_edit_priority_and_verify_in_list(self): + with tempfile.TemporaryDirectory() as d: + _run(["add", "测试任务A"], cwd=d) + rc, out, _ = _run(["edit", "1", "priority", "紧急"], cwd=d) + self.assertEqual(rc, 0) + self.assertIn("已更新", out) + rc, out, _ = _run(["list"], cwd=d) + self.assertIn("紧急", out) + self.assertIn("测试任务A", out) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_exp5_overdue_and_search.py b/tests/test_exp5_overdue_and_search.py new file mode 100644 index 0000000..809a5fe --- /dev/null +++ b/tests/test_exp5_overdue_and_search.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +实验5:迭代开发 - 功能A 显示逾期任务 / 功能B 搜索任务关键字 +TDD 阶段:先写测试(RED),验证会失败,然后写代码让它通过(GREEN)。 +""" +import unittest +import os +import sys +import tempfile +import shutil +from datetime import datetime, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_test_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +class TestOverdueFeature(unittest.TestCase): + """功能A:显示逾期任务 get_overdue_tasks()""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_no_deadline_not_overdue(self): + """无截止日期的任务:不应出现在逾期列表""" + from campus_task.task_service import add_task, get_overdue_tasks + add_task("无截止日期的任务") + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_future_deadline_not_overdue(self): + """未来截止日期的任务:不逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + add_task("10天后交的作业", future) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_past_deadline_pending_is_overdue(self): + """过去截止 + 状态=待完成:算逾期""" + from campus_task.task_service import add_task, get_overdue_tasks + past = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d") + add_task("3天前要交的报告", past) + result = get_overdue_tasks() + self.assertEqual(len(result), 1) + self.assertEqual(result[0].title, "3天前要交的报告") + + def test_past_deadline_done_is_NOT_overdue(self): + """过去截止 + 状态=已完成:不算逾期""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d") + add_task("5天前截止但已完成", past) + mark_task_done(1) + self.assertEqual(len(get_overdue_tasks()), 0) + + def test_empty_task_list_overdue_count_zero(self): + """任务列表为空:逾期为空列表(不报错)""" + from campus_task.task_service import get_overdue_tasks + self.assertEqual(get_overdue_tasks(), []) + + def test_mixed_list_only_returns_pending_past(self): + """混合任务:只返回 待完成+过期 的那些""" + from campus_task.task_service import add_task, mark_task_done, get_overdue_tasks + past1 = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d") + past2 = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + future = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + t1 = add_task("逾期作业A", past1) + t2 = add_task("逾期作业B", past2) + add_task("明天交", future) + add_task("无截止") + t5 = add_task("过去但已完成", past1) + mark_task_done(t5.id) + result = get_overdue_tasks() + ids = sorted([t.id for t in result]) + self.assertEqual(ids, sorted([t1.id, t2.id])) + + +class TestSearchFeature(unittest.TestCase): + """功能B:搜索任务关键字 search_tasks(keyword)""" + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + # ---------------- 测试用例 ---------------- + def test_empty_keyword_raises_error(self): + """空或全空格关键字:应抛 ValueError""" + from campus_task.task_service import add_task, search_tasks + add_task("随便的任务") + with self.assertRaises(ValueError): + search_tasks("") + with self.assertRaises(ValueError): + search_tasks(" ") + + def test_search_no_match_returns_empty(self): + """关键字完全不匹配:返回空列表""" + from campus_task.task_service import add_task, search_tasks + add_task("软件工程实验报告") + add_task("数据结构作业") + self.assertEqual(search_tasks("不存在的文字"), []) + + def test_search_partial_match_case_insensitive(self): + """子串匹配:中文包含,英文大小写不敏感""" + from campus_task.task_service import add_task, search_tasks + add_task("写软件工程EXPERIMENT报告") + add_task("软件工程期末复习") + add_task("数据结构作业") + r1 = search_tasks("软件工程") + self.assertEqual(len(r1), 2) + r2 = search_tasks("experiment") + self.assertEqual(len(r2), 1) + self.assertIn("EXPERIMENT", r2[0].title) + + def test_search_returns_regardless_of_status(self): + """搜索结果包含待完成和已完成任务""" + from campus_task.task_service import add_task, mark_task_done, search_tasks + add_task("实验报告") + add_task("实验报告副本") + mark_task_done(1) + result = search_tasks("实验") + self.assertEqual(len(result), 2) + status_set = {t.status for t in result} + self.assertEqual(status_set, {"待完成", "已完成"}) + + def test_search_keyword_stripped(self): + """关键字首尾空格被去掉;中间空格保留""" + from campus_task.task_service import add_task, search_tasks + add_task("写 软件工程 实验") + r = search_tasks(" 软件工程 ") + self.assertEqual(len(r), 1) + r2 = search_tasks("写 软件工程") + self.assertEqual(len(r2), 1) + + def test_empty_task_list_search_safe(self): + """空任务列表搜索不报错,返回空""" + from campus_task.task_service import search_tasks + self.assertEqual(search_tasks("任意"), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_exp6_export_and_edit.py b/tests/test_exp6_export_and_edit.py new file mode 100644 index 0000000..e79ff08 --- /dev/null +++ b/tests/test_exp6_export_and_edit.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +""" +实验6:产品化收尾 +功能A:CR-003 导出任务为 CSV(UTF-8 BOM,Excel 2019 直接打开不乱码) +功能B:编辑任务(title/deadline/priority/status 局部更新,非全字段覆盖) +TDD:先写测试(RED),再写代码(GREEN) +""" +import unittest +import os +import sys +import tempfile +import shutil +import csv +import io + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +_ORIG_CWD = os.getcwd() +_TEMP_DIRS = [] + + +def _make_isolated_dir(): + d = tempfile.mkdtemp(prefix="campus_task_e6_") + _TEMP_DIRS.append(d) + return d + + +def _cleanup(): + for d in _TEMP_DIRS: + try: + shutil.rmtree(d) + except Exception: + pass + + +# ---------------- CSV 导出(功能A) ---------------- +class TestExportCSV(unittest.TestCase): + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + def test_default_filename_is_tasks_csv(self): + """不传文件名:默认生成 tasks.csv""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("测试任务1") + path = export_tasks_csv() + self.assertEqual(os.path.basename(path), "tasks.csv") + self.assertTrue(os.path.exists(path)) + + def test_custom_filename_works(self): + """传自定义文件名:导出到指定路径,返回绝对路径""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("测试任务2") + path = export_tasks_csv("分工清单.csv") + self.assertTrue(os.path.abspath(path)) + self.assertTrue(path.endswith("分工清单.csv")) + + def test_csv_header_fixed_and_has_utf8_bom(self): + """① 表头固定为: 编号,标题,状态,优先级,创建时间,截止日期 + ② 文件首 3 字节 = UTF-8 BOM (EF BB BF),Excel 2019 直接打开不乱码""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("写报告", "2026-06-30") + path = export_tasks_csv() + with open(path, "rb") as f: + head = f.read(3) + self.assertEqual(head, b"\xef\xbb\xbf", "未检测到 UTF-8 BOM,Excel 打开将乱码") + + with open(path, "r", encoding="utf-8-sig", newline="") as f: + reader = csv.reader(f) + header = next(reader) + self.assertEqual( + header, + ["编号", "标题", "状态", "优先级", "创建时间", "截止日期"], + "CSV 表头与 CR-003 验收标准不一致", + ) + + def test_title_with_comma_is_quoted(self): + """标题含逗号:按 CSV 标准用双引号包裹,解析后与原值一致""" + from campus_task.task_service import add_task, export_tasks_csv + add_task("阅读《软件工程导论》第6,7,8章,做笔记") + path = export_tasks_csv() + with open(path, "r", encoding="utf-8-sig", newline="") as f: + reader = csv.reader(f) + next(reader) # 跳过表头 + row = next(reader) + self.assertEqual( + row[1], "阅读《软件工程导论》第6,7,8章,做笔记", + "标题含逗号未被正确 CSV 转义" + ) + + def test_empty_task_list_still_has_bom_and_header(self): + """空任务列表:文件必须有 BOM + 表头(不要全空,保证 Excel 打开格式一致)""" + from campus_task.task_service import export_tasks_csv + path = export_tasks_csv() + with open(path, "rb") as f: + bom = f.read(3) + self.assertEqual(bom, b"\xef\xbb\xbf") + with open(path, "r", encoding="utf-8-sig", newline="") as f: + content = f.read().strip().splitlines() + self.assertEqual(len(content), 1) # 只有表头 + self.assertIn("编号", content[0]) + + +# ---------------- 编辑任务(功能B) ---------------- +class TestEditTask(unittest.TestCase): + + def setUp(self): + self._cwd = _make_isolated_dir() + os.chdir(self._cwd) + import importlib + from campus_task import task_storage, task_service + importlib.reload(task_storage) + importlib.reload(task_service) + + def tearDown(self): + os.chdir(_ORIG_CWD) + + @classmethod + def tearDownClass(cls): + _cleanup() + + def test_edit_title_only_others_unchanged(self): + """仅改标题:其他 5 个字段与编辑前完全一致""" + from campus_task.task_service import add_task, edit_task, get_task_by_id + old = add_task("旧标题", "2026-12-31") + old_id, old_deadline, old_priority, old_status, old_created = ( + old.id, old.deadline, old.priority, old.status, old.created_at + ) + new = edit_task(old.id, title="新标题") + self.assertEqual(new.title, "新标题") + self.assertEqual(new.id, old_id) + self.assertEqual(new.deadline, old_deadline) + self.assertEqual(new.priority, old_priority) + self.assertEqual(new.status, old_status) + self.assertEqual(new.created_at, old_created) + # 持久化:重新读取后也要一致 + persisted = get_task_by_id(old.id) + self.assertEqual(persisted.title, "新标题") + + def test_edit_multiple_fields_together(self): + """同时改 4 个字段(title/deadline/priority/status)全部生效""" + from campus_task.task_service import add_task, edit_task + t = add_task("草稿", "2026-01-01") + r = edit_task( + t.id, + title="最终稿", + deadline="2026-07-15", + priority="高", + status="已完成", + ) + self.assertEqual(r.title, "最终稿") + self.assertEqual(r.deadline, "2026-07-15") + self.assertEqual(r.priority, "高") + self.assertEqual(r.status, "已完成") + + def test_empty_whitespace_title_raises(self): + """改标题为空字符串或全空白:抛 ValueError(与 add_task 校验一致)""" + from campus_task.task_service import add_task, edit_task + t = add_task("正常标题") + with self.assertRaises(ValueError): + edit_task(t.id, title="") + with self.assertRaises(ValueError): + edit_task(t.id, title=" \t\n ") + + def test_nonexistent_id_raises(self): + """编辑不存在的 id:抛 ValueError,附带 id 信息便于排错""" + from campus_task.task_service import edit_task + with self.assertRaises(ValueError): + edit_task(99999, title="随便改") + + def test_invalid_status_raises(self): + """传入非法状态:抛 ValueError(值域必须与 get_tasks_by_status 对齐)""" + from campus_task.task_service import add_task, edit_task + t = add_task("待完成任务") + with self.assertRaises(ValueError): + edit_task(t.id, status="取消") + with self.assertRaises(ValueError): + edit_task(t.id, status="PENDING") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_status_filter.py b/tests/test_status_filter.py index 4a6056b..dbfab33 100644 --- a/tests/test_status_filter.py +++ b/tests/test_status_filter.py @@ -3,7 +3,7 @@ import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from campus_task import add_task, mark_task_done, get_tasks_by_status +from campus_task import add_task, get_tasks_by_status, mark_task_done class TestStatusFilterFeature(unittest.TestCase): @@ -23,6 +23,8 @@ class TestStatusFilterFeature(unittest.TestCase): pending_tasks = get_tasks_by_status("待完成") self.assertEqual(len(pending_tasks), 2) + self.assertEqual(pending_tasks[0].title, "待完成任务1") + self.assertEqual(pending_tasks[1].title, "待完成任务2") def test_get_tasks_by_status_completed(self): add_task("任务1") @@ -31,6 +33,7 @@ class TestStatusFilterFeature(unittest.TestCase): completed_tasks = get_tasks_by_status("已完成") self.assertEqual(len(completed_tasks), 1) + self.assertEqual(completed_tasks[0].title, "任务1") def test_invalid_status_raises_error(self): with self.assertRaises(ValueError): -- Gitee From 576645eb0ff09766d479fdd0ddd125949d839ba0 Mon Sep 17 00:00:00 2001 From: kuiran <2940496089@qq.com> Date: Tue, 30 Jun 2026 13:55:33 +0800 Subject: [PATCH 25/25] fix: add missing sort functions and export compatibility --- campus_task/__init__.py | 6 ++++++ campus_task/task_service.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/campus_task/__init__.py b/campus_task/__init__.py index c62d2ba..419884e 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -21,8 +21,11 @@ from .task_service import ( # -------- 实验5 新增 -------- get_overdue_tasks, search_tasks, + sort_tasks_by_deadline, + sort_tasks_by_priority, # -------- 实验6 新增 -------- export_tasks_csv, + export_tasks_to_csv, edit_task, ) @@ -51,7 +54,10 @@ __all__ = [ # -------- 实验5 新增 -------- "get_overdue_tasks", "search_tasks", + "sort_tasks_by_deadline", + "sort_tasks_by_priority", # -------- 实验6 新增 -------- "export_tasks_csv", + "export_tasks_to_csv", "edit_task", ] diff --git a/campus_task/task_service.py b/campus_task/task_service.py index 7ad47a9..90bd0c3 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -202,3 +202,35 @@ def edit_task( save_tasks(tasks) return target + + +# ---------------- 实验5:新增功能C:按截止日期排序 ---------------- +def sort_tasks_by_deadline() -> List[Task]: + """按截止日期升序排列任务,无截止日期的排在最后""" + tasks = load_tasks() + return sorted(tasks, key=lambda t: t.deadline if t.deadline else "9999-12-31") + + +# ---------------- 实验5:新增功能D:按优先级排序 ---------------- +def sort_tasks_by_priority() -> List[Task]: + """按优先级从高到低排列:高 > 中 > 普通 > 低""" + priority_order = {"高": 0, "中": 1, "普通": 2, "低": 3} + tasks = load_tasks() + return sorted(tasks, key=lambda t: priority_order.get(t.priority, 3)) + + +# ---------------- 实验6:导出 CSV 兼容函数(用于测试) ---------------- +def export_tasks_to_csv(file_path: str) -> bool: + """导出任务为CSV文件(兼容测试用例)""" + try: + with open(file_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(["编号", "标题", "状态", "优先级", "创建时间", "截止日期"]) + for task in load_tasks(): + writer.writerow([ + task.id, task.title, task.status, + task.priority, task.created_at, task.deadline or '' + ]) + return True + except IOError: + return False -- Gitee