diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000000000000000000000000000000000..28b5216cf86953fddeee6fadc0c93f23476779f5 --- /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 0000000000000000000000000000000000000000..2d8a0ea17603a27c872f8c0224d171ffb8de49d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# ============== +# 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 +# 导出产物(用户隐私数据,不应入库) +*.csv +# ============== +# 生成的报告文件(由 generate_report.py 生成,不入代码仓) +# ============== +*.docx + +# ============== +# 测试/覆盖率 +# ============== +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# ============== +# 临时文件 +# ============== +*.tmp +*.bak +*.old diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..59b155231531e591a37fc0442a7054e74c6aceac --- /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/Experiment5/CHANGELOG.md b/Experiment5/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..81d126b4775b776dd6e205809d9a748dcc9f776b --- /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 0000000000000000000000000000000000000000..8724556eb78d6bb8e96c1477d29713285bc32f69 --- /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 0000000000000000000000000000000000000000..809a5fe7a3b1904d37f1d9b4f6dbedde2bda54b6 --- /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 0000000000000000000000000000000000000000..146d3b2dfc22ce29efb2cf0e787e0c1f844f74df --- /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 0000000000000000000000000000000000000000..b44b25c2937ad7e781347ff49f7d03e8880f8f78 --- /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 Binary files /dev/null and "b/Experiment5/\345\256\236\351\252\214\344\272\224\345\256\236\351\252\214\346\212\245\345\221\212.docx" differ 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 0000000000000000000000000000000000000000..8fb09dc3d46cadb7f695171f44b83b7336c34c09 --- /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 Binary files /dev/null and "b/Experiment7/\345\256\236\351\252\214\344\270\203\345\256\236\351\252\214\346\212\245\345\221\212.docx" differ diff --git a/Experiment8/blackbox_results.json b/Experiment8/blackbox_results.json new file mode 100644 index 0000000000000000000000000000000000000000..8a3b578b5411181c7809043f0a2667500e674bc7 --- /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 0000000000000000000000000000000000000000..3a19c651a75606add867da2f685c23ebfc3d6cbc --- /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 0000000000000000000000000000000000000000..b961e0c19074196ff6a7acecc90dc3f43c45dfce --- /dev/null +++ b/Experiment8/coverage.xml @@ -0,0 +1,246 @@ + + + + + + D:\Report\Software Engineer\AI-Experiment\SoftwareProject\campus-task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiment8/generate_report.py b/Experiment8/generate_report.py new file mode 100644 index 0000000000000000000000000000000000000000..24c0b4b5d34fd2889ebb2a12e771ac4aab22e4b0 --- /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 0000000000000000000000000000000000000000..04c89f4877dab4d72d7643da8fce81d2892db7b4 --- /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 Binary files /dev/null and "b/Experiment8/\345\256\236\351\252\214\345\205\253\345\256\236\351\252\214\346\212\245\345\221\212.docx" differ diff --git a/README.md b/README.md index 431d9d57093179b68f304252b0925b990ac09118..8376353d36efe781a87b83433323d8671a8d2d29 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,14 +63,28 @@ campus_task/ |------|------|------| | **task_model.py** | 模型层 | 定义 Task 类,封装任务数据结构和序列化方法 | | **task_storage.py** | 存储层 | 负责从 JSON 文件读取和写入任务数据 | -| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成等业务逻辑 | +| **task_service.py** | 业务逻辑层 | 实现任务的添加、查询、完成、筛选、删除等业务逻辑 | | **main.py** | 命令行交互层 | 处理命令行参数和交互式界面,调用业务层 | +--- + ## 功能特性 -- 添加任务:支持设置任务标题和截止日期 -- 查看任务:显示所有任务的详细信息 -- 完成任务:标记任务为已完成状态 +### 基础功能 +- ✅ 添加任务:支持设置任务标题和截止日期 +- ✅ 查看任务:显示所有任务的详细信息 +- ✅ 完成任务:标记任务为已完成状态 + +### 实验4新增功能 +- ✅ **按状态筛选任务**(成员3开发):按"待完成"或"已完成"筛选任务列表 +- ✅ **按优先级筛选任务**(成员1开发):按"高/中/低/普通"优先级筛选 +- ✅ **删除任务**(成员2开发):根据任务编号删除指定任务 + +### 实验5新增功能(成员3 v1.1.0) +- ✅ **显示逾期任务**(成员3开发):一键查看"截止日期已过 + 状态为待完成"的任务列表 +- ✅ **搜索任务关键字**(成员3开发):按标题子串搜索,支持英文大小写不敏感、不区分状态 + +--- ## 命令用法 @@ -86,11 +114,64 @@ 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 +``` + +### 显示逾期任务(实验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 ``` +进入交互式模式后可选择: +1. 添加任务 +2. 查看任务列表 +3. 标记任务完成 +4. 按状态筛选任务 +5. 查看逾期任务(实验5新增) +6. 搜索任务关键字(实验5新增) +7. 退出 + +--- + ## 用户故事 ### 1. 记录任务截止日期 @@ -113,34 +194,235 @@ python main.py - **用户故事**:作为学生,我希望列出所有任务,以便一次性查看全部待办和已办。 - **验收标准**:list 命令能显示所有已添加的任务。 +### 6. 按状态筛选任务(实验4新增) +- **用户故事**:作为学生,我希望按状态筛选任务,以便只查看待完成或已完成的任务。 +- **验收标准**:filter 命令能根据"待完成/已完成"正确筛选任务列表,无效状态抛出错误。 + +### 7. 查看逾期任务(实验5新增) +- **用户故事**:作为学生,我希望每天早上一键查看「已过期但还没做」的作业,避免遗漏。 +- **验收标准**:`overdue` 命令仅返回「截止日期 < 今天」且「状态为待完成」的任务;截止日期合法但已完成的不出现;没有逾期时返回友好 emoji 提示。 + +### 8. 搜索任务关键字(实验5新增) +- **用户故事**:作为学生,当任务超过 15 条时,我希望按关键字搜索标题,找到特定任务。 +- **验收标准**:`search` 命令按标题子串匹配、英文大小写不敏感、不区分状态返回;空关键字抛 ValueError;无命中返回空。 + +--- + ## 数据存储 任务数据保存在 `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` 中同时存在多个功能选项 + +--- + +## 实验五:迭代开发与变更管理(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 版实验报告(含过程与反思) | + +--- + ## 测试与验收 -### 运行测试 +### 运行所有测试 +```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 +438,21 @@ 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 | +| 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)** 空任务列表搜索不报错返回 [] | ### 需求-测试用例对应表 @@ -165,6 +462,9 @@ 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 | +| 查看逾期任务(实验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 | @@ -183,10 +483,17 @@ python tests/test_campus_task.py - 将 `tasks.remove(task)` 改回 `task.status = "已完成"` **步骤 4:验证测试通过** -- 所有 12 个测试用例全部通过 +- 所有测试用例全部通过 + +--- ## 课后反思问题 +### 实验一反思 +为什么"会写 Python"还不等于"会做软件"? + +答:编写单个 Python 代码片段与开发完整软件系统存在本质区别。软件不仅需要实现功能,还需要考虑需求分析、架构设计、错误处理、数据持久化、用户体验等多个方面。会写 Python 只是掌握了编程语言本身,而会做软件需要理解软件工程的完整流程,包括需求转化、模块化设计、测试验证、文档编写等能力。 + ### 实验二反思 为什么软件设计常常不是"多此一举",而是为了未来少改错? @@ -195,4 +502,28 @@ python tests/test_campus_task.py ### 实验三反思 测试是在"找老师要分",还是在给未来的自己提供安全网? -答:测试的真正价值在于为未来的自己和团队提供安全网。通过自动化测试,可以快速验证代码修改是否破坏了现有功能,防止回归错误。测试用例也是文档的一种形式,清晰地说明了代码应该如何工作。当需求变更或代码重构时,测试用例确保系统行为的一致性,减少了维护成本。 \ No newline at end of file +答:测试的真正价值在于为未来的自己和团队提供安全网。通过自动化测试,可以快速验证代码修改是否破坏了现有功能,防止回归错误。测试用例也是文档的一种形式,清晰地说明了代码应该如何工作。当需求变更或代码重构时,测试用例确保系统行为的一致性,减少了维护成本。 + +### 实验四反思 +为什么在团队协作中,"先自己分支写好测过"再提 PR,比直接改主分支更靠谱? + +答:分支开发是团队协作的最佳实践,原因如下: +1. **隔离风险**:功能开发不影响主分支的稳定性,即使代码有问题也不会阻碍其他成员 +2. **代码评审**:PR 机制强制代码经过他人审查,提前发现 Bug 和设计问题 +3. **可追溯性**:每个功能对应一个分支和 PR,历史记录清晰,便于回溯和版本回退 +4. **并行开发**:多成员可同时在不同分支开发不同功能,互不干扰 +5. **渐进式合并**:功能完整且测试通过后才合并,保证主分支始终可运行 + +### 实验五反思 +为什么"用户说的"和"系统该做的"不是一回事?请结合本次实验中「逾期任务」判定里"今天到期"算不算逾期这个细节回答。 + +答:「用户说的」是场景化、情绪化的自然语言(带上下文和潜台词);「系统该做的」是经过「需求翻译」之后可执行、可测试的精确验收标准。两者之间差了一个关键的**去模糊化**步骤——这正是软件工程师的核心价值,而不是"把用户的话直接写成 if-else"。 + +本次实验小李的原话是「每天早上只想看今天已经过期但还没做的作业」。如果直接死译为 `deadline <= today`,会把「今天到期」和「半年前已过期」的任务混在一起列出来,小李看到会更焦虑(因为真正紧急的和不那么紧急的没有层次感)。 + +正确的「翻译」过程拆解了三层: +1. **「还没做的」→ `status == "待完成"`**:已完成的任务即使过了截止日期也不显示,排除噪音; +2. **「已经过期」→ `deadline.date() 严格 < today`(不是 ≤)**:给"今天到期"的任务留足一整天的缓冲时间,避免一早就给用户造成不必要的压力; +3. **「都没有的别吓我」→ 空结果展示 🎉 emoji** 而不是冷冰冰的空列表。 + +如果跳过这一步"翻译"会导致两个具体坏处:① 今天到期的任务被过早地当成逾期,用户一早产生不必要的压力;② 已完成的任务也出现在列表里干扰视线。所以做需求 ≠ 逐字执行用户的话,而是把模糊的「用户原话」拆解为可验证的验收标准。这正是迭代开发(实验5)相对于瀑布式开发最大的进步。 diff --git a/README2.md b/README2.md deleted file mode 100644 index fd3c5864305fb865084487e77c1ae5ef018beb02..0000000000000000000000000000000000000000 --- 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 diff --git a/USER_GUIDE.md b/USER_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..2b384b4941a9d724d57b7bbf512c69dbfe712535 --- /dev/null +++ b/USER_GUIDE.md @@ -0,0 +1,213 @@ +# CampusTask 用户手册 + +## 概述 + +CampusTask 是一个命令行任务管理工具,帮助学生管理课程作业、实验报告和考试复习任务。 + +## 安装 + +```bash +git clone +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 0000000000000000000000000000000000000000..8e841fc124b0ffdbcd54342e7744e5230026c390 --- /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 f8f220ae8fb4fb161e1871a21cd82afd0cfbc5b6..419884ef7d3fef35e5af341c8f078ce4e8fc7cb2 100644 --- a/campus_task/__init__.py +++ b/campus_task/__init__.py @@ -1,4 +1,63 @@ from .task_model import Task -from .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 .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, + sort_tasks_by_deadline, + sort_tasks_by_priority, + # -------- 实验6 新增 -------- + export_tasks_csv, + export_tasks_to_csv, + edit_task, +) -__all__ = ["Task", "add_task", "get_all_tasks", "mark_task_done", "get_task_by_id", "get_tasks_by_priority", "delete_task", "get_tasks_by_status"] \ 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", + "sort_tasks_by_deadline", + "sort_tasks_by_priority", + # -------- 实验6 新增 -------- + "export_tasks_csv", + "export_tasks_to_csv", + "edit_task", +] diff --git a/campus_task/__pycache__/__init__.cpython-313.pyc b/campus_task/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index b037b868296f80a6f19e1e61d5c703a1f90d4e5a..0000000000000000000000000000000000000000 Binary files a/campus_task/__pycache__/__init__.cpython-313.pyc and /dev/null differ 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 6e87539c29116da121e4041213bccf3c610a8220..0000000000000000000000000000000000000000 Binary files a/campus_task/__pycache__/task_model.cpython-313.pyc and /dev/null differ 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 e14a666ef438d2c0707a385b5c6fc02eddc333a7..0000000000000000000000000000000000000000 Binary files a/campus_task/__pycache__/task_service.cpython-313.pyc and /dev/null differ diff --git a/campus_task/__pycache__/task_storage.cpython-313.pyc b/campus_task/__pycache__/task_storage.cpython-313.pyc deleted file mode 100644 index f2b6181ebeb3e9d97a28261b7e47ebf1cc7e7324..0000000000000000000000000000000000000000 Binary files a/campus_task/__pycache__/task_storage.cpython-313.pyc and /dev/null differ diff --git a/campus_task/task_printer.py b/campus_task/task_printer.py new file mode 100644 index 0000000000000000000000000000000000000000..87adec9f0759a54d3bc2e86bb9fe94dc54f05c1e --- /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 43a3e57fcd50e163a17a2b227d57ce1aeb5c16f4..90bd0c336695bb86c9137a6c065de754b3200681 100644 --- a/campus_task/task_service.py +++ b/campus_task/task_service.py @@ -1,24 +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: - if not title or title.strip() == "": - raise ValueError("任务标题不能为空") - 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) @@ -43,25 +56,181 @@ 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("优先级必须是: 高、中、低、普通") +def get_tasks_by_status(status: str) -> List[Task]: + # 统一走 validate_status 校验,错误消息与 edit_task 等其他入口保持一致 + validate_status(status) tasks = load_tasks() - return [task for task in tasks if task.priority == priority] + 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(由 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() - for i, task in enumerate(tasks): - if task.id == task_id: - del tasks[i] - save_tasks(tasks) - return True - return False + 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_status(status: str) -> List[Task]: - valid_statuses = ["待完成", "已完成"] - if status not in valid_statuses: - raise ValueError("状态必须是: 待完成、已完成") + +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 + + +# ---------------- 实验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() - return [task for task in tasks if task.status == status] + 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 + + +# ---------------- 实验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 diff --git a/campus_task/task_validator.py b/campus_task/task_validator.py new file mode 100644 index 0000000000000000000000000000000000000000..d8d8f5c1e6b1b9b76b9bdc73cb8ec41a73887cb5 --- /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/eval_cases.json b/eval_cases.json new file mode 100644 index 0000000000000000000000000000000000000000..aa4b1c367df74c3891486f0277ae14260ad5436a --- /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 000030c4f0b5dec332c96a22c31c4628150a715d..6e28bc7f9abad6ddb95c1f144a1c3f12eed387b6 100644 --- a/main.py +++ b/main.py @@ -1,36 +1,51 @@ +# -*- coding: utf-8 -*- +"""CampusTask 命令行入口(实验七重构:OCP COMMANDS 分派 + SRP printer/validator)""" 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, + get_overdue_tasks, # 实验5 新增 + search_tasks, # 实验5 新增 + export_tasks_csv, # 实验6 新增 + edit_task, # 实验6 新增 + print_tasks, # 实验7 重构:task_printer 模块统一格式化输出 +) + + +# 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("=" * 60) - print(f"{'编号':<4} {'状态':<6} {'标题':<30} {'创建时间':<19} {'截止日期'}") - print("-" * 60) - 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) def handle_add(args): if len(args) < 1: print("用法: python main.py add [deadline]") - return + return EXIT_USAGE title = args[0] deadline = args[1] if len(args) > 1 else None - task = add_task(title, deadline) - print(f"任务已添加: {task.title}") + try: + task = add_task(title, deadline) + except ValueError as e: + # deadline/title 校验错误(实验七统一由 validator 抛出) + print(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) @@ -40,33 +55,130 @@ def handle_done(args): print(f"未找到任务 {task_id}") except ValueError: print("任务编号必须是数字") + 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} 已删除") - else: - print(f"未找到任务 {task_id}") - except ValueError: - print("任务编号必须是数字") -def handle_status(args): +def handle_filter(args): if len(args) < 1: - print("用法: python main.py status <status>") - print("状态可选值: 待完成、已完成") - return + print("用法: python main.py filter <status>") + print("status 可选值: 待完成, 已完成") + return EXIT_USAGE status = args[0] try: tasks = get_tasks_by_status(status) - print(f"状态为 '{status}' 的任务:") + if not tasks: + print(f"暂无{status}任务") + return EXIT_OK + print(f"===== {status}任务 共{len(tasks)}条 =====") print_tasks(tasks) except ValueError as e: - print(f"筛选失败: {e}") + print(e) + return EXIT_USAGE + return EXIT_OK + + +# ----------- 实验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 <关键字>") + 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 EXIT_OK + print(f"[搜索结果] 关键字[{keyword}]命中 {len(tasks)} 条任务:") + print_tasks(tasks) + return EXIT_OK + + +# ----------- 实验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 + + +# ----------- 实验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 + + +# ----------- 实验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: @@ -74,11 +186,14 @@ def interactive_mode(): print("1. 添加任务") print("2. 查看任务列表") print("3. 标记任务完成") - print("4. 删除任务") - print("5. 按状态筛选任务") - print("6. 退出") - choice = input("请输入选择 (1-6): ") - + 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: @@ -86,8 +201,12 @@ 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}") + try: + 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": @@ -102,49 +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() + status = input("请输入筛选状态 (待完成/已完成): ").strip() try: tasks = get_tasks_by_status(status) - print(f"状态为 '{status}' 的任务:") - print_tasks(tasks) + if not tasks: + print(f"暂无{status}任务") + else: + print(f"===== {status}任务 共{len(tasks)}条 =====") + print_tasks(tasks) except ValueError as e: - print(f"筛选失败: {e}") + print(e) + elif choice == "5": + # 实验5 新增 + handle_overdue() elif choice == "6": - print("退出程序") + # 实验5 新增 + 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-6") + print("无效选择,请输入 1-9") + 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 == "delete": - handle_delete(args) - elif command == "status": - handle_status(args) - else: + handler = COMMANDS.get(command) + if handler is None: + available = " | ".join(COMMANDS.keys()) print(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/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 Binary files /dev/null and "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" differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..8ae25051c591817bd7f2d5e0e3d632f330f61de6 --- /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 0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc..0000000000000000000000000000000000000000 --- a/tasks.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/tasks2.json b/tasks2.json deleted file mode 100644 index f484bb0b4eb1762a6b12128b6e9550519258c59b..0000000000000000000000000000000000000000 --- 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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a27233a6e42d7607326e658f99b293dffce08ecc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# 让 tests 作为 Python package,支持 unittest discover 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 Binary files a/tests/__pycache__/test_campus_task.cpython-313.pyc and /dev/null differ diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..d64af6166baa36c35a7c74944e63c8da9675a01e --- /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 0000000000000000000000000000000000000000..809a5fe7a3b1904d37f1d9b4f6dbedde2bda54b6 --- /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 0000000000000000000000000000000000000000..e79ff080476b5b67eb3bb85e38fe42e9d46fc648 --- /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_experiment5.py b/tests/test_experiment5.py new file mode 100644 index 0000000000000000000000000000000000000000..ef2c445a41dc6c7e4a84ccbefc3739c90ac305c9 --- /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/tests/test_status_filter.py b/tests/test_status_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..dbfab337d091defc14212ad91f71511f0dc5b8eb --- /dev/null +++ b/tests/test_status_filter.py @@ -0,0 +1,43 @@ +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, get_tasks_by_status, mark_task_done + +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) + 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") + 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_invalid_status_raises_error(self): + with self.assertRaises(ValueError): + get_tasks_by_status("无效状态") + +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 0000000000000000000000000000000000000000..61cf2304343196fdbe3675d9a57ffd274680e0d6 --- /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 0000000000000000000000000000000000000000..0d384e810df40081135f97fdab4d124edf9ec4cf --- /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 0000000000000000000000000000000000000000..924c2be687dae7b3a54e62a785bfd1d0dc7e913d --- /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 0000000000000000000000000000000000000000..c7c8a14841f8466e2b9cae7465ef8298b618dd3d --- /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 0000000000000000000000000000000000000000..1a601c906dce4b60fb49afcdbd161cf7111cebed --- /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 0000000000000000000000000000000000000000..5a39ec10cb44f221a94a7ba39941ec7a84f16b1c --- /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 0000000000000000000000000000000000000000..c6fbf176a702018e06756f709c12b02d31bae29c --- /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