88using System . Threading . Tasks ;
99using FluentAssertions ;
1010using Microsoft . Deployment . DotNet . Releases ;
11- using Microsoft . Dotnet . Installation ;
1211using Microsoft . DotNet . Tools . Bootstrapper ;
1312using Microsoft . DotNet . Tools . Dnup . Tests . Utilities ;
13+ using Microsoft . Dotnet . Installation ;
1414using Xunit ;
15- using Microsoft . Dotnet . Installation . Internal ;
1615
1716namespace Microsoft . DotNet . Tools . Dnup . Tests ;
1817
@@ -50,7 +49,7 @@ public void Test(string channel)
5049 var updateChannel = new UpdateChannel ( channel ) ;
5150 var expectedVersion = new ManifestChannelVersionResolver ( ) . Resolve (
5251 new DotnetInstallRequest (
53- new DotnetInstallRoot ( testEnv . InstallPath , InstallerUtilities . GetDefaultInstallArchitecture ( ) ) ,
52+ new DotnetInstallRoot ( testEnv . InstallPath , DnupUtilities . GetDefaultInstallArchitecture ( ) ) ,
5453 updateChannel ,
5554 InstallComponent . SDK ,
5655 new InstallRequestOptions ( ) ) ) ;
@@ -60,17 +59,15 @@ public void Test(string channel)
6059 Console . WriteLine ( $ "Channel '{ channel } ' resolved to version: { expectedVersion } ") ;
6160
6261 // Execute the command with explicit manifest path as a separate process
63- var args = DnupTestUtilities . BuildArguments ( channel , testEnv . InstallPath , testEnv . ManifestPath ) ;
64-
65- DnupProcessResult result = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
66- result . ExitCode . Should ( ) . Be ( 0 ,
67- $ "dnup exited with code { result . ExitCode } . Output:\n { DnupTestUtilities . FormatOutputForAssertions ( result ) } ") ;
62+ var args = DnupTestUtilities . BuildArguments ( channel , testEnv . InstallPath , testEnv . ManifestPath ) ;
63+ ( int exitCode , string output ) = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
64+ exitCode . Should ( ) . Be ( 0 , $ "dnup exited with code { exitCode } . Output:\n { output } ") ;
6865
6966 Directory . Exists ( testEnv . InstallPath ) . Should ( ) . BeTrue ( ) ;
7067 Directory . Exists ( Path . GetDirectoryName ( testEnv . ManifestPath ) ) . Should ( ) . BeTrue ( ) ;
7168
7269 // Verify the installation was properly recorded in the manifest
73- List < DotnetInstall > installs = [ ] ;
70+ List < DotnetInstall > installs = new ( ) ;
7471 using ( var finalizeLock = new ScopedMutex ( Constants . MutexNames . ModifyInstallationStates ) )
7572 {
7673 var manifest = new DnupSharedManifest ( testEnv . ManifestPath ) ;
@@ -83,10 +80,7 @@ public void Test(string channel)
8380 matchingInstalls . Should ( ) . ContainSingle ( ) ;
8481
8582 var install = matchingInstalls [ 0 ] ;
86- install . Component . Should ( ) . Be ( InstallComponent . SDK ) ;
87-
88- DnupTestUtilities . ValidateInstall ( install ) . Should ( ) . BeTrue (
89- $ "ArchiveInstallationValidator failed for installed version { install . Version } at { testEnv . InstallPath } ") ;
83+ install . Component . Should ( ) . Be ( Microsoft . Dotnet . Installation . InstallComponent . SDK ) ;
9084
9185 // Verify the installed version matches what the resolver predicted
9286 if ( ! updateChannel . IsFullySpecifiedVersion ( ) )
@@ -126,36 +120,31 @@ public void TestReusesExistingInstall()
126120
127121 // Execute dnup to install the SDK the first time with explicit manifest path as a separate process
128122 Console . WriteLine ( $ "First installation of { channel } ") ;
129- DnupProcessResult firstInstall = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
130- firstInstall . ExitCode . Should ( ) . Be ( 0 ,
131- $ "First installation failed with exit code { firstInstall . ExitCode } . Output:\n { DnupTestUtilities . FormatOutputForAssertions ( firstInstall ) } ") ;
123+ ( int exitCode , string firstInstallOutput ) = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
124+ exitCode . Should ( ) . Be ( 0 , $ "First installation failed with exit code { exitCode } . Output:\n { firstInstallOutput } ") ;
132125
133- List < DotnetInstall > firstDnupInstalls = [ ] ;
126+ List < DotnetInstall > firstDnupInstalls = new ( ) ;
134127 // Verify the installation was successful
135128 using ( var finalizeLock = new ScopedMutex ( Constants . MutexNames . ModifyInstallationStates ) )
136129 {
137130 var manifest = new DnupSharedManifest ( testEnv . ManifestPath ) ;
138131 firstDnupInstalls = manifest . GetInstalledVersions ( ) . ToList ( ) ;
139132 }
140133
141- var firstInstallRecord = firstDnupInstalls . Single ( i => DnupUtilities . PathsEqual ( i . InstallRoot . Path , testEnv . InstallPath ) ) ;
142- DnupTestUtilities . ValidateInstall ( firstInstallRecord ) . Should ( ) . BeTrue (
143- $ "ArchiveInstallationValidator failed for initial install of { channel } at { testEnv . InstallPath } ") ;
134+ firstDnupInstalls . Where ( i => DnupUtilities . PathsEqual ( i . InstallRoot . Path , testEnv . InstallPath ) ) . Should ( ) . ContainSingle ( ) ;
144135
145136 // Now install the same SDK again and capture the console output
146137 Console . WriteLine ( $ "Installing .NET SDK { channel } again (should be skipped)") ;
147- DnupProcessResult secondInstall = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
148- secondInstall . ExitCode . Should ( ) . Be ( 0 ,
149- $ "Second installation failed with exit code { secondInstall . ExitCode } . Output:\n { DnupTestUtilities . FormatOutputForAssertions ( secondInstall ) } ") ;
138+ ( exitCode , string output ) = DnupTestUtilities . RunDnupProcess ( args , captureOutput : true , workingDirectory : testEnv . TempRoot ) ;
139+ exitCode . Should ( ) . Be ( 0 , $ "Second installation failed with exit code { exitCode } . Output:\n { output } ") ;
150140
151- DnupTestUtilities . AssertOutput ( secondInstall , output =>
152- {
153- output . Should ( ) . Contain ( "is already installed, skipping installation" ,
154- "dnup should detect that the SDK is already installed and skip the installation" ) ;
141+ // Verify the output contains a message indicating the SDK is already installed
142+ output . Should ( ) . Contain ( "is already installed, skipping installation" ,
143+ "dnup should detect that the SDK is already installed and skip the installation" ) ;
155144
156- output . Should ( ) . NotContain ( "Downloading .NET SDK" ,
157- "dnup should not attempt to download the SDK again" ) ;
158- } ) ;
145+ // The output should not contain download progress
146+ output . Should ( ) . NotContain ( "Downloading .NET SDK" ,
147+ "dnup should not attempt to download the SDK again" ) ;
159148
160149 List < DotnetInstall > matchingInstalls = new ( ) ;
161150 // Verify the installation record in the manifest hasn't changed
@@ -170,8 +159,6 @@ public void TestReusesExistingInstall()
170159 matchingInstalls . Should ( ) . ContainSingle ( ) ;
171160 // And it should be for the specified version
172161 matchingInstalls [ 0 ] . Version . ToString ( ) . Should ( ) . Be ( channel ) ;
173- DnupTestUtilities . ValidateInstall ( matchingInstalls [ 0 ] ) . Should ( ) . BeTrue (
174- $ "ArchiveInstallationValidator failed after reinstall attempt for { channel } at { testEnv . InstallPath } ") ;
175162 }
176163}
177164
@@ -181,82 +168,32 @@ public void TestReusesExistingInstall()
181168[ Collection ( "DnupConcurrencyCollection" ) ]
182169public class ConcurrentInstallationTests
183170{
184- public static IEnumerable < object [ ] > ConcurrentInstallChannels => new List < object [ ] >
171+ [ Fact ]
172+ public async Task ConcurrentInstallsSerializeViaGlobalMutex ( )
185173 {
186- new object [ ] { "9.0.103" , "9.0.103" , false } ,
187- new object [ ] { "9.0.103" , "preview" , true }
188- } ;
174+ const string channel = "9.0.103" ;
189175
190- [ Theory ]
191- [ MemberData ( nameof ( ConcurrentInstallChannels ) ) ]
192- public async Task ConcurrentInstallsSerializeViaGlobalMutex ( string firstChannel , string secondChannel , bool expectDistinct )
193- {
194176 using var testEnv = DnupTestUtilities . CreateTestEnvironment ( ) ;
177+ var args1 = DnupTestUtilities . BuildArguments ( channel , testEnv . InstallPath , testEnv . ManifestPath ) ;
178+ var args2 = DnupTestUtilities . BuildArguments ( channel , testEnv . InstallPath , testEnv . ManifestPath ) ;
195179
196- var resolver = new ManifestChannelVersionResolver ( ) ;
197- ReleaseVersion ? firstResolved = resolver . Resolve (
198- new DotnetInstallRequest (
199- new DotnetInstallRoot ( testEnv . InstallPath , InstallerUtilities . GetDefaultInstallArchitecture ( ) ) ,
200- new UpdateChannel ( firstChannel ) ,
201- InstallComponent . SDK ,
202- new InstallRequestOptions ( ) ) ) ;
203-
204- ReleaseVersion ? secondResolved = resolver . Resolve (
205- new DotnetInstallRequest (
206- new DotnetInstallRoot ( testEnv . InstallPath , InstallerUtilities . GetDefaultInstallArchitecture ( ) ) ,
207- new UpdateChannel ( secondChannel ) ,
208- InstallComponent . SDK ,
209- new InstallRequestOptions ( ) ) ) ;
210-
211- firstResolved . Should ( ) . NotBeNull ( $ "Channel { firstChannel } should resolve to a version") ;
212- secondResolved . Should ( ) . NotBeNull ( $ "Channel { secondChannel } should resolve to a version") ;
213-
214- if ( expectDistinct && string . Equals ( firstResolved ! . ToString ( ) , secondResolved ! . ToString ( ) , StringComparison . OrdinalIgnoreCase ) )
215- {
216- Console . WriteLine ( $ "Skipping concurrent distinct-version scenario because both channels resolved to { firstResolved } ") ;
217- return ;
218- }
219- var args1 = DnupTestUtilities . BuildArguments ( firstChannel , testEnv . InstallPath , testEnv . ManifestPath ) ;
220- var args2 = DnupTestUtilities . BuildArguments ( secondChannel , testEnv . InstallPath , testEnv . ManifestPath ) ;
221-
222- var installTask1 = Task . Run ( ( ) => DnupTestUtilities . RunDnupProcess ( args1 , captureOutput : true , workingDirectory : testEnv . TempRoot ) ) ;
223- var installTask2 = Task . Run ( ( ) => DnupTestUtilities . RunDnupProcess ( args2 , captureOutput : true , workingDirectory : testEnv . TempRoot ) ) ;
224-
225- DnupProcessResult [ ] results = await Task . WhenAll ( installTask1 , installTask2 ) ;
180+ var installTask1 = Task . Run ( ( ) => DnupTestUtilities . RunDnupProcess ( args1 , captureOutput : true , workingDirectory : testEnv . TempRoot ) ) ;
181+ var installTask2 = Task . Run ( ( ) => DnupTestUtilities . RunDnupProcess ( args2 , captureOutput : true , workingDirectory : testEnv . TempRoot ) ) ;
226182
227- results [ 0 ] . ExitCode . Should ( ) . Be ( 0 ,
228- $ "First concurrent install failed with exit code { results [ 0 ] . ExitCode } . Output:\n { DnupTestUtilities . FormatOutputForAssertions ( results [ 0 ] ) } ") ;
229- results [ 1 ] . ExitCode . Should ( ) . Be ( 0 ,
230- $ "Second concurrent install failed with exit code { results [ 1 ] . ExitCode } . Output:\n { DnupTestUtilities . FormatOutputForAssertions ( results [ 1 ] ) } ") ;
183+ var results = await Task . WhenAll ( installTask1 , installTask2 ) ;
231184
232- var installs = new List < DotnetInstall > ( ) ;
185+ results [ 0 ] . exitCode . Should ( ) . Be ( 0 ,
186+ $ "First concurrent install failed with exit code { results [ 0 ] . exitCode } . Output:\n { results [ 0 ] . output } ") ;
187+ results [ 1 ] . exitCode . Should ( ) . Be ( 0 ,
188+ $ "Second concurrent install failed with exit code { results [ 1 ] . exitCode } . Output:\n { results [ 1 ] . output } ") ;
233189
234- using ( var finalizeLock = new ScopedMutex ( Constants . MutexNames . ModifyInstallationStates ) )
190+ string manifestMutexName = DnupTestUtilities . GetManifestMutexName ( testEnv . ManifestPath ) ;
191+ using ( var finalizeLock = new ScopedMutex ( manifestMutexName ) )
235192 {
236193 var manifest = new DnupSharedManifest ( testEnv . ManifestPath ) ;
237- installs = manifest . GetInstalledVersions ( )
238- . Where ( i => DnupUtilities . PathsEqual ( i . InstallRoot . Path , testEnv . InstallPath ) )
239- . ToList ( ) ;
240- }
241-
242- int expectedInstallCount = string . Equals ( firstResolved ! . ToString ( ) , secondResolved ! . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ? 1 : 2 ;
243- installs . Should ( ) . HaveCount ( expectedInstallCount ) ;
244-
245- var expectedVersions = new HashSet < string > ( StringComparer . OrdinalIgnoreCase )
246- {
247- firstResolved . ToString ( ) ! ,
248- secondResolved ! . ToString ( ) !
249- } ;
250-
251- foreach ( var install in installs )
252- {
253- install . Component . Should ( ) . Be ( InstallComponent . SDK ) ;
254- expectedVersions . Should ( ) . Contain ( install . Version . ToString ( ) ) ;
255- DnupTestUtilities . ValidateInstall ( install ) . Should ( ) . BeTrue (
256- $ "ArchiveInstallationValidator failed for concurrent install { install . Version } at { testEnv . InstallPath } ") ;
194+ var installs = manifest . GetInstalledVersions ( ) . Where ( i => DnupUtilities . PathsEqual ( i . InstallRoot . Path , testEnv . InstallPath ) ) . ToList ( ) ;
195+ installs . Should ( ) . ContainSingle ( ) ;
196+ installs [ 0 ] . Version . ToString ( ) . Should ( ) . Be ( channel ) ;
257197 }
258-
259- var actualVersions = installs . Select ( i => i . Version . ToString ( ) ) . ToHashSet ( StringComparer . OrdinalIgnoreCase ) ;
260- actualVersions . Should ( ) . BeEquivalentTo ( expectedVersions ) ;
261198 }
262199}
0 commit comments