okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
c9cb3a0
main
1"""Unit tests for the Google Colab tool plugin (albedo_plugins.colab).""" 2 3from __future__import annotations 4 5import asyncio 6import json 7import os 8from pathlib import Path 9import sys 10import tempfile 11import unittest 12from unittest .mock import AsyncMock ,patch 13 14ROOT = Path (__file__ ).resolve ().parents [2 ] 15sys .path .insert (0 ,str (ROOT / "priv" / "python" )) 16sys .path .insert (0 ,str (ROOT / "test" )) 17 18import albedo_ssh # noqa: E402 19from albedo_api import PythonApi # noqa: E402 20from albedo_capture import Capture # noqa: E402 21from albedo_plugins import colab # noqa: E402 22import scratch # noqa: E402 23 24scratch .claim ("harness" ) 25 26REAL_ACCOUNT = colab ._account 27ACCOUNT = {"access" :"test-token" ,"email" :"user@example.com" } 28 29 30def instance (** fields )-> colab .ColabInstance : 31return colab .ColabInstance ( 32 { 33"id" :"gpu-t4-1" , 34"endpoint" :"gpu-t4-1" , 35"name" :"dev-t4" , 36"variant" :"GPU" , 37"accelerator" :"T4" , 38"shape" :"standard" , 39"status" :"active" , 40"url" :None , 41"token" :None , 42"token_expires_in" :None , 43"raw" : {}, 44** fields , 45 } 46 ) 47 48 49class ColabPluginTest (unittest .IsolatedAsyncioTestCase ): 50def setUp (self ): 51self .home = tempfile .mkdtemp (prefix = "albedo-colab-home-" ) 52 (Path (self .home )/ ".ssh" ).mkdir () 53env = patch .dict (os .environ , {"HOME" :self .home }) 54env .start () 55self .addCleanup (env .stop ) 56account = patch .object (colab ,"_account" ,AsyncMock (return_value = ACCOUNT )) 57account .start () 58self .addCleanup (account .stop ) 59self .addCleanup (albedo_ssh .ALIASES .clear ) 60 61async def test_account_comes_from_the_daemon_and_its_refusal_is_a_colab_error (self ): 62class Refused (Exception ): 63pass 64 65calls = [] 66 67async def host (method ,args ): 68calls .append (method ) 69raise Refused ("Google Colab is not authenticated; run /login" ) 70 71with ( 72patch .object (colab ,"_account" ,REAL_ACCOUNT ), 73patch .multiple (colab ,host_call = host ,HostErrorType = Refused ), 74 ): 75with self .assertRaises (colab .ColabError )as cm : 76await colab ._account () 77self .assertEqual (calls , ["colab.account" ]) 78self .assertIn ("not authenticated" ,str (cm .exception )) 79 80async def test_instances_query (self ): 81body = ( 82b")]}'\n" 83+ json .dumps ( 84 { 85"assignments" : [ 86 { 87"endpoint" :"gpu-t4-s-12345" , 88"name" :"colab-t4" , 89"variant" :"GPU" , 90"accelerator" :"T4" , 91"shape" :"standard" , 92"status" :"active" , 93"runtimeProxyInfo" : { 94"url" :"https://vm1.colab.googleusercontent.com" , 95"token" :"proxy-tok-123" , 96"tokenExpiresInSeconds" :3600 , 97 }, 98 } 99 ] 100 } 101 ).encode () 102 ) 103with patch .object (colab ,"_send" ,return_value = (200 ,body ))as send : 104found = await colab .instances () 105self .assertEqual (len (found ),1 ) 106inst = found [0 ] 107self .assertEqual (inst .id ,"gpu-t4-s-12345" ) 108self .assertEqual (inst .gpu ,"T4" ) 109self .assertEqual (inst .url ,"https://vm1.colab.googleusercontent.com" ) 110self .assertEqual (inst .token ,"proxy-tok-123" ) 111method ,url ,headers ,_ ,_ = send .call_args [0 ] 112self .assertEqual (method ,"GET" ) 113self .assertIn ("/tun/m/assignments?authuser=0" ,url ) 114self .assertEqual (headers ["Authorization" ],"Bearer test-token" ) 115self .assertEqual (headers ["X-Colab-Tunnel" ],"Google" ) 116 117async def test_status_summary_and_single_lookup (self ): 118with patch .object (colab ,"instances" ,AsyncMock (return_value = [instance ()])): 119overall = await colab .status () 120self .assertTrue (overall .authenticated ) 121self .assertEqual (overall ."user@example.com" ) 122self .assertEqual (overall .active ,1 ) 123self .assertIn ("gpu-t4-1" ,str (overall )) 124self .assertEqual ((await colab .status ("gpu-t4-1" )).id ,"gpu-t4-1" ) 125self .assertEqual ((await colab .status ("dev-t4" )).id ,"gpu-t4-1" ) 126with self .assertRaises (colab .ColabError ): 127await colab .status ("missing-instance" ) 128 129def test_notebook_hash (self ): 130nbh = colab ._notebook_hash () 131self .assertEqual (len (nbh ),44 ) 132self .assertNotIn ("-" ,nbh ) 133self .assertTrue (nbh .endswith ("." )) 134 135async def test_launch_provisions_with_the_xsrf_token (self ): 136xsrf = b')]}\'\n{"token": "xsrf-token-123"}' 137assigned = json .dumps ( 138 { 139"assignment" : { 140"endpoint" :"gpu-t4-assigned-999" , 141"variant" :"GPU" , 142"accelerator" :"T4" , 143"shape" :"highmem" , 144"runtimeProxyInfo" : {"url" :"https://vm" ,"token" :"tok-t4" }, 145 } 146 } 147 ).encode () 148with patch .object ( 149colab ,"_send" ,side_effect = [(200 ,xsrf ), (200 ,assigned )] 150 )as send : 151inst = await colab .launch (gpu = "T4" ,mem = "high" ) 152self .assertEqual (inst .id ,"gpu-t4-assigned-999" ) 153self .assertEqual (inst .mem ,"high" ) 154 (m1 ,u1 ,_ ,b1 ,_ ), (m2 ,u2 ,h2 ,b2 ,_ )= (c [0 ]for c in send .call_args_list ) 155self .assertEqual (m1 ,"GET" ) 156for part in ( 157"/tun/m/assign" , 158"authuser=0" , 159"variant=GPU" , 160"accelerator=T4" , 161"shape=hm" , 162 ): 163self .assertIn (part ,u1 ) 164self .assertIsNone (b1 ) 165self .assertEqual (m2 ,"POST" ) 166self .assertIn ("/tun/m/assign" ,u2 ) 167self .assertEqual (h2 ["X-Goog-Colab-Token" ],"xsrf-token-123" ) 168self .assertIsNone (b2 ) 169 170async def test_launch_returns_an_existing_assignment (self ): 171existing = json .dumps ( 172 { 173"endpoint" :"gpu-t4-existing-111" , 174"variant" :"GPU" , 175"accelerator" :"T4" , 176"runtimeProxyInfo" : {"url" :"https://vm" ,"token" :"tok" }, 177 } 178 ).encode () 179with patch .object (colab ,"_send" ,return_value = (200 ,existing ))as send : 180inst = await colab .launch (gpu = "T4" ) 181self .assertEqual (inst .id ,"gpu-t4-existing-111" ) 182self .assertEqual (send .call_count ,1 ) 183 184async def test_launch_capacity_exhausted (self ): 185with patch .object (colab ,"_send" ,return_value = (503 ,b"Resource exhausted" )): 186with self .assertRaises (colab .ColabError )as cm : 187await colab .launch (gpu = "A100" ) 188self .assertIn ("capacity exhausted" ,str (cm .exception )) 189self .assertEqual (cm .exception .status ,503 ) 190 191async def test_stop_instance_and_already_stopped (self ): 192xsrf = b')]}\'\n{"token": "unassign-xsrf-456"}' 193with patch .object ( 194colab ,"_send" ,side_effect = [(200 ,xsrf ), (204 ,b"" )] 195 )as send : 196result = await colab .stop ("gpu-t4-s-12345" ) 197self .assertTrue (result .success ) 198 (m1 ,u1 ,_ ,_ ,_ ), (m2 ,u2 ,h2 ,_ ,_ )= (c [0 ]for c in send .call_args_list ) 199self .assertEqual ((m1 ,m2 ), ("GET" ,"POST" )) 200for url in (u1 ,u2 ): 201self .assertIn ("/tun/m/unassign/gpu-t4-s-12345" ,url ) 202self .assertEqual (h2 ["X-Goog-Colab-Token" ],"unassign-xsrf-456" ) 203 204with patch .object (colab ,"_send" ,return_value = (404 ,b"Not Found" )): 205result = await colab .stop ("already-stopped" ) 206self .assertFalse (result .success ) 207self .assertIn ("not found or already stopped" ,str (result .message )) 208 209async def test_connect_names_the_host_in_memory_and_leaves_ssh_config_alone (self ): 210ssh = Path (self .home )/ ".ssh" 211 (ssh / "id_ed25519.pub" ).write_text ("ssh-ed25519 AAAA test@box\n" ) 212live = instance ( 213url = "https://vm.colab.googleusercontent.com" ,token = "proxy tok/1" 214 ) 215reached = AsyncMock (return_value = "connection" ) 216with ( 217patch .object (colab ,"instances" ,AsyncMock (return_value = [live ])), 218patch .object (colab .remote ,"connect" ,reached ), 219 ): 220result = await live .connect (remote_cwd = "/content/project" ) 221 222self .assertEqual (result ,"connection" ) 223reached .assert_awaited_once_with ( 224"colab-gpu-t4-1" , 225remote_cwd = "/content/project" , 226python = "/usr/bin/python3" , 227modules = None , 228timeout = 60.0 , 229 ) 230self .assertEqual (list (ssh .iterdir ()), [ssh / "id_ed25519.pub" ]) 231 232argv = albedo_ssh .base ("colab-gpu-t4-1" ) 233options = [argv [i + 1 ]for i ,arg in enumerate (argv )if arg == "-o" ] 234self .assertTrue (options [0 ].startswith ("ProxyCommand=" )) 235self .assertIn ("-m albedo_plugins.ws" ,options [0 ]) 236self .assertLess ( 237options .index ("StrictHostKeyChecking=no" ), 238options .index ("StrictHostKeyChecking=accept-new" ), 239 ) 240self .assertIn ("User=root" ,options ) 241 242environment = albedo_ssh .env ("colab-gpu-t4-1" ) 243self .assertEqual ( 244environment ["ALBEDO_WS_URL" ], 245"wss://vm.colab.googleusercontent.com/colab/ssh?colab-runtime-proxy-token=proxy+tok%2F1" , 246 ) 247self .assertEqual ( 248json .loads (environment ["ALBEDO_WS_HEADERS" ]), 249 {"X-Colab-Ssh-Pubkey" :"ssh-ed25519 AAAA test@box" }, 250 ) 251self .assertNotIn ("wss://" ," " .join (argv )) 252self .assertNotIn ("proxy+tok" ," " .join (argv )) 253self .assertNotIn ("ALBEDO_WS_URL" ,albedo_ssh .env ("other-host" )) 254 255with patch .object (colab ,"_send" ,return_value = (404 ,b"" )): 256await colab .stop (live ) 257self .assertNotIn ("colab-gpu-t4-1" ,albedo_ssh .ALIASES ) 258 259async def test_connect_needs_a_runtime_proxy_and_a_public_key (self ): 260waiting = instance () 261with patch .object (colab ,"instances" ,AsyncMock (return_value = [waiting ])): 262with self .assertRaises (colab .ColabError )as cm : 263await colab .connect ("gpu-t4-1" ) 264self .assertIn ("no runtime proxy" ,str (cm .exception )) 265 266ready = instance (url = "https://vm" ,token = "t" ) 267with patch .object (colab ,"instances" ,AsyncMock (return_value = [ready ])): 268with self .assertRaises (colab .ColabError )as cm : 269await colab .connect ("gpu-t4-1" ) 270self .assertIn ("no ssh public key" ,str (cm .exception )) 271self .assertEqual (albedo_ssh .ALIASES , {}) 272 273def test_setup_exports (self ): 274loop = asyncio .new_event_loop () 275api = PythonApi ( 276loop , 277lambda m ,a :asyncio .sleep (0 ), 278RuntimeError , 279Capture , 28064 * 1024 , 281lambda e :None , 282lambda c :None , 283lambda cls :None , 284 ) 285with patch .multiple ( 286colab ,host_call = colab .host_call ,HostErrorType = colab .HostErrorType 287 ): 288exports = colab .setup (api ) 289self .assertIs (colab .HostErrorType ,RuntimeError ) 290loop .close () 291self .assertEqual ( 292sorted (exports ), ["ColabError" ,"ColabInstance" ,"ColabStatus" ,"colab" ] 293 ) 294self .assertIs (exports ["colab" ],colab .colab )