@@ -1274,13 +1274,13 @@ public async Task TestLinuxScanner_SyftCacheKey_BindOrderDoesNotMatterAsync()
12741274
12751275 var result1 = await scanner1 . GetSyftOutputAsync (
12761276 "oci-dir:/img" ,
1277- new List < string > { "/host/a:/container/a:ro" , "/host/b:/container/b:ro" } ,
1277+ [ "/host/a:/container/a:ro" , "/host/b:/container/b:ro" ] ,
12781278 LinuxScannerScope . AllLayers
12791279 ) ;
12801280
12811281 var result2 = await scanner2 . GetSyftOutputAsync (
12821282 "oci-dir:/img" ,
1283- new List < string > { "/host/b:/container/b:ro" , "/host/a:/container/a:ro" } ,
1283+ [ "/host/b:/container/b:ro" , "/host/a:/container/a:ro" ] ,
12841284 LinuxScannerScope . AllLayers
12851285 ) ;
12861286
@@ -1375,4 +1375,77 @@ await this.linuxScanner.ScanLinuxAsync(
13751375 Times . Exactly ( 2 )
13761376 ) ;
13771377 }
1378+
1379+ [ TestMethod ]
1380+ public async Task TestLinuxScanner_CancelledCaller_DoesNotBlockOnInFlightSyftRunAsync ( )
1381+ {
1382+ LinuxScanner . ResetCache ( ) ;
1383+
1384+ // Use a TCS to control when the syft container "completes",
1385+ // so the first caller's run stays in-flight while we cancel the second.
1386+ var syftCompletionSource = new TaskCompletionSource < ( string , string ) > ( ) ;
1387+
1388+ this . mockDockerService . Setup ( service =>
1389+ service . CreateAndRunContainerAsync (
1390+ It . IsAny < string > ( ) ,
1391+ It . IsAny < List < string > > ( ) ,
1392+ It . IsAny < IList < string > > ( ) ,
1393+ It . IsAny < CancellationToken > ( )
1394+ )
1395+ )
1396+ . Returns ( syftCompletionSource . Task ) ;
1397+
1398+ var enabledTypes = new HashSet < ComponentType > { ComponentType . Linux } ;
1399+
1400+ var layers = new [ ]
1401+ {
1402+ new DockerLayer
1403+ {
1404+ LayerIndex = 0 ,
1405+ DiffId = "sha256:f95fc50d21d981f1efe1f04109c2c3287c271794f5d9e4fdf9888851a174a971" ,
1406+ } ,
1407+ } ;
1408+
1409+ var scanner1 = new LinuxScanner (
1410+ this . mockDockerService . Object ,
1411+ this . mockLogger . Object ,
1412+ this . componentFactories ,
1413+ this . artifactFilters
1414+ ) ;
1415+ var scanner2 = new LinuxScanner (
1416+ this . mockDockerService . Object ,
1417+ this . mockLogger . Object ,
1418+ this . componentFactories ,
1419+ this . artifactFilters
1420+ ) ;
1421+
1422+ // First caller starts the syft run (it will block on syftCompletionSource).
1423+ var task1 = scanner1 . ScanLinuxAsync ( "cancel_hash" , layers , 0 , enabledTypes , LinuxScannerScope . AllLayers ) ;
1424+
1425+ // Second caller with a cancellable token joins the same in-flight run.
1426+ using var cts = new CancellationTokenSource ( ) ;
1427+ var task2 = scanner2 . ScanLinuxAsync ( "cancel_hash" , layers , 0 , enabledTypes , LinuxScannerScope . AllLayers , cts . Token ) ;
1428+
1429+ // Cancel the second caller while the first is still running.
1430+ await cts . CancelAsync ( ) ;
1431+
1432+ // The second caller should throw OperationCanceledException promptly.
1433+ try
1434+ {
1435+ await task2 ;
1436+ Assert . Fail ( "Expected OperationCanceledException was not thrown" ) ;
1437+ }
1438+ catch ( OperationCanceledException )
1439+ {
1440+ // Expected — the second caller was cancelled while waiting for the in-flight run.
1441+ }
1442+
1443+ // The first caller should still be running (not cancelled).
1444+ task1 . IsCompleted . Should ( ) . BeFalse ( ) ;
1445+
1446+ // Now let the first caller complete normally.
1447+ syftCompletionSource . SetResult ( ( SyftOutputNoAuthorOrLicense , string . Empty ) ) ;
1448+ var result1 = await task1 ;
1449+ result1 . Should ( ) . NotBeEmpty ( ) ;
1450+ }
13781451}
0 commit comments