Skip to content

Commit d96296a

Browse files
rmarinhoCopilot
andcommitted
Refactor AvdManagerRunner: resolved path constructor, pattern matching, ThrowIfFailed overload
Apply review patterns from PR #283 (AdbRunner): - Constructor takes resolved 'string avdManagerPath' instead of Func<string?> delegates - Add IDictionary<string, string>? environmentVariables for ANDROID_HOME/JAVA_HOME - Remove AvdManagerPath property, IsAvailable, RequireAvdManagerPath(), ConfigureEnvironment() - Use 'is { Length: > 0 }' pattern matching for null/empty checks - Add ThrowIfFailed(StringWriter) overload to ProcessUtils - Change ThrowIfFailed/ValidateNotNullOrEmpty/FindCmdlineTool to internal visibility - Update tests: FindCmdlineTool tests replace AvdManagerPath tests, add constructor validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 37c3a79 commit d96296a

3 files changed

Lines changed: 61 additions & 81 deletions

File tree

src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,27 +207,32 @@ static string JoinArguments (string[] args)
207207
/// Throws <see cref="InvalidOperationException"/> when <paramref name="exitCode"/> is non-zero.
208208
/// Includes stderr/stdout context in the message when available.
209209
/// </summary>
210-
public static void ThrowIfFailed (int exitCode, string command, string? stderr = null, string? stdout = null)
210+
internal static void ThrowIfFailed (int exitCode, string command, string? stderr = null, string? stdout = null)
211211
{
212212
if (exitCode == 0)
213213
return;
214214

215215
var message = $"'{command}' failed with exit code {exitCode}.";
216216

217-
if (!string.IsNullOrEmpty (stderr))
218-
message += $" stderr:{Environment.NewLine}{stderr!.Trim ()}";
219-
if (!string.IsNullOrEmpty (stdout))
220-
message += $" stdout:{Environment.NewLine}{stdout!.Trim ()}";
217+
if (stderr is { Length: > 0 })
218+
message += $" stderr:{Environment.NewLine}{stderr.Trim ()}";
219+
if (stdout is { Length: > 0 })
220+
message += $" stdout:{Environment.NewLine}{stdout.Trim ()}";
221221

222222
throw new InvalidOperationException (message);
223223
}
224224

225+
internal static void ThrowIfFailed (int exitCode, string command, StringWriter? stderr, StringWriter? stdout = null)
226+
{
227+
ThrowIfFailed (exitCode, command, stderr?.ToString (), stdout?.ToString ());
228+
}
229+
225230
/// <summary>
226231
/// Validates that <paramref name="value"/> is not null or empty.
227232
/// Throws <see cref="ArgumentNullException"/> for null values and
228233
/// <see cref="ArgumentException"/> for empty strings.
229234
/// </summary>
230-
public static void ValidateNotNullOrEmpty (string? value, string paramName)
235+
internal static void ValidateNotNullOrEmpty (string? value, string paramName)
231236
{
232237
if (value is null)
233238
throw new ArgumentNullException (paramName);
@@ -239,7 +244,7 @@ public static void ValidateNotNullOrEmpty (string? value, string paramName)
239244
/// Searches versioned cmdline-tools directories (descending) and "latest" for a specific tool binary.
240245
/// Falls back to the legacy tools/bin path. Returns null if not found.
241246
/// </summary>
242-
public static string? FindCmdlineTool (string sdkPath, string toolName, string extension)
247+
internal static string? FindCmdlineTool (string sdkPath, string toolName, string extension)
243248
{
244249
var cmdlineToolsDir = Path.Combine (sdkPath, "cmdline-tools");
245250

src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.IO;
87
using System.Linq;
98
using System.Threading;
@@ -16,53 +15,30 @@ namespace Xamarin.Android.Tools;
1615
/// </summary>
1716
public class AvdManagerRunner
1817
{
19-
readonly Func<string?> getSdkPath;
20-
readonly Func<string?>? getJdkPath;
18+
readonly string avdManagerPath;
19+
readonly IDictionary<string, string>? environmentVariables;
2120

22-
public AvdManagerRunner (Func<string?> getSdkPath)
23-
: this (getSdkPath, null)
24-
{
25-
}
26-
27-
public AvdManagerRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath)
28-
{
29-
this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath));
30-
this.getJdkPath = getJdkPath;
31-
}
32-
33-
public string? AvdManagerPath {
34-
get {
35-
var sdkPath = getSdkPath ();
36-
if (string.IsNullOrEmpty (sdkPath))
37-
return null;
38-
39-
return ProcessUtils.FindCmdlineTool (sdkPath, "avdmanager", OS.IsWindows ? ".bat" : "");
40-
}
41-
}
42-
43-
public bool IsAvailable => !string.IsNullOrEmpty (AvdManagerPath);
44-
45-
string RequireAvdManagerPath ()
46-
{
47-
return AvdManagerPath ?? throw new InvalidOperationException ("AVD Manager not found.");
48-
}
49-
50-
void ConfigureEnvironment (ProcessStartInfo psi)
21+
/// <summary>
22+
/// Creates a new AvdManagerRunner with the full path to the avdmanager executable.
23+
/// </summary>
24+
/// <param name="avdManagerPath">Full path to avdmanager (e.g., "/path/to/sdk/cmdline-tools/latest/bin/avdmanager").</param>
25+
/// <param name="environmentVariables">Optional environment variables to pass to avdmanager processes.</param>
26+
public AvdManagerRunner (string avdManagerPath, IDictionary<string, string>? environmentVariables = null)
5127
{
52-
AndroidEnvironmentHelper.ConfigureEnvironment (psi, getSdkPath (), getJdkPath?.Invoke ());
28+
if (string.IsNullOrWhiteSpace (avdManagerPath))
29+
throw new ArgumentException ("Path to avdmanager must not be empty.", nameof (avdManagerPath));
30+
this.avdManagerPath = avdManagerPath;
31+
this.environmentVariables = environmentVariables;
5332
}
5433

5534
public async Task<IReadOnlyList<AvdInfo>> ListAvdsAsync (CancellationToken cancellationToken = default)
5635
{
57-
var avdManagerPath = RequireAvdManagerPath ();
58-
5936
using var stdout = new StringWriter ();
6037
using var stderr = new StringWriter ();
6138
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "avd");
62-
ConfigureEnvironment (psi);
63-
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken).ConfigureAwait (false);
39+
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
6440

65-
ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list avd", stderr.ToString ());
41+
ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list avd", stderr);
6642

6743
return ParseAvdListOutput (stdout.ToString ());
6844
}
@@ -73,8 +49,6 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
7349
ProcessUtils.ValidateNotNullOrEmpty (name, nameof (name));
7450
ProcessUtils.ValidateNotNullOrEmpty (systemImage, nameof (systemImage));
7551

76-
var avdManagerPath = RequireAvdManagerPath ();
77-
7852
// Check if AVD already exists — return it instead of failing
7953
if (!force) {
8054
var existing = (await ListAvdsAsync (cancellationToken).ConfigureAwait (false))
@@ -89,7 +63,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
8963
force = true;
9064

9165
var args = new List<string> { "create", "avd", "-n", name, "-k", systemImage };
92-
if (!string.IsNullOrEmpty (deviceProfile))
66+
if (deviceProfile is { Length: > 0 })
9367
args.AddRange (new [] { "-d", deviceProfile });
9468
if (force)
9569
args.Add ("--force");
@@ -98,10 +72,9 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
9872
using var stderr = new StringWriter ();
9973
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, args.ToArray ());
10074
psi.RedirectStandardInput = true;
101-
ConfigureEnvironment (psi);
10275

10376
// avdmanager prompts "Do you wish to create a custom hardware profile?" — answer "no"
104-
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken,
77+
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables,
10578
onStarted: p => {
10679
try {
10780
p.StandardInput.WriteLine ("no");
@@ -111,7 +84,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
11184
}
11285
}).ConfigureAwait (false);
11386

114-
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager create avd -n {name}", stderr.ToString (), stdout.ToString ());
87+
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager create avd -n {name}", stderr, stdout);
11588

11689
// Re-list to get the actual path from avdmanager (respects ANDROID_USER_HOME/ANDROID_AVD_HOME)
11790
var avds = await ListAvdsAsync (cancellationToken).ConfigureAwait (false);
@@ -131,14 +104,11 @@ public async Task DeleteAvdAsync (string name, CancellationToken cancellationTok
131104
{
132105
ProcessUtils.ValidateNotNullOrEmpty (name, nameof (name));
133106

134-
var avdManagerPath = RequireAvdManagerPath ();
135-
136107
using var stderr = new StringWriter ();
137108
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "delete", "avd", "--name", name);
138-
ConfigureEnvironment (psi);
139-
var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken).ConfigureAwait (false);
109+
var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
140110

141-
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr.ToString ());
111+
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr);
142112
}
143113

144114
internal static List<AvdInfo> ParseAvdListOutput (string output)
@@ -173,12 +143,12 @@ static string GetAvdRootDirectory ()
173143
{
174144
// ANDROID_AVD_HOME takes highest priority
175145
var avdHome = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidAvdHome);
176-
if (!string.IsNullOrEmpty (avdHome))
146+
if (avdHome is { Length: > 0 })
177147
return avdHome;
178148

179149
// ANDROID_USER_HOME/avd is the next option
180150
var userHome = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidUserHome);
181-
if (!string.IsNullOrEmpty (userHome))
151+
if (userHome is { Length: > 0 })
182152
return Path.Combine (userHome, "avd");
183153

184154
// Default: ~/.android/avd

tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void ParseAvdListOutput_SingleAvdNoDevice ()
8989
}
9090

9191
[Test]
92-
public void AvdManagerPath_FindsVersionedDir ()
92+
public void FindCmdlineTool_FindsVersionedDir ()
9393
{
9494
var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}");
9595
var binDir = Path.Combine (tempDir, "cmdline-tools", "12.0", "bin");
@@ -99,16 +99,16 @@ public void AvdManagerPath_FindsVersionedDir ()
9999
var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager";
100100
File.WriteAllText (Path.Combine (binDir, avdMgrName), "");
101101

102-
var runner = new AvdManagerRunner (() => tempDir, null);
103-
Assert.IsNotNull (runner.AvdManagerPath);
104-
Assert.IsTrue (runner.AvdManagerPath!.Contains ("12.0"));
102+
var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : "");
103+
Assert.IsNotNull (path);
104+
Assert.IsTrue (path!.Contains ("12.0"));
105105
} finally {
106106
Directory.Delete (tempDir, true);
107107
}
108108
}
109109

110110
[Test]
111-
public void AvdManagerPath_PrefersHigherVersion ()
111+
public void FindCmdlineTool_PrefersHigherVersion ()
112112
{
113113
var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}");
114114
var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager";
@@ -121,16 +121,16 @@ public void AvdManagerPath_PrefersHigherVersion ()
121121
File.WriteAllText (Path.Combine (binDir12, avdMgrName), "");
122122

123123
try {
124-
var runner = new AvdManagerRunner (() => tempDir, null);
125-
Assert.IsNotNull (runner.AvdManagerPath);
126-
Assert.IsTrue (runner.AvdManagerPath!.Contains ("12.0"));
124+
var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : "");
125+
Assert.IsNotNull (path);
126+
Assert.IsTrue (path!.Contains ("12.0"));
127127
} finally {
128128
Directory.Delete (tempDir, true);
129129
}
130130
}
131131

132132
[Test]
133-
public void AvdManagerPath_FallsBackToLatest ()
133+
public void FindCmdlineTool_FallsBackToLatest ()
134134
{
135135
var tempDir = Path.Combine (Path.GetTempPath (), $"avd-test-{Path.GetRandomFileName ()}");
136136
var binDir = Path.Combine (tempDir, "cmdline-tools", "latest", "bin");
@@ -140,60 +140,65 @@ public void AvdManagerPath_FallsBackToLatest ()
140140
var avdMgrName = OS.IsWindows ? "avdmanager.bat" : "avdmanager";
141141
File.WriteAllText (Path.Combine (binDir, avdMgrName), "");
142142

143-
var runner = new AvdManagerRunner (() => tempDir, null);
144-
Assert.IsNotNull (runner.AvdManagerPath);
145-
Assert.IsTrue (runner.AvdManagerPath!.Contains ("latest"));
143+
var path = ProcessUtils.FindCmdlineTool (tempDir, "avdmanager", OS.IsWindows ? ".bat" : "");
144+
Assert.IsNotNull (path);
145+
Assert.IsTrue (path!.Contains ("latest"));
146146
} finally {
147147
Directory.Delete (tempDir, true);
148148
}
149149
}
150150

151151
[Test]
152-
public void AvdManagerPath_NullSdk_ReturnsNull ()
152+
public void FindCmdlineTool_MissingSdk_ReturnsNull ()
153153
{
154-
var runner = new AvdManagerRunner (() => null, null);
155-
Assert.IsNull (runner.AvdManagerPath);
154+
var path = ProcessUtils.FindCmdlineTool ("/nonexistent/path", "avdmanager", OS.IsWindows ? ".bat" : "");
155+
Assert.IsNull (path);
156156
}
157157

158158
[Test]
159-
public void AvdManagerPath_MissingSdk_ReturnsNull ()
159+
public void Constructor_NullPath_ThrowsArgumentException ()
160160
{
161-
var runner = new AvdManagerRunner (() => "/nonexistent/path", null);
162-
Assert.IsNull (runner.AvdManagerPath);
161+
Assert.Throws<ArgumentException> (() => new AvdManagerRunner (null!));
162+
}
163+
164+
[Test]
165+
public void Constructor_EmptyPath_ThrowsArgumentException ()
166+
{
167+
Assert.Throws<ArgumentException> (() => new AvdManagerRunner (""));
163168
}
164169

165170
[Test]
166171
public void CreateAvdAsync_NullName_ThrowsArgumentNullException ()
167172
{
168-
var runner = new AvdManagerRunner (() => "/fake/sdk", null);
173+
var runner = new AvdManagerRunner ("/fake/avdmanager");
169174
Assert.ThrowsAsync<ArgumentNullException> (() => runner.CreateAvdAsync (null!, "system-image"));
170175
}
171176

172177
[Test]
173178
public void CreateAvdAsync_EmptyName_ThrowsArgumentException ()
174179
{
175-
var runner = new AvdManagerRunner (() => "/fake/sdk", null);
180+
var runner = new AvdManagerRunner ("/fake/avdmanager");
176181
Assert.ThrowsAsync<ArgumentException> (() => runner.CreateAvdAsync ("", "system-image"));
177182
}
178183

179184
[Test]
180185
public void CreateAvdAsync_EmptySystemImage_ThrowsArgumentException ()
181186
{
182-
var runner = new AvdManagerRunner (() => "/fake/sdk", null);
187+
var runner = new AvdManagerRunner ("/fake/avdmanager");
183188
Assert.ThrowsAsync<ArgumentException> (() => runner.CreateAvdAsync ("test-avd", ""));
184189
}
185190

186191
[Test]
187192
public void DeleteAvdAsync_NullName_ThrowsArgumentNullException ()
188193
{
189-
var runner = new AvdManagerRunner (() => "/fake/sdk", null);
194+
var runner = new AvdManagerRunner ("/fake/avdmanager");
190195
Assert.ThrowsAsync<ArgumentNullException> (() => runner.DeleteAvdAsync (null!));
191196
}
192197

193198
[Test]
194199
public void DeleteAvdAsync_EmptyName_ThrowsArgumentException ()
195200
{
196-
var runner = new AvdManagerRunner (() => "/fake/sdk", null);
201+
var runner = new AvdManagerRunner ("/fake/avdmanager");
197202
Assert.ThrowsAsync<ArgumentException> (() => runner.DeleteAvdAsync (""));
198203
}
199204
}

0 commit comments

Comments
 (0)