Skip to content

Commit a1de118

Browse files
[azsdk-cli] Add in some checks and niceties to make the tsp init command a bit easier for new users (#11956)
Add in some checks and niceties to make this command a bit easier for a novice to use: - Check that the output path we're choosing is under the azure-rest-api-specs folder, and that it's under specification - Create the directory if it doesn't already exist - Provide some next steps that take into account the folder they created the project in.
1 parent fdeb1f7 commit a1de118

6 files changed

Lines changed: 215 additions & 36 deletions

File tree

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Helpers/TypeSpecHelperTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,38 @@ public void Test_GetSpecRepoPath()
4848
Assert.That(result.EndsWith("TypeSpecTestData"), Is.True);
4949
}
5050

51+
[TestCase("https://github.com/Azure/azure-rest-api-specs.git")]
52+
[TestCase("https://github.com/Azure/azure-rest-api-specs")]
53+
[TestCase("https://github.com/myuser/azure-rest-api-specs.git")]
54+
[TestCase("https://github.com/Azure/azure-rest-api-specs-pr.git")]
55+
[TestCase("https://github.com/myuser/azure-rest-api-specs-pr.git")]
56+
[TestCase("git@github.com:Azure/azure-rest-api-specs.git")]
57+
[TestCase("git@github.com:myuser/azure-rest-api-specs.git")]
58+
[Test]
59+
public void Test_IsRepoPathForSpecRepo(Uri repo)
60+
{
61+
var gitHelper = CreateGitHelper(repo);
62+
var helper = new TypeSpecHelper(gitHelper);
63+
Assert.That(helper.IsRepoPathForSpecRepo("unused because of mock"), "is a specs repo (public or private)");
64+
}
65+
66+
[TestCase("https://github.com/Azure/azure-rest-api-specs-pr.git")]
67+
[TestCase("https://github.com/myuser/azure-rest-api-specs-pr.git")]
68+
[TestCase("git@github.com:Azure/azure-rest-api-specs-pr.git")]
69+
[TestCase("git@github.com:myuser/azure-rest-api-specs-pr.git")]
70+
[TestCase("git@github.com:Azure/azure-sdk-for-php.git")]
71+
[Test]
72+
public void Test_IsRepoPathForPublicSpecRepo(Uri repo)
73+
{
74+
var helper = new TypeSpecHelper(CreateGitHelper(repo));
75+
Assert.That(!helper.IsRepoPathForPublicSpecRepo("unused because of the mock"), "not the public specs repo");
76+
}
77+
78+
private static IGitHelper CreateGitHelper(Uri getRepoRemoteUri)
79+
{
80+
var gitHelperMock = new Mock<IGitHelper>();
81+
gitHelperMock.Setup(ghm => ghm.GetRepoRemoteUri(It.IsAny<string>())).Returns(getRepoRemoteUri);
82+
return gitHelperMock.Object;
83+
}
5184
}
5285
}

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/TypeSpec/TspInitToolTest.cs

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void GetCommand_ShouldReturnCommand()
1616
var npxHelper = new Mock<INpxHelper>().Object;
1717
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
1818
var outputService = new Mock<IOutputHelper>().Object;
19-
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
19+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);
2020

2121
// Act
2222
var command = tool.GetCommand();
@@ -34,7 +34,7 @@ public async Task Init_WithInvalidTemplate_ShouldReturnError()
3434
var npxHelper = new Mock<INpxHelper>().Object;
3535
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
3636
var outputService = new Mock<IOutputHelper>().Object;
37-
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
37+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);
3838

3939
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "invalid-template", serviceNamespace: "MyService", isCli: false);
4040

@@ -52,7 +52,7 @@ public async Task Init_WithInvalidServiceNamespace_ShouldReturnError()
5252
var npxHelper = new Mock<INpxHelper>().Object;
5353
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
5454
var outputService = new Mock<IOutputHelper>().Object;
55-
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
55+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);
5656

5757
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "azure-core", serviceNamespace: "", isCli: false);
5858

@@ -64,20 +64,91 @@ public async Task Init_WithInvalidServiceNamespace_ShouldReturnError()
6464
}
6565

6666
[Test]
67-
public async Task Init_WithNonExistentDirectory_ShouldReturnError()
67+
public async Task Init_WithNonEmptyDirectory_ShouldReturnError()
6868
{
6969
var npxHelper = new Mock<INpxHelper>().Object;
7070
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
7171
var outputService = new Mock<IOutputHelper>().Object;
72-
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
72+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(), logger, outputService);
73+
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");
7374

74-
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}"), template: "azure-core", serviceNamespace: "MyService", isCli: false);
75+
Directory.CreateDirectory(tempDir);
7576

76-
Assert.Multiple(() =>
77+
try
7778
{
78-
Assert.That(result.IsSuccessful, Is.False);
79-
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
80-
});
79+
await File.WriteAllTextAsync(Path.Join(tempDir, "somefile.txt"), "some file's contents");
80+
81+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);
82+
83+
Assert.Multiple(() =>
84+
{
85+
Assert.That(result.IsSuccessful, Is.False);
86+
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
87+
});
88+
}
89+
finally
90+
{
91+
Directory.Delete(tempDir, true);
92+
}
93+
}
94+
95+
[Test]
96+
public async Task Init_IncorrectGitRepo()
97+
{
98+
var npxHelper = new Mock<INpxHelper>().Object;
99+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
100+
var outputService = new Mock<IOutputHelper>().Object;
101+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(false), logger, outputService);
102+
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");
103+
104+
try
105+
{
106+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);
107+
108+
Assert.Multiple(() =>
109+
{
110+
Assert.That(result.IsSuccessful, Is.False);
111+
Assert.That(result.ResponseError, Is.EqualTo($"Failed: Invalid --output-directory, must be under the azure-rest-api-specs or azure-rest-api-specs-pr repo"
112+
));
113+
});
114+
}
115+
finally
116+
{
117+
Directory.Delete(tempDir, true);
118+
}
119+
}
120+
121+
[Test]
122+
public async Task Init_NotUnderSpecifications()
123+
{
124+
var npxHelper = new Mock<INpxHelper>().Object;
125+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
126+
var outputService = new Mock<IOutputHelper>().Object;
127+
var tool = new TypeSpecInitTool(npxHelper, CreateTypeSpecHelper(true), logger, outputService);
128+
var tempDir = Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}");
129+
130+
try
131+
{
132+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: tempDir, template: "azure-core", serviceNamespace: "MyService", isCli: false);
133+
134+
Assert.Multiple(() =>
135+
{
136+
Assert.That(result.IsSuccessful, Is.False);
137+
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
138+
Assert.That(result.ResponseError, Is.EqualTo($"Failed: Invalid --output-directory, must be under <azure-rest-api-specs or azure-rest-api-specs-pr>{Path.DirectorySeparatorChar}specification"));
139+
});
140+
}
141+
finally
142+
{
143+
Directory.Delete(tempDir, true);
144+
}
145+
}
146+
147+
private static ITypeSpecHelper CreateTypeSpecHelper(bool isSpecRepo = false)
148+
{
149+
var mock = new Mock<ITypeSpecHelper>();
150+
mock.Setup(m => m.IsRepoPathForSpecRepo(It.IsAny<string>())).Returns(isSpecRepo);
151+
return mock.Object;
81152
}
82153
}
83154
}

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public interface IGitHelper
1616
public string DiscoverRepoRoot(string path);
1717
public string GetRepoName(string path);
1818
}
19+
1920
public class GitHelper(IGitHubService gitHubService, ILogger<GitHelper> logger) : IGitHelper
2021
{
2122
private readonly ILogger<GitHelper> logger = logger;
@@ -66,7 +67,8 @@ public async Task<string> GetRepoOwnerNameAsync(string path, bool findUpstreamPa
6667
repoName = segments[^1].TrimEnd(".git".ToCharArray());
6768
}
6869

69-
if(findUpstreamParent) {
70+
if (findUpstreamParent)
71+
{
7072
// Check if the repo is a fork and get the parent repo
7173
var parentRepoUrl = await gitHubService.GetGitHubParentRepoUrlAsync(repoOwner, repoName);
7274
logger.LogDebug($"Parent repo URL: {parentRepoUrl}");
@@ -86,7 +88,7 @@ public async Task<string> GetRepoOwnerNameAsync(string path, bool findUpstreamPa
8688
}
8789

8890
throw new InvalidOperationException("Unable to determine repository owner.");
89-
}
91+
}
9092

9193
public string DiscoverRepoRoot(string path)
9294
{
@@ -95,7 +97,7 @@ public string DiscoverRepoRoot(string path)
9597
{
9698
throw new InvalidOperationException($"No git repository found at or above the path: {path}");
9799
}
98-
100+
99101
// Repository.Discover returns the path to .git directory
100102
// The repository root is the parent directory of .git
101103
var gitDir = new DirectoryInfo(repoPath);

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/TypeSpecHelper.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3+
using System.Text.RegularExpressions;
34
using Azure.Sdk.Tools.Cli.Models;
45

56
namespace Azure.Sdk.Tools.Cli.Helpers
@@ -8,14 +9,35 @@ public interface ITypeSpecHelper
89
{
910
public bool IsValidTypeSpecProjectPath(string path);
1011
public bool IsTypeSpecProjectForMgmtPlane(string Path);
12+
13+
/// <summary>
14+
/// Checks if the path is within either the azure-rest-api-specs repo.
15+
/// This should also work for forks of these repos.
16+
/// </summary>
17+
/// <param name="path">Path within a repo</param>
18+
/// <returns>true if within the azure-rest-api-specs repo, false otherwise</returns>
1119
public bool IsRepoPathForPublicSpecRepo(string path);
20+
21+
/// <summary>
22+
/// Checks if the path is within either the azure-rest-api-specs or azure-rest-api-specs-pr repo.
23+
/// This should also work for forks of these repos.
24+
/// </summary>
25+
/// <param name="path">Path within a repo</param>
26+
/// <returns>true if one of our specs repos, false otherwise</returns>
27+
public bool IsRepoPathForSpecRepo(string path);
28+
1229
public string GetSpecRepoRootPath(string path);
1330
public string GetTypeSpecProjectRelativePath(string typeSpecProjectPath);
1431
}
15-
public class TypeSpecHelper : ITypeSpecHelper
32+
public partial class TypeSpecHelper : ITypeSpecHelper
1633
{
34+
[GeneratedRegex("azure-rest-api-specs(-pr){0,1}(.git){0,1}$")]
35+
private static partial Regex RestApiSpecsPublicOrPrivateRegex();
36+
37+
[GeneratedRegex("azure-rest-api-specs{0,1}(.git){0,1}$")]
38+
private static partial Regex RestApiSpecsPublicRegex();
39+
1740
private IGitHelper _gitHelper;
18-
private static readonly string SPEC_REPO_NAME = "azure-rest-api-specs";
1941

2042
public TypeSpecHelper(IGitHelper gitHelper)
2143
{
@@ -36,7 +58,13 @@ public bool IsTypeSpecProjectForMgmtPlane(string Path)
3658
public bool IsRepoPathForPublicSpecRepo(string path)
3759
{
3860
var uri = _gitHelper.GetRepoRemoteUri(path);
39-
return uri.ToString().Contains(SPEC_REPO_NAME);
61+
return RestApiSpecsPublicRegex().IsMatch(uri.ToString());
62+
}
63+
64+
public bool IsRepoPathForSpecRepo(string path)
65+
{
66+
var uri = _gitHelper.GetRepoRemoteUri(path);
67+
return RestApiSpecsPublicOrPrivateRegex().IsMatch(uri.ToString());
4068
}
4169

4270
public string GetSpecRepoRootPath(string path)

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/Responses/TspToolResponse.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ public override string ToString()
2020
}
2121
else
2222
{
23-
return $"### TypeSpec Project Path: {TypeSpecProjectPath}";
23+
return string.Join(
24+
Environment.NewLine,
25+
[
26+
$"### TypeSpec Project Path: {TypeSpecProjectPath}",
27+
string.Empty,
28+
..this.NextSteps ?? Enumerable.Empty<string>()
29+
]
30+
);
2431
}
2532
}
2633
}

0 commit comments

Comments
 (0)