Skip to content

Commit 458777a

Browse files
committed
Address Copilot review comments on PR 4242
- Fix net481 build: add IsExternalInit polyfill, GetRelativePath helper, and Split overload compat for net481 in BuildVersionOverrideTests - Use PackageVersions constants instead of hard-coded version strings in EntryPointTests.HelpOutputContainsDefaultVersions (fix #1) - Fix WaitForExit deadlock risk on net481: read stdout/stderr async before waiting; check timeout bool and kill/throw on expiry (fix #2) - Fix Directory.Packages.props typo: explcitily -> explicitly (fix #4) - Fix README.md typo: -p:SqlServerVersio= -> -p:SqlServerVersion= (fix #5) - Move WaitForCapturedOutput inside try block so late console output is captured before streams are restored (fix #6) - Use ArgumentList on NET and QuoteArgument helper on net481 so paths with spaces are passed correctly to dotnet build (fix #7) - Fix testing.instructions.md list continuation indents (4-space -> 2-space) so GitHub renders them as wrapped text not code blocks (fix #8)
1 parent 9241684 commit 458777a

6 files changed

Lines changed: 92 additions & 25 deletions

File tree

.github/instructions/testing.instructions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,16 @@ class and method level.
182182
- Add XML `<summary>` comments to every test class.
183183
- Add XML `<summary>` comments to every test method (`[Fact]`, `[Theory]`, conditional variants).
184184
- For helper methods used by tests, add XML `<summary>` comments and XML `<param>` / `<returns>`
185-
where applicable.
185+
where applicable.
186186
- For fixture and collection types, add XML `<summary>` comments describing why the fixture exists
187-
(for example, serialization of console-mutating tests).
187+
(for example, serialization of console-mutating tests).
188188

189189
### What the Comments Must Explain
190190
- The behavior/contract being tested (not just restating the method name).
191191
- Why the scenario matters (for example: regression guard, parsing contract, sync/async parity,
192-
isolation requirement).
192+
isolation requirement).
193193
- For helper methods, what side effects occur (for example console redirection, file system
194-
copying, process execution) and why they are needed.
194+
copying, process execution) and why they are needed.
195195

196196
### Style Guidance
197197
- Keep comments concise and factual.

tools/PackageCompatibility/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<!-- SqlClient Packages -->
1818
<ItemGroup>
1919
<!--
20-
Normal apps wouldn't explcitily reference these 2 packages, but we do here to force the
20+
Normal apps wouldn't explicitly reference these 2 packages, but we do here to force the
2121
desired versions. This helps detect version conflicts.
2222
-->
2323
<PackageVersion Include="Microsoft.Data.SqlClient.Extensions.Abstractions" Version="$(AbstractionsVersion)" />

tools/PackageCompatibility/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ dotnet run \
184184
-p:AzureVersion=1.0.0 \
185185
-p:LoggingVersion=1.0.1 \
186186
-p:SqlClientVersion=7.1.0-preview1 \
187-
-p:SqlServerVersio=1.0.0 \
187+
-p:SqlServerVersion=1.0.0 \
188188
-- -c "<connection string>"
189189
```
190190

tools/PackageCompatibility/test/BuildVersionOverrideTests.cs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary<string, string>
138138
var psi = new ProcessStartInfo
139139
{
140140
FileName = "dotnet",
141-
Arguments = string.Join(" ", buildArgs),
142141
UseShellExecute = false,
143142
RedirectStandardOutput = true,
144143
RedirectStandardError = true,
145144
CreateNoWindow = true
146145
};
147146

147+
#if NET
148+
foreach (string buildArg in buildArgs)
149+
{
150+
psi.ArgumentList.Add(buildArg);
151+
}
152+
#else
153+
psi.Arguments = string.Join(" ", buildArgs.Select(QuoteArgument));
154+
#endif
155+
148156
using (var process = Process.Start(psi))
149157
{
150158
if (process == null)
@@ -162,12 +170,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary<string, string>
162170
process.Kill();
163171
throw new InvalidOperationException("Build timed out after 60 seconds");
164172
}
165-
#else
166-
process.WaitForExit(60000);
167-
#endif
168173

169174
stdout = process.StandardOutput.ReadToEnd();
170175
stderr = process.StandardError.ReadToEnd();
176+
#else
177+
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
178+
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
179+
bool completed = process.WaitForExit(60000);
180+
stdout = stdoutTask.GetAwaiter().GetResult();
181+
stderr = stderrTask.GetAwaiter().GetResult();
182+
if (!completed)
183+
{
184+
process.Kill();
185+
throw new InvalidOperationException("Build timed out after 60 seconds");
186+
}
187+
#endif
171188

172189
// Fail fast with full command/stdout/stderr context for easy diagnosis.
173190
if (process.ExitCode != 0)
@@ -219,7 +236,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire
219236

220237
foreach (string directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
221238
{
222-
string relativePath = Path.GetRelativePath(sourceDirectory, directory);
239+
string relativePath = GetRelativePath(sourceDirectory, directory);
223240
if (ShouldSkip(relativePath))
224241
{
225242
continue;
@@ -230,7 +247,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire
230247

231248
foreach (string file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
232249
{
233-
string relativePath = Path.GetRelativePath(sourceDirectory, file);
250+
string relativePath = GetRelativePath(sourceDirectory, file);
234251
if (ShouldSkip(relativePath))
235252
{
236253
continue;
@@ -251,7 +268,7 @@ private static bool ShouldSkip(string relativePath)
251268
{
252269
// Ignore generated artifacts and the test project itself to avoid recursive/self-copy issues.
253270
string normalizedPath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
254-
string[] segments = normalizedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
271+
string[] segments = normalizedPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
255272

256273
foreach (string segment in segments)
257274
{
@@ -308,4 +325,42 @@ private static string GetPackageCompatibilityDirectory()
308325
/// <param name="OutputDirectory">Build output directory for binaries.</param>
309326
/// <param name="GeneratedVersionsFile">Path to generated <c>PackageVersions.g.cs</c>.</param>
310327
private sealed record BuildArtifacts(string ProjectDirectory, string OutputDirectory, string GeneratedVersionsFile);
328+
329+
#if !NET
330+
/// <summary>
331+
/// Quotes a single command-line argument for use with <see cref="System.Diagnostics.ProcessStartInfo.Arguments"/>
332+
/// on .NET Framework, where <c>ArgumentList</c> is unavailable.
333+
/// Wraps the value in double-quotes and escapes any embedded double-quotes.
334+
/// </summary>
335+
/// <param name="arg">The argument to quote.</param>
336+
/// <returns>The argument, quoted if it contains whitespace or double-quote characters.</returns>
337+
private static string QuoteArgument(string arg)
338+
{
339+
if (arg.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
340+
{
341+
return arg;
342+
}
343+
344+
return "\"" + arg.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
345+
}
346+
#endif
347+
348+
/// <summary>
349+
/// Returns the relative path from <paramref name="relativeTo"/> to <paramref name="path"/>.
350+
/// Polyfills <see cref="Path.GetRelativePath"/> which is unavailable on .NET Framework.
351+
/// </summary>
352+
private static string GetRelativePath(string relativeTo, string path)
353+
{
354+
#if NET
355+
return Path.GetRelativePath(relativeTo, path);
356+
#else
357+
// Ensure the base URI ends with a separator so MakeRelativeUri treats it as a directory.
358+
string baseStr = relativeTo.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
359+
+ Path.DirectorySeparatorChar;
360+
Uri baseUri = new Uri(baseStr);
361+
Uri targetUri = new Uri(path);
362+
return Uri.UnescapeDataString(baseUri.MakeRelativeUri(targetUri).ToString())
363+
.Replace('/', Path.DirectorySeparatorChar);
364+
#endif
365+
}
311366
}

tools/PackageCompatibility/test/EntryPointTests.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public void EntryPointWithoutConnectionStringReturnsNonZero()
2828
using StringWriter output = new();
2929

3030
int exitCode;
31+
string commandOutput;
3132

3233
try
3334
{
@@ -36,6 +37,9 @@ public void EntryPointWithoutConnectionStringReturnsNonZero()
3637

3738
// Act: invoke with no arguments; --connection-string is required.
3839
exitCode = EntryPoint.Main(Array.Empty<string>());
40+
41+
// Wait for command help/validation text to flush before asserting.
42+
commandOutput = WaitForCapturedOutput(output, "--connection-string");
3943
}
4044
finally
4145
{
@@ -44,9 +48,6 @@ public void EntryPointWithoutConnectionStringReturnsNonZero()
4448
Console.SetError(originalError);
4549
}
4650

47-
// Wait for command help/validation text to flush before asserting.
48-
string commandOutput = WaitForCapturedOutput(output, "--connection-string");
49-
5051
// Assert: parser should reject the command and mention the missing option.
5152
Assert.NotEqual(0, exitCode);
5253
Assert.Contains("--connection-string", commandOutput, StringComparison.Ordinal);
@@ -75,7 +76,8 @@ public void HelpOutputContainsDefaultVersions()
7576
int exitCode = EntryPoint.Main(new[] { "--help" });
7677
Assert.Equal(0, exitCode);
7778

78-
helpOutput = output.ToString();
79+
// Ensure final help footer is present so all formatted content has been emitted.
80+
helpOutput = WaitForCapturedOutput(output, "--version");
7981
}
8082
finally
8183
{
@@ -84,9 +86,6 @@ public void HelpOutputContainsDefaultVersions()
8486
Console.SetError(originalError);
8587
}
8688

87-
// Ensure final help footer is present so all formatted content has been emitted.
88-
helpOutput = WaitForCapturedOutput(output, "--version");
89-
9089
// Assert: sample property overrides remain documented in help text.
9190
Assert.Contains("-p:AbstractionsVersion=1.0.1", helpOutput, StringComparison.Ordinal);
9291
Assert.Contains("-p:AkvProviderVersion=7.0.1-preview2", helpOutput, StringComparison.Ordinal);
@@ -96,12 +95,12 @@ public void HelpOutputContainsDefaultVersions()
9695
Assert.Contains("-p:SqlServerVersion=1.0.0", helpOutput, StringComparison.Ordinal);
9796

9897
// Assert: currently resolved package defaults are visible to aid troubleshooting.
99-
Assert.Contains("Abstractions: 1.0.0", helpOutput, StringComparison.Ordinal);
100-
Assert.Contains("AKV Provider: 7.0.0", helpOutput, StringComparison.Ordinal);
98+
Assert.Contains($"Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions}", helpOutput, StringComparison.Ordinal);
99+
Assert.Contains($"AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider}", helpOutput, StringComparison.Ordinal);
101100
Assert.Contains("Azure: N/A", helpOutput, StringComparison.Ordinal);
102-
Assert.Contains("Logging: 1.0.0", helpOutput, StringComparison.Ordinal);
103-
Assert.Contains("SqlClient: 7.0.1", helpOutput, StringComparison.Ordinal);
104-
Assert.Contains("SqlServer: 1.0.0", helpOutput, StringComparison.Ordinal);
101+
Assert.Contains($"Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging}", helpOutput, StringComparison.Ordinal);
102+
Assert.Contains($"SqlClient: {PackageVersions.MicrosoftDataSqlClient}", helpOutput, StringComparison.Ordinal);
103+
Assert.Contains($"SqlServer: {PackageVersions.MicrosoftSqlServerServer}", helpOutput, StringComparison.Ordinal);
105104
}
106105

107106
/// <summary>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
// Polyfill required for 'record' types and 'init' setters on .NET Framework.
6+
// The compiler emits a reference to this type when using these C# features; .NET 5+
7+
// includes it in the BCL, but .NET Framework does not.
8+
#if NETFRAMEWORK
9+
namespace System.Runtime.CompilerServices
10+
{
11+
internal static class IsExternalInit { }
12+
}
13+
#endif

0 commit comments

Comments
 (0)