okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""Workspace directory and preview resources preserve real filesystem and VCS facts. 2 3What the picker shows comes from real directories, git and jj run by the 4daemon with its own environment, so the checks are end to end: a plain tree, 5a git repository (branch, changes, detached HEAD), a colocated jj repository 6(jj wins, bookmarks, and browsing never writes a jj operation), linguist 7shares, and path validation. The jj cases skip when jj is not installed. 8""" 9 10from datetime import datetime 11import json 12import os 13from pathlib import Path 14import shutil 15import subprocess 16import tempfile 17import unittest 18import urllib .error 19import urllib .parse 20 21from harness import Albedo ,exclusive 22 23GIT_ENV = { 24"GIT_AUTHOR_NAME" :"fixture" , 25"GIT_AUTHOR_EMAIL" :"fixture@example.com" , 26"GIT_COMMITTER_NAME" :"fixture" , 27"GIT_COMMITTER_EMAIL" :"fixture@example.com" , 28"JJ_USER" :"fixture" , 29"JJ_EMAIL" :"fixture@example.com" , 30} 31 32 33def sh (cwd ,* args ): 34return subprocess .run ( 35args , 36cwd = cwd , 37env = dict (os .environ ,** GIT_ENV ), 38check = True , 39capture_output = True , 40text = True , 41 ).stdout 42 43 44def write (path ,text ): 45path .parent .mkdir (parents = True ,exist_ok = True ) 46path .write_text (text ) 47 48 49class FoldersTest (unittest .TestCase ): 50def setUp (self ): 51self .app = Albedo ().__enter__ () 52self .addCleanup (self .app .__exit__ ,None ,None ,None ) 53directory = ( 54Path ("/var/tmp" ) 55if self ._testMethodName 56== "test_plain_tree_lists_directories_and_previews_two_levels" 57else self .app .root 58 ) 59self .root = Path (tempfile .mkdtemp (prefix = "albedo-folders-" ,dir = directory )) 60self .addCleanup (shutil .rmtree ,self .root ,True ) 61 62def get (self ,location ,* ,preview = False ): 63query = {"location" :str (location )} 64if preview : 65query ["include" ]= "preview" 66with self .app .api ("/workspaces?" + urllib .parse .urlencode (query ))as response : 67return json .load (response ) 68 69def failure (self ,location ,* ,preview = False ): 70with self .assertRaises (urllib .error .HTTPError )as caught : 71self .get (location ,preview = preview ) 72return caught .exception .code ,json .load (caught .exception ) 73 74@exclusive 75def test_plain_tree_lists_directories_and_previews_two_levels (self ): 76for name in ("beta" ,"Alpha" ,".hidden" ,"gamma/one" ,"gamma/two" ): 77 (self .root / name ).mkdir (parents = True ) 78for name in "abcdef" : 79 (self .root / "gamma" / f" { name } .py" ).write_text ("x = 1\n" ) 80 (self .root / "notes.txt" ).write_text ("not a directory\n" ) 81 (self .root / "zeta" ).symlink_to (self .root / "beta" ) 82 83listed = self .get (f" { self . root } /./gamma/../" ) 84self .assertEqual (listed ["directory" ],str (self .root )) 85self .assertIsNone (listed ["next" ]) 86self .assertEqual ( 87 [(e ["name" ],e ["hidden" ],e ["vcs" ])for e in listed ["items" ]], 88 [ 89 (".hidden" ,True ,None ), 90 ("Alpha" ,False ,None ), 91 ("beta" ,False ,None ), 92 ("gamma" ,False ,None ), 93 ("zeta" ,False ,None ), 94 ], 95 ) 96modified = datetime .fromisoformat ( 97listed ["items" ][1 ]["modified_at" ].replace ("Z" ,"+00:00" ) 98 ) 99self .assertAlmostEqual ( 100modified .timestamp (), (self .root / "Alpha" ).stat ().st_mtime ,delta = 1 101 ) 102 103preview = self .get (self .root ,preview = True )["preview" ] 104self .assertIsNone (preview ["repository" ]) 105self .assertEqual (preview ["languages" ], []) 106self .assertEqual (preview ["more" ],0 ) 107self .assertEqual ( 108 [(e ["name" ],e ["kind" ])for e in preview ["tree" ]], 109 [ 110 ("Alpha" ,"directory" ), 111 ("beta" ,"directory" ), 112 ("gamma" ,"directory" ), 113 ("zeta" ,"symlink" ), 114 ("notes.txt" ,"file" ), 115 ], 116 ) 117gamma = preview ["tree" ][2 ] 118self .assertEqual ( 119 [c ["name" ]for c in gamma ["children" ]], ["one" ,"two" ,"a.py" ,"b.py" ] 120 ) 121self .assertEqual (gamma ["more" ],4 ) 122self .assertNotIn ("children" ,gamma ["children" ][0 ]) 123self .assertEqual (gamma ["children" ][2 ]["language" ],"Python" ) 124self .assertEqual (preview ["tree" ][4 ]["language" ],"Text" ) 125 126def test_git_repository_reports_branch_changes_and_detached_head (self ): 127repo = self .root / "project" 128write (repo / "src" / "main.go" ,"package main\n" * 30 ) 129write (repo / "src" / "util.go" ,"package main\n" * 30 ) 130write (repo / "script.py" ,"print(1)\n" * 10 ) 131write (repo / "data.json" ,"{}\n" * 500 ) 132write (repo / ".gitignore" ,"build/\n" ) 133sh (repo ,"git" ,"init" ,"-q" ,"-b" ,"trunk" ) 134sh (repo ,"git" ,"add" ,"." ) 135sh (repo ,"git" ,"commit" ,"-q" ,"-m" ,"one" ) 136write (repo / "src" / "main.go" ,"package main // edited\n" ) 137write (repo / "src" / "new.go" ,"package main\n" ) 138write (repo / "build" / "out.o" ,"ignored\n" ) 139commit = sh (repo ,"git" ,"rev-parse" ,"--short" ,"HEAD" ).strip () 140 141self .assertEqual (self .get (self .root )["items" ][0 ]["vcs" ],"git" ) 142found = self .get (repo / "src" ,preview = True )["preview" ]["repository" ] 143self .assertEqual ( 144 { 145key :found [key ] 146for key in ("kind" ,"root" ,"branch" ,"revision" ,"changed" ) 147 }, 148 { 149"kind" :"git" , 150"root" :str (repo ), 151"branch" :"trunk" , 152"revision" :commit , 153"changed" :2 , 154 }, 155 ) 156 157preview = self .get (repo ,preview = True )["preview" ] 158self .assertEqual (preview ["repository" ],found ) 159# JSON is data, so only Go and Python count, largest first. 160self .assertEqual ( 161 [language ["name" ]for language in preview ["languages" ]], ["Go" ,"Python" ] 162 ) 163self .assertAlmostEqual ( 164sum (language ["share" ]for language in preview ["languages" ]),1.0 165 ) 166tree = {e ["name" ]:e for e in preview ["tree" ]} 167# Inside a repository only tracked or changed paths show: the 168# ignored build/ stays out, the untracked src/new.go is there. 169self .assertEqual (list (tree ), ["src" ,"data.json" ,"script.py" ]) 170self .assertEqual (tree ["src" ]["changed" ],2 ) 171self .assertEqual ( 172 {c ["name" ]:c ["changed" ]for c in tree ["src" ]["children" ]}, 173 {"main.go" :1 ,"new.go" :1 ,"util.go" :0 }, 174 ) 175self .assertEqual (tree ["script.py" ]["changed" ],0 ) 176 177sh (repo ,"git" ,"checkout" ,"-q" ,"--detach" ) 178self .assertIsNone ( 179self .get (repo ,preview = True )["preview" ]["repository" ]["branch" ] 180 ) 181 182def test_unborn_git_repository_has_no_commit (self ): 183repo = self .root / "fresh" 184repo .mkdir () 185sh (repo ,"git" ,"init" ,"-q" ,"-b" ,"main" ) 186 (repo / "draft.md" ).write_text ("# draft\n" ) 187found = self .get (repo ,preview = True )["preview" ]["repository" ] 188self .assertEqual ( 189 { 190key :found [key ] 191for key in ( 192"kind" , 193"root" , 194"branch" , 195"revision" , 196"changed" , 197"touched_at" , 198 ) 199 }, 200 { 201"kind" :"git" , 202"root" :str (repo ), 203"branch" :"main" , 204"revision" :None , 205"changed" :1 , 206"touched_at" :None , 207 }, 208 ) 209 210@ unittest . skipUnless ( shutil . which ( "jj" ), "jj is not installed" ) 211def test_colocated_jj_repository_wins_without_writing_an_operation (self ): 212repo = self .root / "colocated" 213repo .mkdir () 214sh (repo ,"jj" ,"git" ,"init" ,"--colocate" ) 215write (repo / "lib" / "core.gleam" ,"pub fn main() { Nil }\n" ) 216sh (repo ,"jj" ,"commit" ,"-m" ,"one" ) 217sh (repo ,"jj" ,"bookmark" ,"create" ,"main" ,"-r" ,"@-" ) 218write (repo / "lib" / "more.gleam" ,"pub fn more() { Nil }\n" ) 219sh (repo ,"jj" ,"commit" ,"-m" ,"two" ) 220write (repo / "lib" / "core.gleam" ,"pub fn main() { 1 }\n" ) 221sh (repo ,"jj" ,"status" ) 222change = sh ( 223repo ,"jj" ,"log" ,"-r" ,"@" ,"--no-graph" ,"-T" ,"change_id.shortest(4)" 224 ).strip () 225# Not yet snapshotted, so jj does not know it and the tree leaves it 226# out; a browse that snapshotted would record an operation. 227write (repo / "lib" / "later.gleam" ,"pub fn later() { Nil }\n" ) 228operations = self .operations (repo ) 229 230self .assertEqual (self .get (self .root )["items" ][0 ]["vcs" ],"jj" ) 231found = self .get (repo / "lib" ,preview = True )["preview" ]["repository" ] 232self .assertEqual (found ["kind" ],"jj" ) 233self .assertEqual (found ["root" ],str (repo )) 234self .assertEqual (found ["change_id" ],change ) 235self .assertEqual (found ["bookmark" ], {"name" :"main" ,"ahead" :2 }) 236self .assertEqual (found ["changed" ],1 ) 237self .assertIsInstance (found ["touched_at" ],str ) 238datetime .fromisoformat (found ["touched_at" ].replace ("Z" ,"+00:00" )) 239 240preview = self .get (repo ,preview = True )["preview" ] 241self .assertEqual (preview ["repository" ],found ) 242self .assertEqual ( 243 [ 244 (language ["name" ],language ["share" ]) 245for language in preview ["languages" ] 246 ], 247 [("Gleam" ,1.0 )], 248 ) 249lib = preview ["tree" ][0 ] 250self .assertEqual ((lib ["name" ],lib ["changed" ]), ("lib" ,1 )) 251self .assertEqual ( 252 {c ["name" ]:c ["changed" ]for c in lib ["children" ]}, 253 {"core.gleam" :1 ,"more.gleam" :0 }, 254 ) 255 256self .assertEqual (self .operations (repo ),operations ) 257 258# A bookmark that moved on without @ still names @'s line of work, 259# rather than an older backup behind @. 260def log (rev ): 261return sh ( 262repo ,"jj" ,"log" ,"-r" ,rev ,"--no-graph" ,"-T" ,"change_id" 263 ).strip () 264 265two ,one = log ("@-" ),log ("@--" ) 266sh (repo ,"jj" ,"bookmark" ,"create" ,"backup/old" ,"-r" ,one ) 267sh (repo ,"jj" ,"new" ,two ,"-m" ,"side" ) 268sh (repo ,"jj" ,"bookmark" ,"set" ,"main" ,"-r" ,"@" ) 269sh (repo ,"jj" ,"edit" ,change ) 270self .assertEqual ( 271self .get (repo ,preview = True )["preview" ]["repository" ]["bookmark" ], 272 {"name" :"main" ,"ahead" :1 }, 273 ) 274 275@staticmethod 276def operations (repo ): 277return sh ( 278repo , 279"jj" , 280"op" , 281"log" , 282"--no-graph" , 283"--ignore-working-copy" , 284"-T" , 285'id ++ "\\n"' , 286 ) 287 288def test_paths_expand_home_and_refuse_the_rest (self ): 289home = self .app .root / "user-home" 290listed = self .get ("~" ) 291self .assertEqual ((listed ["directory" ],listed ["home" ]), (str (home ),str (home ))) 292self .assertEqual (self .get ("~/" )["directory" ],str (home )) 293 294 (self .root / "file.txt" ).write_text ("x\n" ) 295for preview in (False ,True ): 296self .assertEqual (self .failure ("relative/dir" ,preview = preview )[0 ],400 ) 297self .assertEqual (self .failure ("~someone" ,preview = preview )[0 ],400 ) 298self .assertEqual (self .failure ("" ,preview = preview )[0 ],400 ) 299code ,body = self .failure (self .root / "missing" ,preview = preview ) 300self .assertEqual (code ,404 ) 301self .assertEqual (body ["status" ],404 ) 302self .assertEqual ( 303self .failure (self .root / "file.txt" ,preview = preview )[0 ],404 304 )