@@ -10725,6 +10725,110 @@ fn url_hash_mismatch() -> Result<()> {
1072510725 Ok ( ( ) )
1072610726}
1072710727
10728+ #[ tokio:: test]
10729+ async fn concurrent_url_source_locks_leave_a_usable_cache ( ) -> Result < ( ) > {
10730+ use std:: time:: Duration ;
10731+
10732+ use wiremock:: {
10733+ Mock , MockServer , ResponseTemplate ,
10734+ matchers:: { method, path} ,
10735+ } ;
10736+
10737+ let context1 = uv_test:: test_context!( "3.12" ) ;
10738+ let context2 = uv_test:: test_context!( "3.12" ) ;
10739+ let context3 = uv_test:: test_context!( "3.12" ) ;
10740+
10741+ let server = MockServer :: start ( ) . await ;
10742+ let archive_path = context1
10743+ . workspace_root
10744+ . join ( "test/links/tqdm-999.0.0.tar.gz" ) ;
10745+ let archive_bytes = fs_err:: read ( & archive_path) ?;
10746+ let archive_url = format ! ( "{}/files/tqdm-999.0.0.tar.gz" , server. uri( ) ) ;
10747+ let shared_cache = context1. temp_dir . child ( "shared-cache" ) ;
10748+ fs_err:: create_dir_all ( shared_cache. path ( ) ) ?;
10749+
10750+ Mock :: given ( method ( "GET" ) )
10751+ . and ( path ( "/files/tqdm-999.0.0.tar.gz" ) )
10752+ . respond_with (
10753+ ResponseTemplate :: new ( 200 )
10754+ . set_delay ( Duration :: from_secs ( 1 ) )
10755+ . set_body_bytes ( archive_bytes) ,
10756+ )
10757+ . mount ( & server)
10758+ . await ;
10759+
10760+ let write_pyproject = |context : & TestContext | -> Result < ( ) > {
10761+ context
10762+ . temp_dir
10763+ . child ( "pyproject.toml" )
10764+ . write_str ( & formatdoc ! { r#"
10765+ [project]
10766+ name = "project"
10767+ version = "0.1.0"
10768+ requires-python = ">=3.12"
10769+ dependencies = ["tqdm @ {archive_url}"]
10770+ "# ,
10771+ } ) ?;
10772+ Ok ( ( ) )
10773+ } ;
10774+
10775+ write_pyproject ( & context1) ?;
10776+ write_pyproject ( & context2) ?;
10777+ write_pyproject ( & context3) ?;
10778+
10779+ let lock_command = |context : & TestContext | {
10780+ let mut command = std:: process:: Command :: new ( uv_test:: get_bin!( ) ) ;
10781+ command
10782+ . arg ( "lock" )
10783+ . arg ( "--cache-dir" )
10784+ . arg ( shared_cache. path ( ) )
10785+ . current_dir ( context. temp_dir . path ( ) ) ;
10786+ context. add_shared_env ( & mut command, false ) ;
10787+ command
10788+ } ;
10789+
10790+ let child1 = lock_command ( & context1) . spawn ( ) ?;
10791+ let child2 = lock_command ( & context2) . spawn ( ) ?;
10792+
10793+ let output1 = child1. wait_with_output ( ) ?;
10794+ let output2 = child2. wait_with_output ( ) ?;
10795+
10796+ assert ! (
10797+ output1. status. success( ) ,
10798+ "first `uv lock` failed\n stdout:\n {}\n stderr:\n {}" ,
10799+ String :: from_utf8_lossy( & output1. stdout) ,
10800+ String :: from_utf8_lossy( & output1. stderr) ,
10801+ ) ;
10802+ assert ! (
10803+ output2. status. success( ) ,
10804+ "second `uv lock` failed\n stdout:\n {}\n stderr:\n {}" ,
10805+ String :: from_utf8_lossy( & output2. stdout) ,
10806+ String :: from_utf8_lossy( & output2. stderr) ,
10807+ ) ;
10808+
10809+ let archive_requests = server
10810+ . received_requests ( )
10811+ . await
10812+ . unwrap_or_default ( )
10813+ . into_iter ( )
10814+ . filter ( |request| request. url . path ( ) == "/files/tqdm-999.0.0.tar.gz" )
10815+ . count ( ) ;
10816+ assert ! (
10817+ archive_requests >= 2 ,
10818+ "expected at least two archive requests, saw {archive_requests}"
10819+ ) ;
10820+
10821+ let offline_output = lock_command ( & context3) . arg ( "--offline" ) . output ( ) ?;
10822+ assert ! (
10823+ offline_output. status. success( ) ,
10824+ "offline `uv lock` failed after concurrent cache writes\n stdout:\n {}\n stderr:\n {}" ,
10825+ String :: from_utf8_lossy( & offline_output. stdout) ,
10826+ String :: from_utf8_lossy( & offline_output. stderr) ,
10827+ ) ;
10828+
10829+ Ok ( ( ) )
10830+ }
10831+
1072810832#[ test]
1072910833fn path_hash_mismatch ( ) -> Result < ( ) > {
1073010834 let context = uv_test:: test_context!( "3.12" ) ;
0 commit comments