Commit 5a49060364a6948ec560010cd491438347838fa6
1 parent
1a31a2ad
U-002: target-project .graphlemon/ scaffold (c_project_create)
Implements c_project_create (03-C §3.0) as a plain function for U-007 to wire up later: idempotent by target_folder_path, creates .graphlemon/ + one init commit in an existing git repo, PATH_NOT_FOUND/PATH_NOT_WRITABLE/ GIT_INIT_DECLINED per contract, never silently `git init`s (02-V §2.1). Also corrects a real SDD-vs-plan conflict found while scoping this Unit: development-plan.md's U-002 line cited architecture doc §7.3's directory tree (.claude/agents/, skills/, top-level runs/) as what to create inside a target project. But §13.1 (a later, confirmed decision) states those are graphlemon's own platform-repo assets (U-003 scope), and 01-M's own graphlemon_subdir_initialized field already points at §13.2, whose target- project tree is only .git/ + .graphlemon/ + the target's pre-existing src/. §7.3 predates the "target project is an independent code space" pivot and was superseded by it. Corrected development-plan.md, unit_matrix.json, CLAUDE.md, and 05-实施约束与依赖.md accordingly (user confirmed, 2026-07-31). 23 stdlib unittest cases (src/test_schema.py + new src/test_project_scaffold.py), all passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
6 changed files
with
250 additions
and
6 deletions
CLAUDE.md
| ... | ... | @@ -115,7 +115,9 @@ directly; anything worth reusing has already been copied and adapted into this r |
| 115 | 115 | - `supervisor/isolation/` — staged utility library (isolate/diff/apply), not yet wired to anything |
| 116 | 116 | - `spikes/` — one-off validation spikes (currently just A-1) |
| 117 | 117 | - `src/` (created by U-001) — graphlemon's own backend source (schema, then U-007+ HTTP/WebSocket server). |
| 118 | - **Not the same `src/` U-002 generates inside a target project's own repo** — those are two different | |
| 119 | - physical repos that can coexist on disk during PH5 self-target. See 05-实施约束与依赖.md §5.7. | |
| 118 | + **Not the same `src/` a target project already has in its own repo** — a target project's `src/` is | |
| 119 | + its own pre-existing source tree; graphlemon does not create or restructure it (see 05-实施约束与依赖.md | |
| 120 | + §5.7 and architecture doc §13.2 — U-002 only creates `.graphlemon/` inside a target project, corrected | |
| 121 | + 2026-07-31 after finding development-plan.md's U-002 line had cited the wrong architecture section). | |
| 120 | 122 | - Everything else (`.claude/agents/`, `skills/`, `runs/`, per-target-project scaffolding) gets created by |
| 121 | 123 | U-002/U-003 — it doesn't exist yet, and that's expected at this stage. | ... | ... |
docs/development-plan.md
| ... | ... | @@ -48,9 +48,10 @@ |
| 48 | 48 | - 覆盖:01-M 全部实体 |
| 49 | 49 | - 源码位置:graphlemon 仓库根目录 `src/`(见 05 节 §5.7"源码目录约定",与 U-002 在目标项目里生成的 `src/` 是两回事) |
| 50 | 50 | |
| 51 | -- **U-002 [Enabler]** 目标项目 Git 仓库目录骨架初始化逻辑(架构文档 §7.3 目录树:`.claude/agents/`、`.claude/agent-memory/`、`.claude/workflows/`、`skills/`、`runs/`、`src/`)+ `.graphlemon/` 子目录(架构文档 §13.2) | |
| 51 | +- **U-002 [Enabler]** 目标项目 Git 仓库目录骨架初始化逻辑:只生成 `.graphlemon/` 子目录(含 `runs/<task_id>/`,架构文档 §13.2——01-M `project.graphlemon_subdir_initialized` 字段本身指向的就是这节,不是 §7.3) | |
| 52 | + - **2026-07-31 订正**:development-plan.md 这行原先误引用了架构文档 §7.3 的旧目录树(`.claude/agents/`、`.claude/agent-memory/`、`.claude/workflows/`、`skills/`、顶层 `runs/`)。但 §13.1("已确认"决策,晚于 §7.3)明确写着这些是 graphlemon **平台自己仓库**的资产("graphlemon 平台自己的仓库只保留平台资产:Org/Work Graph 引擎、Skill 库、Agent 人设...不含任何目标项目的业务代码"),属于 U-003 范围,不是目标项目、不是 U-002。§7.3 是"graphlemon 只服务自己开发自己"阶段(自举模式)的旧设计,已被 §13"目标项目是独立代码空间"这个决策取代。目标项目自己的 `src/` 不需要 graphlemon 创建或规定结构("目标项目自己原有的源码目录,graphlemon 不规定具体结构,尊重目标项目原有约定")。用户确认,2026-07-31。 | |
| 52 | 53 | - 依赖:U-001(`project.graphlemon_subdir_initialized` 字段需要有实体支撑) |
| 53 | - - Done:给定一个空 git 仓库,能生成完整目录骨架并产生一次初始化 commit;非 git 仓库时按 02-V 2.1 节的确认态流程走(不静默 `git init`) | |
| 54 | + - Done:给定一个空 git 仓库,能生成 `.graphlemon/` 子目录骨架并产生一次初始化 commit;非 git 仓库时按 02-V 2.1 节的确认态流程走(不静默 `git init`) | |
| 54 | 55 | - 覆盖:01-M `project`;03-C `c_project_create` |
| 55 | 56 | |
| 56 | 57 | - **U-003 [Enabler]** graphlemon 平台自身的仓库/运行时目录结构(与目标项目仓库物理分离;本地非 Git 的原始日志目录,见架构文档 §13.2) | ... | ... |
docs/sdd/05-实施约束与依赖.md
| ... | ... | @@ -68,7 +68,7 @@ |
| 68 | 68 | - 前端是本地 Web 应用(浏览器页面),后端用 `http.server`/`socketserver` 等 stdlib 原生能力提供 HTTP API;WebSocket 若 stdlib 支持不足,实施时(U-007)需要验证并可退化为 SSE/长轮询,但仍在 stdlib 范围内解决,不引入第三方包;前端不引入 npm/构建步骤,可以是原生 HTML/CSS/JS 或参考 graphloop `graph.html`/`project.html` 的单文件 no-build 页面模式 |
| 69 | 69 | - **例外**:这条约束只针对 graphlemon 平台自身的实现语言,不代表 graphlemon 生成/审查的目标项目代码也必须是 Python——目标项目用什么语言由目标项目自己的技术栈决定 |
| 70 | 70 | - **影响**:development-plan.md 的 U-007(本地后端骨架)、U-008(前端骨架)、U-036(Deploy Supervisor 骨架)的 Done 标准隐含"纯 stdlib"这条约束 |
| 71 | -- **源码目录约定(2026-07-30 补充)**:graphlemon 平台自身的后端源码(U-001 的 schema 实现、U-007 起的 HTTP/WebSocket 后端)落在 graphlemon 仓库根目录的 `src/`。这与 U-002 在**目标项目**仓库内生成的 `src/` 是两个完全不同的目录(分属两个物理仓库,见架构文档 §13.1"目标项目是独立代码空间"决策)——PH5 self-target 场景下二者会同时存在于同一台机器上,实现时不要混淆,必要时以绝对路径而非相对路径区分 | |
| 71 | +- **源码目录约定(2026-07-30 补充,2026-07-31 订正)**:graphlemon 平台自身的后端源码(U-001 的 schema 实现、U-007 起的 HTTP/WebSocket 后端)落在 graphlemon 仓库根目录的 `src/`。这与**目标项目**仓库自己已有的 `src/` 是两个完全不同的目录,分属两个物理仓库(见架构文档 §13.1"目标项目是独立代码空间"决策)——PH5 self-target 场景下二者会同时存在于同一台机器上,实现时不要混淆,必要时以绝对路径而非相对路径区分。**订正**:目标项目的 `src/` 不是 U-002 生成的——U-002 只在目标项目里创建 `.graphlemon/` 子目录(架构文档 §13.2),目标项目自己的 `src/` 是其原有源码目录,graphlemon 不创建也不规定其结构;development-plan.md 此前 U-002 那行误引用了架构文档 §7.3 的旧目录树,已于 2026-07-31 订正(§7.3 是"graphlemon 只服务自己开发自己"阶段的旧设计,已被 §13 取代,§7.3 里的 `.claude/agents/`、`skills/`、顶层 `runs/` 实际是 graphlemon 平台自己仓库的资产,属于 U-003 范围) | |
| 72 | 72 | |
| 73 | 73 | ## 5.8 self-target 场景治理规则(`self-development-feasibility.md` 缺口 1/3,2026-07-30 回填) |
| 74 | 74 | ... | ... |
docs/sdd/unit_matrix.json
| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | }, |
| 13 | 13 | "units": [ |
| 14 | 14 | {"id": "U-001", "phase": "PH0", "sub_phase": "PH0-S1", "name": "01-M 全部 15 实体物理 schema", "classification": "Enabler", "depends_on": [], "coverage": {"model": ["project", "org_graph_definition", "task", "work_graph_run", "work_node_execution", "artifact", "bug_record", "skill", "skill_iteration", "agent_profile", "budget_guardrail_config", "org_concurrency_config", "execution_event", "execution_log_file", "log_retention_config"], "view": [], "controller": [], "process": [], "requirement": []}, "done": "15 张表全部建表成功,关键约束(唯一/外键/枚举)有单测覆盖"}, |
| 15 | - {"id": "U-002", "phase": "PH0", "sub_phase": "PH0-S1", "name": "目标项目 Git 仓库目录骨架初始化", "classification": "Enabler", "depends_on": ["U-001"], "coverage": {"model": ["project"], "view": [], "controller": ["c_project_create"], "process": [], "requirement": []}, "done": "给定空 git 仓库,能生成完整目录骨架并产生初始化 commit"}, | |
| 15 | + {"id": "U-002", "phase": "PH0", "sub_phase": "PH0-S1", "name": "目标项目 Git 仓库目录骨架初始化(仅 .graphlemon/,见架构文档 §13.2;2026-07-31 订正,此前误引用 §7.3)", "classification": "Enabler", "depends_on": ["U-001"], "coverage": {"model": ["project"], "view": [], "controller": ["c_project_create"], "process": [], "requirement": []}, "done": "给定空 git 仓库,能生成 .graphlemon/ 子目录骨架并产生初始化 commit"}, | |
| 16 | 16 | {"id": "U-003", "phase": "PH0", "sub_phase": "PH0-S1", "name": "graphlemon 平台自身仓库/运行时目录结构", "classification": "Enabler", "depends_on": [], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "平台目录与目标项目目录物理分离,互不干扰"}, |
| 17 | 17 | {"id": "U-004", "phase": "PH0", "sub_phase": "PH0-S2", "name": "Hermes profile 迁移到 .claude/agents/*.md", "classification": "Risk Gate", "depends_on": ["U-002"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": [], "assumption": ["A-2"]}, "done": "claude --agent planner -p 可正常启动且系统提示词正确;skills/memory/tools/model frontmatter 配置完整"}, |
| 18 | 18 | {"id": "U-004.5", "phase": "PH0", "sub_phase": "PH0-S2", "name": "RUN_REAL_AGENT_E2E 门控测试 harness(2026-07-30 新增)", "classification": "Enabler", "depends_on": ["U-004"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "有可复用的 skip_unless_env(\"RUN_REAL_AGENT_E2E\") 装饰器/基类;默认 unittest 跳过真实 claude 子进程调用;显式开启环境变量时至少一个真实往返用例可跑通"}, | ... | ... |
src/project_scaffold.py
0 → 100644
| 1 | +"""c_project_create (03-C §3.0): register/scaffold a target project. | |
| 2 | + | |
| 3 | +U-002 scope: the directory-scaffolding + DB-persistence half of c_project_create -- | |
| 4 | +no HTTP endpoint yet (that's U-007); this is the plain function U-007 will wire up. | |
| 5 | + | |
| 6 | +Target-project directory convention is architecture doc §13.2, NOT §7.3 (see | |
| 7 | +development-plan.md's 2026-07-31 correction: §7.3 describes graphlemon's own | |
| 8 | +platform repo, not a target project). c_project_create only ever creates | |
| 9 | +`.graphlemon/` inside the target folder; a target project's own `src/` and | |
| 10 | +everything else in it is left untouched. | |
| 11 | +""" | |
| 12 | + | |
| 13 | +import os | |
| 14 | +import subprocess | |
| 15 | +import uuid | |
| 16 | + | |
| 17 | +_GIT_IDENTITY = [ | |
| 18 | + "-c", "user.name=graphlemon", | |
| 19 | + "-c", "user.email=graphlemon@localhost", | |
| 20 | +] | |
| 21 | + | |
| 22 | + | |
| 23 | +class PathNotFoundError(Exception): | |
| 24 | + """PATH_NOT_FOUND -- 03-C §3.0: target_folder_path does not exist.""" | |
| 25 | + | |
| 26 | + | |
| 27 | +class PathNotWritableError(Exception): | |
| 28 | + """PATH_NOT_WRITABLE -- 03-C §3.0: no write permission on target_folder_path.""" | |
| 29 | + | |
| 30 | + | |
| 31 | +class GitInitDeclinedError(Exception): | |
| 32 | + """GIT_INIT_DECLINED -- 03-C §3.0: not a git repo and caller didn't ask to init one. | |
| 33 | + | |
| 34 | + 02-V §2.1: the UI must get explicit user confirmation before setting | |
| 35 | + init_git_if_missing=True; this function never silently runs `git init`. | |
| 36 | + """ | |
| 37 | + | |
| 38 | + | |
| 39 | +def _run_git(args, cwd): | |
| 40 | + subprocess.run(["git"] + args, cwd=cwd, check=True, capture_output=True, text=True) | |
| 41 | + | |
| 42 | + | |
| 43 | +def create_project(conn, target_folder_path, name=None, init_git_if_missing=False): | |
| 44 | + """Implements c_project_create (03-C §3.0). | |
| 45 | + | |
| 46 | + Returns {"project_id", "has_git_repo", "graphlemon_subdir_initialized"}. | |
| 47 | + Idempotent: a second call with the same target_folder_path returns the | |
| 48 | + existing row untouched (01-M 1.0 幂等约束) -- no re-validation, no repeat | |
| 49 | + side effects, matching REQ-11's failure-case wording. | |
| 50 | + """ | |
| 51 | + existing = conn.execute( | |
| 52 | + "SELECT project_id, has_git_repo, graphlemon_subdir_initialized " | |
| 53 | + "FROM project WHERE target_folder_path = ?", | |
| 54 | + (target_folder_path,), | |
| 55 | + ).fetchone() | |
| 56 | + if existing is not None: | |
| 57 | + return { | |
| 58 | + "project_id": existing[0], | |
| 59 | + "has_git_repo": bool(existing[1]), | |
| 60 | + "graphlemon_subdir_initialized": bool(existing[2]), | |
| 61 | + } | |
| 62 | + | |
| 63 | + if not os.path.isdir(target_folder_path): | |
| 64 | + raise PathNotFoundError(target_folder_path) | |
| 65 | + if not os.access(target_folder_path, os.W_OK): | |
| 66 | + raise PathNotWritableError(target_folder_path) | |
| 67 | + | |
| 68 | + has_git_repo = os.path.isdir(os.path.join(target_folder_path, ".git")) | |
| 69 | + if not has_git_repo: | |
| 70 | + if not init_git_if_missing: | |
| 71 | + raise GitInitDeclinedError(target_folder_path) | |
| 72 | + _run_git(["init"], cwd=target_folder_path) | |
| 73 | + has_git_repo = True | |
| 74 | + | |
| 75 | + graphlemon_subdir = os.path.join(target_folder_path, ".graphlemon") | |
| 76 | + os.makedirs(graphlemon_subdir, exist_ok=True) | |
| 77 | + # git doesn't track empty directories; this is only so the init commit has | |
| 78 | + # something under .graphlemon/ to record. Real content (runs/<task_id>/...) | |
| 79 | + # arrives once tasks actually execute (01-M 1.5 artifact.file_path). | |
| 80 | + open(os.path.join(graphlemon_subdir, ".gitkeep"), "w").close() | |
| 81 | + | |
| 82 | + _run_git(["add", ".graphlemon"], cwd=target_folder_path) | |
| 83 | + _run_git( | |
| 84 | + _GIT_IDENTITY + ["commit", "-m", "graphlemon: initialize .graphlemon/ workspace"], | |
| 85 | + cwd=target_folder_path, | |
| 86 | + ) | |
| 87 | + | |
| 88 | + project_id = str(uuid.uuid4()) | |
| 89 | + project_name = name or os.path.basename(os.path.normpath(target_folder_path)) | |
| 90 | + conn.execute( | |
| 91 | + "INSERT INTO project " | |
| 92 | + "(project_id, name, target_folder_path, has_git_repo, graphlemon_subdir_initialized, status) " | |
| 93 | + "VALUES (?, ?, ?, ?, 1, 'active')", | |
| 94 | + (project_id, project_name, target_folder_path, int(has_git_repo)), | |
| 95 | + ) | |
| 96 | + conn.commit() | |
| 97 | + | |
| 98 | + return { | |
| 99 | + "project_id": project_id, | |
| 100 | + "has_git_repo": has_git_repo, | |
| 101 | + "graphlemon_subdir_initialized": True, | |
| 102 | + } | ... | ... |
src/test_project_scaffold.py
0 → 100644
| 1 | +"""U-002 tests: c_project_create's directory-scaffolding + DB-persistence logic. | |
| 2 | + | |
| 3 | +Run: python3 -m unittest src.test_project_scaffold -v | |
| 4 | +""" | |
| 5 | + | |
| 6 | +import os | |
| 7 | +import shutil | |
| 8 | +import stat | |
| 9 | +import subprocess | |
| 10 | +import tempfile | |
| 11 | +import unittest | |
| 12 | + | |
| 13 | +from src import project_scaffold, schema | |
| 14 | + | |
| 15 | + | |
| 16 | +def _init_git_repo(path): | |
| 17 | + subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True) | |
| 18 | + | |
| 19 | + | |
| 20 | +class ProjectScaffoldTestBase(unittest.TestCase): | |
| 21 | + def setUp(self): | |
| 22 | + self.conn = schema.connect(":memory:") | |
| 23 | + schema.create_schema(self.conn) | |
| 24 | + self.tmpdir = tempfile.mkdtemp(prefix="graphlemon-u002-") | |
| 25 | + | |
| 26 | + def tearDown(self): | |
| 27 | + self.conn.close() | |
| 28 | + shutil.rmtree(self.tmpdir, ignore_errors=True) | |
| 29 | + | |
| 30 | + | |
| 31 | +class HappyPathTest(ProjectScaffoldTestBase): | |
| 32 | + def test_existing_git_repo_gets_graphlemon_subdir_and_init_commit(self): | |
| 33 | + _init_git_repo(self.tmpdir) | |
| 34 | + | |
| 35 | + result = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 36 | + | |
| 37 | + self.assertTrue(result["has_git_repo"]) | |
| 38 | + self.assertTrue(result["graphlemon_subdir_initialized"]) | |
| 39 | + self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon"))) | |
| 40 | + | |
| 41 | + log = subprocess.run( | |
| 42 | + ["git", "log", "--oneline"], cwd=self.tmpdir, check=True, capture_output=True, text=True | |
| 43 | + ) | |
| 44 | + self.assertIn("graphlemon: initialize .graphlemon/ workspace", log.stdout) | |
| 45 | + | |
| 46 | + row = self.conn.execute( | |
| 47 | + "SELECT project_id, target_folder_path, has_git_repo, graphlemon_subdir_initialized, status " | |
| 48 | + "FROM project WHERE project_id = ?", | |
| 49 | + (result["project_id"],), | |
| 50 | + ).fetchone() | |
| 51 | + self.assertIsNotNone(row) | |
| 52 | + self.assertEqual(row[1], self.tmpdir) | |
| 53 | + self.assertEqual(row[2], 1) | |
| 54 | + self.assertEqual(row[3], 1) | |
| 55 | + self.assertEqual(row[4], "active") | |
| 56 | + | |
| 57 | + def test_project_name_defaults_to_folder_name(self): | |
| 58 | + _init_git_repo(self.tmpdir) | |
| 59 | + result = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 60 | + row = self.conn.execute( | |
| 61 | + "SELECT name FROM project WHERE project_id = ?", (result["project_id"],) | |
| 62 | + ).fetchone() | |
| 63 | + self.assertEqual(row[0], os.path.basename(self.tmpdir)) | |
| 64 | + | |
| 65 | + def test_explicit_name_used_when_given(self): | |
| 66 | + _init_git_repo(self.tmpdir) | |
| 67 | + result = project_scaffold.create_project(self.conn, self.tmpdir, name="Snake Game") | |
| 68 | + row = self.conn.execute( | |
| 69 | + "SELECT name FROM project WHERE project_id = ?", (result["project_id"],) | |
| 70 | + ).fetchone() | |
| 71 | + self.assertEqual(row[0], "Snake Game") | |
| 72 | + | |
| 73 | + | |
| 74 | +class NonGitRepoConfirmationFlowTest(ProjectScaffoldTestBase): | |
| 75 | + """02-V §2.1 / 03-C §3.0: never silently `git init`.""" | |
| 76 | + | |
| 77 | + def test_declines_without_confirmation_and_leaves_folder_untouched(self): | |
| 78 | + with self.assertRaises(project_scaffold.GitInitDeclinedError): | |
| 79 | + project_scaffold.create_project(self.conn, self.tmpdir, init_git_if_missing=False) | |
| 80 | + | |
| 81 | + self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, ".git"))) | |
| 82 | + self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon"))) | |
| 83 | + count = self.conn.execute("SELECT COUNT(*) FROM project").fetchone()[0] | |
| 84 | + self.assertEqual(count, 0) | |
| 85 | + | |
| 86 | + def test_initializes_git_when_explicitly_confirmed(self): | |
| 87 | + result = project_scaffold.create_project(self.conn, self.tmpdir, init_git_if_missing=True) | |
| 88 | + | |
| 89 | + self.assertTrue(result["has_git_repo"]) | |
| 90 | + self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".git"))) | |
| 91 | + self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon"))) | |
| 92 | + | |
| 93 | + | |
| 94 | +class IdempotentCreationTest(ProjectScaffoldTestBase): | |
| 95 | + def test_second_call_returns_existing_record_without_new_commit(self): | |
| 96 | + _init_git_repo(self.tmpdir) | |
| 97 | + first = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 98 | + second = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 99 | + | |
| 100 | + self.assertEqual(first["project_id"], second["project_id"]) | |
| 101 | + count = self.conn.execute("SELECT COUNT(*) FROM project").fetchone()[0] | |
| 102 | + self.assertEqual(count, 1) | |
| 103 | + | |
| 104 | + log = subprocess.run( | |
| 105 | + ["git", "log", "--oneline"], cwd=self.tmpdir, check=True, capture_output=True, text=True | |
| 106 | + ) | |
| 107 | + self.assertEqual(len(log.stdout.strip().splitlines()), 1) | |
| 108 | + | |
| 109 | + def test_idempotent_hit_skips_path_validation(self): | |
| 110 | + # REQ-11 failure case: repeat calls on an already-registered path must | |
| 111 | + # return the existing record even if the path itself is no longer | |
| 112 | + # valid, not re-validate and error. | |
| 113 | + _init_git_repo(self.tmpdir) | |
| 114 | + first = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 115 | + shutil.rmtree(self.tmpdir) | |
| 116 | + | |
| 117 | + second = project_scaffold.create_project(self.conn, self.tmpdir) | |
| 118 | + self.assertEqual(first["project_id"], second["project_id"]) | |
| 119 | + | |
| 120 | + | |
| 121 | +class PathValidationErrorsTest(ProjectScaffoldTestBase): | |
| 122 | + def test_nonexistent_path_raises_path_not_found(self): | |
| 123 | + missing = os.path.join(self.tmpdir, "does-not-exist") | |
| 124 | + with self.assertRaises(project_scaffold.PathNotFoundError): | |
| 125 | + project_scaffold.create_project(self.conn, missing) | |
| 126 | + | |
| 127 | + def test_unwritable_path_raises_path_not_writable(self): | |
| 128 | + os.chmod(self.tmpdir, stat.S_IRUSR | stat.S_IXUSR) | |
| 129 | + try: | |
| 130 | + if os.access(self.tmpdir, os.W_OK): | |
| 131 | + self.skipTest("running as a user that bypasses permission bits (e.g. root)") | |
| 132 | + with self.assertRaises(project_scaffold.PathNotWritableError): | |
| 133 | + project_scaffold.create_project(self.conn, self.tmpdir) | |
| 134 | + finally: | |
| 135 | + os.chmod(self.tmpdir, stat.S_IRWXU) | |
| 136 | + | |
| 137 | + | |
| 138 | +if __name__ == "__main__": | |
| 139 | + unittest.main() | ... | ... |