Skip to content

Commit 7296933

Browse files
raych1Copilot
andcommitted
Implement Rust packaging support in azsdk pkg pack
Add PackAsync override to RustLanguageService that shells out to eng/scripts/Pack-Crates.ps1 in the azure-sdk-for-rust repo. The implementation passes -PackageNames and -NoVerify flags, and optionally -OutputPath when specified. Artifact path resolution handles both default (target/package/) and custom output locations with fallback search for .crate files. Add comprehensive tests in both PackToolTests (integration-level) and RustLanguageServiceTests (unit-level) covering success, failure, missing script, output path forwarding, and argument verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5c88da3 commit 7296933

File tree

4 files changed

+450
-7
lines changed

4 files changed

+450
-7
lines changed

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/RustLanguageServiceTests.cs

Lines changed: 181 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public async Task BuildAsync_ScriptExists_ExecutesPowershell()
116116
Directory.CreateDirectory(packageDir);
117117
var scriptDir = Path.Combine(_tempDirectory.DirectoryPath, "eng", "scripts");
118118
Directory.CreateDirectory(scriptDir);
119-
File.WriteAllText(Path.Combine(scriptDir, "build-sdk.ps1"), "# build script");
119+
File.WriteAllText(Path.Combine(scriptDir, "Build-Sdk.ps1"), "# build script");
120120

121121
_mockGitHelper
122122
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
@@ -144,7 +144,7 @@ public async Task BuildAsync_ScriptFails_ReturnsFailure()
144144
Directory.CreateDirectory(packageDir);
145145
var scriptDir = Path.Combine(_tempDirectory.DirectoryPath, "eng", "scripts");
146146
Directory.CreateDirectory(scriptDir);
147-
File.WriteAllText(Path.Combine(scriptDir, "build-sdk.ps1"), "# build script");
147+
File.WriteAllText(Path.Combine(scriptDir, "Build-Sdk.ps1"), "# build script");
148148

149149
_mockGitHelper
150150
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
@@ -184,7 +184,7 @@ public async Task BuildAsync_DoesNotUseSpecGenSdkConfig()
184184
Directory.CreateDirectory(packageDir);
185185
var scriptDir = Path.Combine(_tempDirectory.DirectoryPath, "eng", "scripts");
186186
Directory.CreateDirectory(scriptDir);
187-
File.WriteAllText(Path.Combine(scriptDir, "build-sdk.ps1"), "# build script");
187+
File.WriteAllText(Path.Combine(scriptDir, "Build-Sdk.ps1"), "# build script");
188188

189189
_mockGitHelper
190190
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
@@ -203,6 +203,173 @@ public async Task BuildAsync_DoesNotUseSpecGenSdkConfig()
203203

204204
#endregion
205205

206+
#region PackAsync Tests
207+
208+
[Test]
209+
public async Task PackAsync_EmptyPath_ReturnsFailure()
210+
{
211+
var (success, errorMessage, _, _) = await _service.PackAsync(string.Empty);
212+
213+
Assert.That(success, Is.False);
214+
Assert.That(errorMessage, Does.Contain("required and cannot be empty"));
215+
}
216+
217+
[Test]
218+
public async Task PackAsync_NonexistentPath_ReturnsFailure()
219+
{
220+
var (success, errorMessage, _, _) = await _service.PackAsync("/nonexistent/path");
221+
222+
Assert.That(success, Is.False);
223+
Assert.That(errorMessage, Does.Contain("does not exist"));
224+
}
225+
226+
[Test]
227+
public async Task PackAsync_MissingPackScript_ReturnsFailure()
228+
{
229+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
230+
Directory.CreateDirectory(packageDir);
231+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
232+
233+
_mockGitHelper
234+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
235+
.ReturnsAsync(_tempDirectory.DirectoryPath);
236+
237+
SetupCargoMetadata("azure_core", "0.34.0");
238+
239+
var (success, errorMessage, _, _) = await _service.PackAsync(packageDir);
240+
241+
Assert.That(success, Is.False);
242+
Assert.That(errorMessage, Does.Contain("Pack script not found"));
243+
}
244+
245+
[Test]
246+
public async Task PackAsync_ScriptExists_ExecutesPowershellWithCorrectArgs()
247+
{
248+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
249+
Directory.CreateDirectory(packageDir);
250+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
251+
CreatePackScript();
252+
253+
_mockGitHelper
254+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
255+
.ReturnsAsync(_tempDirectory.DirectoryPath);
256+
257+
SetupCargoMetadata("azure_core", "0.34.0");
258+
259+
_mockPowershellHelper
260+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
261+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [(StdioLevel.StandardOutput, "ok")] });
262+
263+
var (success, _, _, _) = await _service.PackAsync(packageDir);
264+
265+
Assert.That(success, Is.True);
266+
_mockPowershellHelper.Verify(x => x.Run(
267+
It.Is<PowershellOptions>(o =>
268+
o.Args.Contains("-PackageNames") &&
269+
o.Args.Contains("azure_core") &&
270+
o.Args.Contains("-NoVerify")),
271+
It.IsAny<CancellationToken>()), Times.Once);
272+
}
273+
274+
[Test]
275+
public async Task PackAsync_WithOutputPath_PassesOutputPathArg()
276+
{
277+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
278+
Directory.CreateDirectory(packageDir);
279+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
280+
CreatePackScript();
281+
var outputDir = Path.Combine(_tempDirectory.DirectoryPath, "output");
282+
283+
_mockGitHelper
284+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
285+
.ReturnsAsync(_tempDirectory.DirectoryPath);
286+
287+
SetupCargoMetadata("azure_core", "0.34.0");
288+
289+
_mockPowershellHelper
290+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
291+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [] });
292+
293+
await _service.PackAsync(packageDir, outputDir);
294+
295+
_mockPowershellHelper.Verify(x => x.Run(
296+
It.Is<PowershellOptions>(o =>
297+
o.Args.Contains("-OutputPath") &&
298+
o.Args.Contains(outputDir)),
299+
It.IsAny<CancellationToken>()), Times.Once);
300+
}
301+
302+
[Test]
303+
public async Task PackAsync_ScriptFails_ReturnsFailure()
304+
{
305+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
306+
Directory.CreateDirectory(packageDir);
307+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
308+
CreatePackScript();
309+
310+
_mockGitHelper
311+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
312+
.ReturnsAsync(_tempDirectory.DirectoryPath);
313+
314+
SetupCargoMetadata("azure_core", "0.34.0");
315+
316+
_mockPowershellHelper
317+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
318+
.ReturnsAsync(new ProcessResult { ExitCode = 1, OutputDetails = [(StdioLevel.StandardOutput, "cargo publish failed")] });
319+
320+
var (success, errorMessage, _, _) = await _service.PackAsync(packageDir);
321+
322+
Assert.That(success, Is.False);
323+
Assert.That(errorMessage, Does.Contain("Pack failed with exit code 1"));
324+
}
325+
326+
[Test]
327+
public async Task PackAsync_ResolvesArtifactPath_WhenCrateExists()
328+
{
329+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
330+
Directory.CreateDirectory(packageDir);
331+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
332+
CreatePackScript();
333+
334+
// Create the .crate artifact
335+
var targetDir = Path.Combine(_tempDirectory.DirectoryPath, "target", "package");
336+
Directory.CreateDirectory(targetDir);
337+
File.WriteAllText(Path.Combine(targetDir, "azure_core-0.34.0.crate"), "fake");
338+
339+
_mockGitHelper
340+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
341+
.ReturnsAsync(_tempDirectory.DirectoryPath);
342+
343+
SetupCargoMetadata("azure_core", "0.34.0");
344+
345+
_mockPowershellHelper
346+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
347+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [] });
348+
349+
var (success, _, _, artifactPath) = await _service.PackAsync(packageDir);
350+
351+
Assert.That(success, Is.True);
352+
Assert.That(artifactPath, Does.Contain("azure_core-0.34.0.crate"));
353+
}
354+
355+
[Test]
356+
public async Task PackAsync_DiscoverRepoRootFails_ReturnsFailure()
357+
{
358+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
359+
Directory.CreateDirectory(packageDir);
360+
361+
_mockGitHelper
362+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
363+
.ReturnsAsync(string.Empty);
364+
365+
var (success, errorMessage, _, _) = await _service.PackAsync(packageDir);
366+
367+
Assert.That(success, Is.False);
368+
Assert.That(errorMessage, Does.Contain("Failed to discover local sdk repo"));
369+
}
370+
371+
#endregion
372+
206373
#region GetPackageInfo Tests
207374

208375
[Test]
@@ -375,4 +542,15 @@ public async Task GetPackageInfo_CargoOutputWithExtraFields_ParsesCorrectly()
375542
}
376543

377544
#endregion
545+
546+
#region Helper Methods
547+
548+
private void CreatePackScript()
549+
{
550+
var scriptDir = Path.Combine(_tempDirectory.DirectoryPath, "eng", "scripts");
551+
Directory.CreateDirectory(scriptDir);
552+
File.WriteAllText(Path.Combine(scriptDir, "Pack-Crates.ps1"), "# pack script");
553+
}
554+
555+
#endregion
378556
}

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/Package/PackToolTests.cs

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ public void Setup()
7979
new JavaLanguageService(_mockProcessHelper.Object, _mockGitHelper.Object, _mockMavenHelper.Object, Mock.Of<IPythonHelper>(), mockCopilotAgentRunner.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>()),
8080
new JavaScriptLanguageService(_mockProcessHelper.Object, _mockNpxHelper.Object, Mock.Of<ICopilotAgentRunner>(), _mockGitHelper.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>()),
8181
new GoLanguageService(_mockProcessHelper.Object, _mockPowerShellHelper.Object, _mockGitHelper.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>()),
82-
new DotnetLanguageService(_mockProcessHelper.Object, _mockPowerShellHelper.Object, Mock.Of<ICopilotAgentRunner>(), _mockGitHelper.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>())
82+
new DotnetLanguageService(_mockProcessHelper.Object, _mockPowerShellHelper.Object, Mock.Of<ICopilotAgentRunner>(), _mockGitHelper.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>()),
83+
new RustLanguageService(_mockProcessHelper.Object, _mockPowerShellHelper.Object, _mockGitHelper.Object, languageLogger, _commonValidationHelpers.Object, _mockPackageInfoHelper.Object, Mock.Of<IFileHelper>(), _mockSpecGenSdkConfigHelper.Object, Mock.Of<IChangelogHelper>())
8384
];
8485

8586
_tool = new PackTool(
@@ -501,6 +502,141 @@ public async Task PackAsync_Python_Failure_ReturnsError()
501502

502503
#endregion
503504

505+
#region Rust Pack Tests
506+
507+
[Test]
508+
public async Task PackAsync_Rust_Success_ReturnsSuccessWithArtifact()
509+
{
510+
// Arrange
511+
SetupRustRepo();
512+
SetupRustPackScript();
513+
514+
// Create package in a properly named subdirectory
515+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
516+
Directory.CreateDirectory(packageDir);
517+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
518+
SetupCargoMetadata("azure_core", "0.34.0");
519+
520+
_mockPowerShellHelper
521+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
522+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [(StdioLevel.StandardOutput, "Finished packing crates")] });
523+
524+
// Create the .crate artifact at the expected location
525+
var targetPackageDir = Path.Combine(_tempDirectory.DirectoryPath, "target", "package");
526+
Directory.CreateDirectory(targetPackageDir);
527+
File.WriteAllText(Path.Combine(targetPackageDir, "azure_core-0.34.0.crate"), "fake crate");
528+
529+
// Act
530+
var result = await _tool.PackAsync(null, packageDir);
531+
532+
// Assert
533+
Assert.That(result.ResponseErrors, Is.Null.Or.Empty);
534+
Assert.That(result.Message, Does.Contain("Pack completed successfully"));
535+
}
536+
537+
[Test]
538+
public async Task PackAsync_Rust_WithOutputPath_PassesOutputArg()
539+
{
540+
// Arrange
541+
SetupRustRepo();
542+
SetupRustPackScript();
543+
544+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
545+
Directory.CreateDirectory(packageDir);
546+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
547+
SetupCargoMetadata("azure_core", "0.34.0");
548+
549+
var outputDir = Path.Combine(_tempDirectory.DirectoryPath, "output");
550+
Directory.CreateDirectory(outputDir);
551+
552+
_mockPowerShellHelper
553+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
554+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [] });
555+
556+
// Act
557+
var result = await _tool.PackAsync(null, packageDir, outputDir);
558+
559+
// Assert
560+
Assert.That(result.ResponseErrors, Is.Null.Or.Empty);
561+
_mockPowerShellHelper.Verify(x => x.Run(
562+
It.Is<PowershellOptions>(o =>
563+
o.Args.Contains("-OutputPath") &&
564+
o.Args.Contains(outputDir)),
565+
It.IsAny<CancellationToken>()), Times.Once);
566+
}
567+
568+
[Test]
569+
public async Task PackAsync_Rust_ScriptFails_ReturnsFailure()
570+
{
571+
// Arrange
572+
SetupRustRepo();
573+
SetupRustPackScript();
574+
575+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
576+
Directory.CreateDirectory(packageDir);
577+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
578+
SetupCargoMetadata("azure_core", "0.34.0");
579+
580+
_mockPowerShellHelper
581+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
582+
.ReturnsAsync(new ProcessResult { ExitCode = 1, OutputDetails = [(StdioLevel.StandardError, "cargo publish failed")] });
583+
584+
// Act
585+
var result = await _tool.PackAsync(null, packageDir);
586+
587+
// Assert
588+
Assert.That(result.ResponseErrors?.First(), Does.Contain("Pack failed with exit code 1"));
589+
}
590+
591+
[Test]
592+
public async Task PackAsync_Rust_MissingPackScript_ReturnsFailure()
593+
{
594+
// Arrange
595+
SetupRustRepo();
596+
// Do NOT create the pack script
597+
598+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
599+
Directory.CreateDirectory(packageDir);
600+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
601+
SetupCargoMetadata("azure_core", "0.34.0");
602+
603+
// Act
604+
var result = await _tool.PackAsync(null, packageDir);
605+
606+
// Assert
607+
Assert.That(result.ResponseErrors?.First(), Does.Contain("Pack script not found"));
608+
}
609+
610+
[Test]
611+
public async Task PackAsync_Rust_PassesNoVerifyAndPackageNames()
612+
{
613+
// Arrange
614+
SetupRustRepo();
615+
SetupRustPackScript();
616+
617+
var packageDir = Path.Combine(_tempDirectory.DirectoryPath, "sdk", "core", "azure_core");
618+
Directory.CreateDirectory(packageDir);
619+
File.WriteAllText(Path.Combine(packageDir, "Cargo.toml"), "[package]\nname = \"azure_core\"\n");
620+
SetupCargoMetadata("azure_core", "0.34.0");
621+
622+
_mockPowerShellHelper
623+
.Setup(x => x.Run(It.IsAny<PowershellOptions>(), It.IsAny<CancellationToken>()))
624+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [] });
625+
626+
// Act
627+
await _tool.PackAsync(null, packageDir);
628+
629+
// Assert
630+
_mockPowerShellHelper.Verify(x => x.Run(
631+
It.Is<PowershellOptions>(o =>
632+
o.Args.Contains("-PackageNames") &&
633+
o.Args.Contains("azure_core") &&
634+
o.Args.Contains("-NoVerify")),
635+
It.IsAny<CancellationToken>()), Times.Once);
636+
}
637+
638+
#endregion
639+
504640
#region Exception Handling Tests
505641

506642
[Test]
@@ -598,5 +734,30 @@ private void SetupGoRepo()
598734
.ReturnsAsync(_tempDirectory.DirectoryPath);
599735
}
600736

737+
private void SetupRustRepo()
738+
{
739+
_mockGitHelper
740+
.Setup(x => x.GetRepoNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
741+
.ReturnsAsync("azure-sdk-for-rust");
742+
_mockGitHelper
743+
.Setup(x => x.DiscoverRepoRootAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
744+
.ReturnsAsync(_tempDirectory.DirectoryPath);
745+
}
746+
747+
private void SetupRustPackScript()
748+
{
749+
var scriptDir = Path.Combine(_tempDirectory.DirectoryPath, "eng", "scripts");
750+
Directory.CreateDirectory(scriptDir);
751+
File.WriteAllText(Path.Combine(scriptDir, "Pack-Crates.ps1"), "# pack script");
752+
}
753+
754+
private void SetupCargoMetadata(string name, string version)
755+
{
756+
var json = System.Text.Json.JsonSerializer.Serialize(new { packages = new[] { new { name, version } } });
757+
_mockProcessHelper
758+
.Setup(x => x.Run(It.Is<ProcessOptions>(o => o.Args.Contains("metadata")), It.IsAny<CancellationToken>()))
759+
.ReturnsAsync(new ProcessResult { ExitCode = 0, OutputDetails = [(StdioLevel.StandardOutput, json)] });
760+
}
761+
601762
#endregion
602763
}

0 commit comments

Comments
 (0)