@@ -233,6 +233,7 @@ def __init__(self, tmp_path, *args, **kwargs):
233233 self .download = kwargs .get ("download" )
234234 if self .download :
235235 self .download = tmp_path / self .download
236+ self .download_dir = tmp_path / kwargs .get ("download_dir" , "_cache" )
236237 self .dry_run = kwargs .get ("dry_run" , True )
237238 self .fallback_source = kwargs .get ("fallback_source" )
238239 self .force = kwargs .get ("force" , True )
@@ -330,7 +331,85 @@ def test_install_from_script(tmp_path, assert_log):
330331 )
331332
332333
333- def test_sanitise_install_urls ():
334+ def test_failed_install_unwind (tmp_path , monkeypatch , assert_log ):
335+ cmd = InstallCommandTestCmd (tmp_path , "1.0" , force = False )
336+ cmd .dry_run = False
337+ cmd .preserve_site_on_upgrade = True
338+
339+ inst = cmd .installs [0 ]
340+ inst .setdefault ("shortcuts" , []).append ({
341+ "kind" : "site-dirs" , "dirs" : ["test-site" ],
342+ })
343+
344+ target = tmp_path / "target"
345+ test_file = target / "test-site/file.txt"
346+ test_file .parent .mkdir (parents = True , exist_ok = True )
347+ test_file .write_text ("Before" )
348+
349+ def remove_existing (* args ):
350+ pass
351+
352+ def download_one (* args , ** kwargs ):
353+ return "<package>"
354+
355+ def extract_package (package , dest , * args , ** kwargs ):
356+ # site dir should be gone
357+ assert not test_file .is_file ()
358+ # create the target directory
359+ dest .mkdir (parents = True , exist_ok = True )
360+ # interrupt the install process
361+ raise RuntimeError ("Failed to extract for test reasons" )
362+
363+ monkeypatch .setattr (IC , "_remove_existing" , remove_existing )
364+ monkeypatch .setattr (IC , "_download_one" , download_one )
365+ monkeypatch .setattr (IC , "extract_package" , extract_package )
366+
367+ with pytest .raises (RuntimeError ):
368+ IC ._install_one (cmd , "<source>" , inst , target = target )
369+
370+ # site dir should be back
371+ assert test_file .is_file ()
372+ assert test_file .read_text () == "Before"
373+
374+
375+ def test_failed_install_unwind_dont_clobber (tmp_path , monkeypatch , assert_log ):
376+ cmd = InstallCommandTestCmd (tmp_path , "1.0" , force = False )
377+ cmd .dry_run = False
378+ cmd .preserve_site_on_upgrade = True
379+
380+ inst = cmd .installs [0 ]
381+ inst .setdefault ("shortcuts" , []).append ({
382+ "kind" : "site-dirs" , "dirs" : ["test-site" ],
383+ })
384+
385+ target = tmp_path / "test-site"
386+ test_file = target / "file.txt"
387+ test_file .parent .mkdir (parents = True , exist_ok = True )
388+ test_file .write_text ("Before" )
389+
390+ def download_one (* args , ** kwargs ):
391+ return "<package>"
392+
393+ def extract_package (package , dest , * args , ** kwargs ):
394+ # site dir should be gone
395+ assert not test_file .is_file ()
396+ # create a new one - it should be preserved
397+ test_file .parent .mkdir (parents = True , exist_ok = True )
398+ test_file .write_text ("After" )
399+ # interrupt the install process
400+ raise RuntimeError ("Failed to extract for test reasons" )
401+
402+ monkeypatch .setattr (IC , "_download_one" , download_one )
403+ monkeypatch .setattr (IC , "extract_package" , extract_package )
404+
405+ with pytest .raises (RuntimeError ):
406+ IC ._install_one (cmd , "<source>" , inst , target = target )
407+
408+ # Ensure file we extracted is still there
409+ assert test_file .read_text () == "After"
410+
411+
412+ def test_finalize_metadata_urls ():
334413 class Cmd :
335414 enable_shortcut_kinds = []
336415 disable_shortcut_kinds = []
@@ -341,13 +420,13 @@ class Cmd:
341420 "source" : "http://user:placeholder@example.com/index.json" ,
342421 }
343422
344- IC ._sanitise_install (Cmd , i )
423+ IC ._finalize_metadata (Cmd , i )
345424
346425 assert i ["url" ] == "http://example.com/package.zip"
347426 assert i ["source" ] == "http://example.com/index.json"
348427
349428
350- def test_sanitise_install_fallback_urls ():
429+ def test_finalize_metadata_fallback_urls ():
351430 class Cmd :
352431 enable_shortcut_kinds = []
353432 disable_shortcut_kinds = []
@@ -358,13 +437,13 @@ class Cmd:
358437 "source" : "http://user:placeholder@example.com/index.json" ,
359438 }
360439
361- IC ._sanitise_install (Cmd , i )
440+ IC ._finalize_metadata (Cmd , i )
362441
363442 assert i ["url" ] == "http://example.com/package.zip"
364443 assert i ["source" ] == "http://user:placeholder@example.com/index.json"
365444
366445
367- def test_sanitise_install_shortcuts ():
446+ def test_finalize_metadata_shortcuts ():
368447 class Cmd :
369448 enable_shortcut_kinds = []
370449 disable_shortcut_kinds = []
@@ -375,13 +454,13 @@ class Cmd:
375454 "shortcuts" : [dict (kind = a ) for a in "abc" ],
376455 }
377456
378- IC ._sanitise_install (Cmd , i )
457+ IC ._finalize_metadata (Cmd , i )
379458
380459 assert [j ["kind" ] for j in i ["shortcuts" ]] == ["a" , "b" , "c" ]
381460 assert [j ["kind" ] for j in i ["__original-shortcuts" ]] == ["a" , "b" , "c" ]
382461
383462
384- def test_sanitise_install_shortcuts_disable ():
463+ def test_finalize_metadata_shortcuts_disable ():
385464 class Cmd :
386465 enable_shortcut_kinds = []
387466 disable_shortcut_kinds = ["b" ]
@@ -392,13 +471,13 @@ class Cmd:
392471 "shortcuts" : [dict (kind = a ) for a in "abc" ],
393472 }
394473
395- IC ._sanitise_install (Cmd , i )
474+ IC ._finalize_metadata (Cmd , i )
396475
397476 assert [j ["kind" ] for j in i ["shortcuts" ]] == ["a" , "c" ]
398477 assert [j ["kind" ] for j in i ["__original-shortcuts" ]] == ["a" , "b" , "c" ]
399478
400479
401- def test_sanitise_install_shortcuts_enable ():
480+ def test_finalize_metadata_shortcuts_enable ():
402481 class Cmd :
403482 enable_shortcut_kinds = ["b" ]
404483 disable_shortcut_kinds = []
@@ -409,7 +488,32 @@ class Cmd:
409488 "shortcuts" : [dict (kind = a ) for a in "abc" ],
410489 }
411490
412- IC ._sanitise_install (Cmd , i )
491+ IC ._finalize_metadata (Cmd , i )
413492
414493 assert [j ["kind" ] for j in i ["shortcuts" ]] == ["b" ]
415494 assert [j ["kind" ] for j in i ["__original-shortcuts" ]] == ["a" , "b" , "c" ]
495+
496+
497+ def test_finalize_metadata_merge_from (tmp_path ):
498+ class Cmd :
499+ enable_shortcut_kinds = []
500+ disable_shortcut_kinds = []
501+ fallback_source = None
502+
503+ merge_from = tmp_path / "file.json"
504+ test_url_1 = "https://example.com/"
505+ test_url_2 = "https://example.com/path2"
506+
507+ # merge_from does not exist, but we shouldn't fail
508+ i = {"url" : test_url_1 }
509+ IC ._finalize_metadata (Cmd , i , merge_from )
510+ assert i ["url" ] == test_url_1
511+
512+ # Update missing fields from merge_from, but don't touch existing ones
513+ with open (merge_from , "w" , encoding = "utf-8" ) as f :
514+ json .dump ({"url" : test_url_1 , "data1" : "b" , "data2" : "c" }, f )
515+ i = {"url" : test_url_2 , "data1" : "a" }
516+ IC ._finalize_metadata (Cmd , i , merge_from )
517+ assert i ["url" ] == test_url_2
518+ assert i ["data1" ] == "a"
519+ assert i ["data2" ] == "c"
0 commit comments