Skip to content

Commit dd3a7a2

Browse files
committed
Implement verification loop
1 parent d79f309 commit dd3a7a2

10 files changed

Lines changed: 1827 additions & 18 deletions

File tree

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

Lines changed: 487 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Sdk.Tools.Cli.Helpers;
5+
using Azure.Sdk.Tools.Cli.Services;
6+
using Azure.Sdk.Tools.Cli.Services.Languages;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
using Moq;
9+
10+
namespace Azure.Sdk.Tools.Cli.Tests.Services.Languages
11+
{
12+
[TestFixture]
13+
public class TypeScriptTypecheckerTests
14+
{
15+
private Mock<IDockerService> mockDockerService;
16+
private TypeScriptTypechecker typechecker;
17+
18+
[SetUp]
19+
public void SetUp()
20+
{
21+
mockDockerService = new Mock<IDockerService>();
22+
typechecker = new TypeScriptTypechecker(mockDockerService.Object, NullLogger<TypeScriptTypechecker>.Instance);
23+
}
24+
25+
[Test]
26+
public void Constructor_WithNullDockerService_ThrowsArgumentNullException()
27+
{
28+
Assert.Throws<ArgumentNullException>(() =>
29+
new TypeScriptTypechecker(null!, NullLogger<TypeScriptTypechecker>.Instance));
30+
}
31+
32+
[Test]
33+
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
34+
{
35+
Assert.Throws<ArgumentNullException>(() =>
36+
new TypeScriptTypechecker(mockDockerService.Object, null!));
37+
}
38+
39+
[Test]
40+
public void ParseImportedPackages_WithBasicImports_ReturnsExpectedPackages()
41+
{
42+
var code = @"
43+
import { Client } from '@azure/storage-blob';
44+
import * as fs from 'fs';
45+
import './relative-file';
46+
import { DefaultAzureCredential } from '@azure/identity';
47+
const path = require('path');
48+
";
49+
50+
var excludedPackages = new HashSet<string>();
51+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
52+
53+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
54+
Assert.That(packages, Contains.Item("@azure/identity"));
55+
Assert.That(packages, Contains.Item("fs"));
56+
Assert.That(packages, Contains.Item("path"));
57+
Assert.That(packages, Contains.Item("typescript"));
58+
Assert.That(packages, Contains.Item("@types/node"));
59+
60+
// Should not contain relative imports
61+
Assert.That(packages, Does.Not.Contain("./relative-file"));
62+
}
63+
64+
[Test]
65+
public void ParseImportedPackages_WithScopedPackages_ExtractsCorrectPackageRoot()
66+
{
67+
var code = @"
68+
import { Client } from '@azure/storage-blob/types';
69+
import { helper } from '@azure/core-auth/helpers/utils';
70+
import { normalPkg } from 'normal-package/sub/path';
71+
";
72+
73+
var excludedPackages = new HashSet<string>();
74+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
75+
76+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
77+
Assert.That(packages, Contains.Item("@azure/core-auth"));
78+
Assert.That(packages, Contains.Item("normal-package"));
79+
}
80+
81+
[Test]
82+
public void ParseImportedPackages_WithExcludedPackages_FiltersCorrectly()
83+
{
84+
var code = @"
85+
import { Client } from '@azure/storage-blob';
86+
import { DefaultAzureCredential } from '@azure/identity';
87+
";
88+
89+
var excludedPackages = new HashSet<string> { "@azure/identity" };
90+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
91+
92+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
93+
Assert.That(packages, Does.Not.Contain("@azure/identity"));
94+
}
95+
96+
[Test]
97+
public void ParseImportedPackages_WithDynamicImports_ExtractsPackages()
98+
{
99+
var code = @"
100+
const module = await import('@azure/storage-blob');
101+
const fs = await import('fs/promises');
102+
";
103+
104+
var excludedPackages = new HashSet<string>();
105+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
106+
107+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
108+
Assert.That(packages, Contains.Item("fs"));
109+
}
110+
111+
[Test]
112+
public async Task TypecheckAsync_WithValidCode_ReturnsSuccess()
113+
{
114+
// Arrange
115+
var code = "const message: string = 'Hello, World!';";
116+
var parameters = new TypeCheckParameters(code, null, null);
117+
118+
SetupSuccessfulDockerInteractions();
119+
120+
// Act
121+
var result = await typechecker.TypecheckAsync(parameters, CancellationToken.None);
122+
123+
// Assert
124+
Assert.That(result.Succeeded, Is.True);
125+
VerifyDockerInteractionSequence();
126+
}
127+
128+
[Test]
129+
public async Task TypecheckAsync_WithFailedTypeCheck_ReturnsFailure()
130+
{
131+
// Arrange
132+
var code = "const message: string = 123; // Type error";
133+
var parameters = new TypeCheckParameters(code, null, null);
134+
135+
SetupFailedTypecheckInteraction();
136+
137+
// Act
138+
var result = await typechecker.TypecheckAsync(parameters, CancellationToken.None);
139+
140+
// Assert
141+
Assert.That(result.Succeeded, Is.False);
142+
Assert.That(result.Output, Contains.Substring("tsc output:"));
143+
}
144+
145+
[Test]
146+
public async Task TypecheckAsync_WithClientDist_InstallsClientPackage()
147+
{
148+
// Arrange
149+
var code = "const message: string = 'Hello, World!';";
150+
var clientDist = "/path/to/client.tgz";
151+
var parameters = new TypeCheckParameters(code, clientDist, null);
152+
153+
SetupSuccessfulDockerInteractions();
154+
SetupClientDistInstallation(clientDist);
155+
156+
// Create a mock temporary file to simulate the client dist file existing
157+
var tempFile = Path.GetTempFileName();
158+
try
159+
{
160+
await File.WriteAllTextAsync(tempFile, "mock client dist content");
161+
162+
// Update the test to use the actual temp file path
163+
var parametersWithRealFile = new TypeCheckParameters(code, tempFile, null);
164+
165+
// Act
166+
var result = await typechecker.TypecheckAsync(parametersWithRealFile, CancellationToken.None);
167+
168+
// Assert
169+
Assert.That(result.Succeeded, Is.True);
170+
VerifyClientDistInstallation(tempFile);
171+
}
172+
finally
173+
{
174+
if (File.Exists(tempFile))
175+
{
176+
File.Delete(tempFile);
177+
}
178+
}
179+
}
180+
181+
private void SetupSuccessfulDockerInteractions()
182+
{
183+
// Container creation and startup
184+
var createResult = new ProcessResult { ExitCode = 0 };
185+
createResult.AppendStdout("container-id");
186+
mockDockerService.Setup(x => x.CreateContainerAsync(
187+
"node:alpine", It.IsAny<string>(), null, null, It.IsAny<CancellationToken>()))
188+
.ReturnsAsync(createResult);
189+
190+
mockDockerService.Setup(x => x.StartContainerAsync(
191+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
192+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
193+
194+
mockDockerService.Setup(x => x.IsContainerRunningAsync(
195+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
196+
.ReturnsAsync(false); // Force container creation
197+
198+
// File operations
199+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
200+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("mkdir")),
201+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
202+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
203+
204+
mockDockerService.Setup(x => x.CopyToContainerAsync(
205+
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
206+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
207+
208+
// npm install
209+
var npmInstallResult = new ProcessResult { ExitCode = 0 };
210+
npmInstallResult.AppendStdout("npm install completed");
211+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
212+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "install" })),
213+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
214+
.ReturnsAsync(npmInstallResult);
215+
216+
// TypeScript compilation
217+
var tscResult = new ProcessResult { ExitCode = 0 };
218+
tscResult.AppendStdout("Compilation successful");
219+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
220+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
221+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
222+
.ReturnsAsync(tscResult);
223+
224+
// Cleanup
225+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
226+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("rm")),
227+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
228+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
229+
}
230+
231+
private void SetupFailedTypecheckInteraction()
232+
{
233+
SetupSuccessfulDockerInteractions();
234+
235+
// Override TypeScript compilation to fail
236+
var failedTscResult = new ProcessResult { ExitCode = 1 };
237+
failedTscResult.AppendStderr("Type error: string is not assignable to number");
238+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
239+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
240+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
241+
.ReturnsAsync(failedTscResult);
242+
}
243+
244+
private void SetupClientDistInstallation(string clientDist)
245+
{
246+
// Client dist installation
247+
var installResult = new ProcessResult { ExitCode = 0 };
248+
installResult.AppendStdout("Client package installed");
249+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
250+
It.IsAny<string>(), It.Is<string[]>(args =>
251+
args.Length >= 4 && args[0] == "npm" && args[1] == "install" && args[2] == "--no-save"),
252+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
253+
.ReturnsAsync(installResult);
254+
}
255+
256+
private void VerifyDockerInteractionSequence()
257+
{
258+
// Verify container creation
259+
mockDockerService.Verify(x => x.CreateContainerAsync(
260+
"node:alpine", It.IsAny<string>(), null, null, It.IsAny<CancellationToken>()),
261+
Times.Once);
262+
263+
// Verify npm install
264+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
265+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "install" })),
266+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
267+
Times.Once);
268+
269+
// Verify TypeScript compilation
270+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
271+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
272+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
273+
Times.Once);
274+
275+
// Verify cleanup
276+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
277+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("rm")),
278+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
279+
Times.Once);
280+
}
281+
282+
private void VerifyClientDistInstallation(string clientDist)
283+
{
284+
var fileName = Path.GetFileName(clientDist);
285+
286+
// Verify client dist copy
287+
mockDockerService.Verify(x => x.CopyToContainerAsync(
288+
It.IsAny<string>(), clientDist, It.IsAny<string>(), It.IsAny<CancellationToken>()),
289+
Times.Once);
290+
291+
// Verify client dist installation
292+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
293+
It.IsAny<string>(), It.Is<string[]>(args =>
294+
args.Length >= 4 && args[0] == "npm" && args[1] == "install" && args[2] == "--no-save" && args[3] == fileName),
295+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
296+
Times.Once);
297+
}
298+
}
299+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Sdk.Tools.Cli.Microagents;
5+
using Azure.Sdk.Tools.Cli.Services;
6+
using Azure.Sdk.Tools.Cli.Tools.Package;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using Moq;
10+
11+
namespace Azure.Sdk.Tools.Cli.Tests.Services
12+
{
13+
[TestFixture]
14+
public class SampleVerificationTests
15+
{
16+
private Mock<IDockerService> mockDockerService;
17+
private Mock<IMicroagentHostService> mockMicroagentService;
18+
private ILogger<SampleVerificationTests> logger;
19+
20+
[SetUp]
21+
public void SetUp()
22+
{
23+
mockDockerService = new Mock<IDockerService>();
24+
mockMicroagentService = new Mock<IMicroagentHostService>();
25+
logger = NullLogger<SampleVerificationTests>.Instance;
26+
}
27+
28+
[Test]
29+
public async Task VerifyAndFixSampleAsync_WithDockerNotAvailable_ReturnsFailureResult()
30+
{
31+
// Arrange
32+
var sample = new GeneratedSample("test.ts", "const x = 1;");
33+
mockDockerService.Setup(x => x.IsDockerAvailableAsync(It.IsAny<CancellationToken>()))
34+
.ReturnsAsync(false);
35+
36+
// Act
37+
var result = await SampleVerification.VerifyAndFixSampleAsync(
38+
sample, "typescript", mockDockerService.Object, mockMicroagentService.Object,
39+
logger, null, null, CancellationToken.None);
40+
41+
// Assert
42+
Assert.That(result.Succeeded, Is.False);
43+
Assert.That(result.Content, Is.EqualTo(sample.Content));
44+
Assert.That(result.AttemptsMade, Is.EqualTo(0));
45+
Assert.That(result.Attempts.Count, Is.EqualTo(1));
46+
Assert.That(result.Attempts[0].TypeCheckOutput, Contains.Substring("Docker is not available"));
47+
}
48+
49+
[Test]
50+
public async Task VerifyAndFixSampleAsync_WithUnsupportedLanguage_ReturnsFailureResult()
51+
{
52+
// Arrange
53+
var sample = new GeneratedSample("test.unsupported", "const x = 1;");
54+
mockDockerService.Setup(x => x.IsDockerAvailableAsync(It.IsAny<CancellationToken>()))
55+
.ReturnsAsync(true);
56+
57+
// Act
58+
var result = await SampleVerification.VerifyAndFixSampleAsync(
59+
sample, "unsupported", mockDockerService.Object, mockMicroagentService.Object,
60+
logger, null, null, CancellationToken.None);
61+
62+
// Assert
63+
Assert.That(result.Succeeded, Is.False);
64+
Assert.That(result.Content, Is.EqualTo(sample.Content));
65+
Assert.That(result.AttemptsMade, Is.EqualTo(0));
66+
Assert.That(result.Attempts.Count, Is.EqualTo(1));
67+
Assert.That(result.Attempts[0].TypeCheckOutput, Contains.Substring("not supported for verification"));
68+
}
69+
}
70+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Sdk.Tools.Cli.Services.Languages
5+
{
6+
/// <summary>
7+
/// Interface for language-specific type checkers.
8+
/// </summary>
9+
internal interface ILanguageTypechecker
10+
{
11+
/// <summary>
12+
/// Performs type checking on the provided code.
13+
/// </summary>
14+
/// <param name="parameters">Type check parameters</param>
15+
/// <param name="ct">Cancellation token</param>
16+
/// <returns>Type check result</returns>
17+
Task<TypeCheckResult> TypecheckAsync(TypeCheckParameters parameters, CancellationToken ct);
18+
}
19+
}

0 commit comments

Comments
 (0)