Skip to content

Commit 941891b

Browse files
committed
add verification loop
1 parent da4f6d8 commit 941891b

10 files changed

Lines changed: 1947 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: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
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+
[TearDown]
26+
public async Task TearDown()
27+
{
28+
if (typechecker != null)
29+
{
30+
// Setup mock for container removal during disposal
31+
mockDockerService.Setup(x => x.RemoveContainerAsync(
32+
It.IsAny<string>(), true, It.IsAny<CancellationToken>()))
33+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
34+
35+
await typechecker.DisposeAsync();
36+
}
37+
}
38+
39+
[Test]
40+
public void Constructor_WithNullDockerService_ThrowsArgumentNullException()
41+
{
42+
Assert.Throws<ArgumentNullException>(() =>
43+
new TypeScriptTypechecker(null!, NullLogger<TypeScriptTypechecker>.Instance));
44+
}
45+
46+
[Test]
47+
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
48+
{
49+
Assert.Throws<ArgumentNullException>(() =>
50+
new TypeScriptTypechecker(mockDockerService.Object, null!));
51+
}
52+
53+
[Test]
54+
public void ParseImportedPackages_WithBasicImports_ReturnsExpectedPackages()
55+
{
56+
var code = @"
57+
import { Client } from '@azure/storage-blob';
58+
import * as fs from 'fs';
59+
import './relative-file';
60+
import { DefaultAzureCredential } from '@azure/identity';
61+
const path = require('path');
62+
";
63+
64+
var excludedPackages = new HashSet<string>();
65+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
66+
67+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
68+
Assert.That(packages, Contains.Item("@azure/identity"));
69+
Assert.That(packages, Contains.Item("fs"));
70+
Assert.That(packages, Contains.Item("path"));
71+
Assert.That(packages, Contains.Item("typescript"));
72+
Assert.That(packages, Contains.Item("@types/node"));
73+
74+
// Should not contain relative imports
75+
Assert.That(packages, Does.Not.Contain("./relative-file"));
76+
}
77+
78+
[Test]
79+
public void ParseImportedPackages_WithScopedPackages_ExtractsCorrectPackageRoot()
80+
{
81+
var code = @"
82+
import { Client } from '@azure/storage-blob/types';
83+
import { helper } from '@azure/core-auth/helpers/utils';
84+
import { normalPkg } from 'normal-package/sub/path';
85+
";
86+
87+
var excludedPackages = new HashSet<string>();
88+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
89+
90+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
91+
Assert.That(packages, Contains.Item("@azure/core-auth"));
92+
Assert.That(packages, Contains.Item("normal-package"));
93+
}
94+
95+
[Test]
96+
public void ParseImportedPackages_WithExcludedPackages_FiltersCorrectly()
97+
{
98+
var code = @"
99+
import { Client } from '@azure/storage-blob';
100+
import { DefaultAzureCredential } from '@azure/identity';
101+
";
102+
103+
var excludedPackages = new HashSet<string> { "@azure/identity" };
104+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
105+
106+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
107+
Assert.That(packages, Does.Not.Contain("@azure/identity"));
108+
}
109+
110+
[Test]
111+
public void ParseImportedPackages_WithDynamicImports_ExtractsPackages()
112+
{
113+
var code = @"
114+
const module = await import('@azure/storage-blob');
115+
const fs = await import('fs/promises');
116+
";
117+
118+
var excludedPackages = new HashSet<string>();
119+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
120+
121+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
122+
Assert.That(packages, Contains.Item("fs"));
123+
}
124+
125+
[Test]
126+
public void ParseImportedPackages_WithBarePackageNames_ExtractsCorrectly()
127+
{
128+
var code = @"
129+
import fs from 'fs';
130+
import path from 'path';
131+
import crypto from 'crypto';
132+
import { Client } from '@azure/storage-blob';
133+
";
134+
135+
var excludedPackages = new HashSet<string>();
136+
var packages = TypeScriptTypechecker.ParseImportedPackages(code, excludedPackages);
137+
138+
// Verify bare package names (Node.js built-ins) are extracted correctly
139+
Assert.That(packages, Contains.Item("fs"));
140+
Assert.That(packages, Contains.Item("path"));
141+
Assert.That(packages, Contains.Item("crypto"));
142+
Assert.That(packages, Contains.Item("@azure/storage-blob"));
143+
}
144+
145+
[Test]
146+
public async Task TypecheckAsync_WithValidCode_ReturnsSuccess()
147+
{
148+
// Arrange
149+
var code = "const message: string = 'Hello, World!';";
150+
var parameters = new TypeCheckRequest(code, null, null);
151+
152+
SetupSuccessfulDockerInteractions();
153+
154+
// Act
155+
var result = await typechecker.TypecheckAsync(parameters, CancellationToken.None);
156+
157+
// Assert
158+
Assert.That(result.Succeeded, Is.True);
159+
VerifyDockerInteractionSequence();
160+
}
161+
162+
[Test]
163+
public async Task TypecheckAsync_WithFailedTypeCheck_ReturnsFailure()
164+
{
165+
// Arrange
166+
var code = "const message: string = 123; // Type error";
167+
var parameters = new TypeCheckRequest(code, null, null);
168+
169+
SetupFailedTypecheckInteraction();
170+
171+
// Act
172+
var result = await typechecker.TypecheckAsync(parameters, CancellationToken.None);
173+
174+
// Assert
175+
Assert.That(result.Succeeded, Is.False);
176+
Assert.That(result.Output, Contains.Substring("tsc output:"));
177+
}
178+
179+
[Test]
180+
public async Task TypecheckAsync_WithClientDist_InstallsClientPackage()
181+
{
182+
// Arrange
183+
var code = "const message: string = 'Hello, World!';";
184+
var clientDist = "/path/to/client.tgz";
185+
var parameters = new TypeCheckRequest(code, clientDist, null);
186+
187+
SetupSuccessfulDockerInteractions();
188+
SetupClientDistInstallation(clientDist);
189+
190+
// Create a mock temporary file to simulate the client dist file existing
191+
var tempFile = Path.GetTempFileName();
192+
try
193+
{
194+
await File.WriteAllTextAsync(tempFile, "mock client dist content");
195+
196+
// Update the test to use the actual temp file path
197+
var parametersWithRealFile = new TypeCheckRequest(code, tempFile, null);
198+
199+
// Act
200+
var result = await typechecker.TypecheckAsync(parametersWithRealFile, CancellationToken.None);
201+
202+
// Assert
203+
Assert.That(result.Succeeded, Is.True);
204+
VerifyClientDistInstallation(tempFile);
205+
}
206+
finally
207+
{
208+
if (File.Exists(tempFile))
209+
{
210+
File.Delete(tempFile);
211+
}
212+
}
213+
}
214+
215+
private void SetupSuccessfulDockerInteractions()
216+
{
217+
// Container creation and startup
218+
var createResult = new ProcessResult { ExitCode = 0 };
219+
createResult.AppendStdout("container-id");
220+
mockDockerService.Setup(x => x.CreateContainerAsync(
221+
"node:alpine", It.IsAny<string>(), null, null, It.IsAny<CancellationToken>()))
222+
.ReturnsAsync(createResult);
223+
224+
mockDockerService.Setup(x => x.StartContainerAsync(
225+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
226+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
227+
228+
mockDockerService.Setup(x => x.IsContainerRunningAsync(
229+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
230+
.ReturnsAsync(false); // Force container creation
231+
232+
// File operations
233+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
234+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("mkdir")),
235+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
236+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
237+
238+
mockDockerService.Setup(x => x.CopyToContainerAsync(
239+
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
240+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
241+
242+
// npm install
243+
var npmInstallResult = new ProcessResult { ExitCode = 0 };
244+
npmInstallResult.AppendStdout("npm install completed");
245+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
246+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "install" })),
247+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
248+
.ReturnsAsync(npmInstallResult);
249+
250+
// TypeScript compilation
251+
var tscResult = new ProcessResult { ExitCode = 0 };
252+
tscResult.AppendStdout("Compilation successful");
253+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
254+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
255+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
256+
.ReturnsAsync(tscResult);
257+
258+
// Cleanup
259+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
260+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("rm")),
261+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
262+
.ReturnsAsync(new ProcessResult { ExitCode = 0 });
263+
}
264+
265+
private void SetupFailedTypecheckInteraction()
266+
{
267+
SetupSuccessfulDockerInteractions();
268+
269+
// Override TypeScript compilation to fail
270+
var failedTscResult = new ProcessResult { ExitCode = 1 };
271+
failedTscResult.AppendStderr("Type error: string is not assignable to number");
272+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
273+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
274+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
275+
.ReturnsAsync(failedTscResult);
276+
}
277+
278+
private void SetupClientDistInstallation(string clientDist)
279+
{
280+
// Client dist installation
281+
var installResult = new ProcessResult { ExitCode = 0 };
282+
installResult.AppendStdout("Client package installed");
283+
mockDockerService.Setup(x => x.RunCommandInContainerAsync(
284+
It.IsAny<string>(), It.Is<string[]>(args =>
285+
args.Length >= 4 && args[0] == "npm" && args[1] == "install" && args[2] == "--no-save"),
286+
It.IsAny<string>(), It.IsAny<CancellationToken>()))
287+
.ReturnsAsync(installResult);
288+
}
289+
290+
private void VerifyDockerInteractionSequence()
291+
{
292+
// Verify container creation
293+
mockDockerService.Verify(x => x.CreateContainerAsync(
294+
"node:alpine", It.IsAny<string>(), null, null, It.IsAny<CancellationToken>()),
295+
Times.Once);
296+
297+
// Verify npm install
298+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
299+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "install" })),
300+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
301+
Times.Once);
302+
303+
// Verify TypeScript compilation
304+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
305+
It.IsAny<string>(), It.Is<string[]>(args => args.SequenceEqual(new[] { "npm", "run", "typecheck" })),
306+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
307+
Times.Once);
308+
309+
// Verify cleanup
310+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
311+
It.IsAny<string>(), It.Is<string[]>(args => args.Contains("rm")),
312+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
313+
Times.Once);
314+
}
315+
316+
private void VerifyClientDistInstallation(string clientDist)
317+
{
318+
var fileName = Path.GetFileName(clientDist);
319+
320+
// Verify client dist copy
321+
mockDockerService.Verify(x => x.CopyToContainerAsync(
322+
It.IsAny<string>(), clientDist, It.IsAny<string>(), It.IsAny<CancellationToken>()),
323+
Times.Once);
324+
325+
// Verify client dist installation
326+
mockDockerService.Verify(x => x.RunCommandInContainerAsync(
327+
It.IsAny<string>(), It.Is<string[]>(args =>
328+
args.Length >= 4 && args[0] == "npm" && args[1] == "install" && args[2] == "--no-save" && args[3] == fileName),
329+
It.IsAny<string>(), It.IsAny<CancellationToken>()),
330+
Times.Once);
331+
}
332+
}
333+
}

0 commit comments

Comments
 (0)