Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using System.Reflection;

namespace Azure.Sdk.Tools.PerfAutomation.Tests
{
[TestFixture]
public class GoTests
{
[Test]
public void ParseOpsPerSecond_ValidOutput_ReturnsValue()
{
var output = "Completed 721 operations in a weighted-average of 30.00s (24.033 ops/s, 0.042 s/op)";
var value = InvokePrivateStaticDouble("ParseOpsPerSecond", output);

Assert.That(value, Is.EqualTo(24.033).Within(0.0001));
}

[Test]
public void ParseOpsPerSecond_CommaFormattedOutput_ReturnsValue()
{
var output = "Completed 1,234 operations in a weighted-average of 1.00s (12,557.81 ops/s, 0.000 s/op)";
var value = InvokePrivateStaticDouble("ParseOpsPerSecond", output);

Assert.That(value, Is.EqualTo(12557.81).Within(0.0001));
}

[Test]
public void ParseOpsPerSecond_MissingPattern_ReturnsMinusOne()
{
var output = "No throughput line present";
var value = InvokePrivateStaticDouble("ParseOpsPerSecond", output);

Assert.That(value, Is.EqualTo(-1d));
}

[Test]
public void FilterRuntimePackageVersions_KeepsExpectedPackages()
{
var go = new Go();

var input = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"] = "v1.6.4",
["golang.org/x/net"] = "v0.53.0",
["go"] = "go1.23.0",
["github.com/some/other"] = "v1.0.0"
};

var filtered = go.FilterRuntimePackageVersions(input);

Assert.That(filtered, Is.Not.Null);
Assert.That(filtered.ContainsKey("github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"), Is.True);
Assert.That(filtered.ContainsKey("golang.org/x/net"), Is.True);
Assert.That(filtered.ContainsKey("go"), Is.True);
Assert.That(filtered.ContainsKey("github.com/some/other"), Is.False);
}

[Test]
public void ResolveSourcePath_AzurePackage_ReturnsExpectedPath()
{
var go = new Go();
go.WorkingDirectory = Path.Combine(Path.DirectorySeparatorChar.ToString(), "tmp", "azure-sdk-for-go");

var result = InvokePrivateInstanceString(go, "ResolveSourcePath", "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob");
var expected = Path.Combine(go.WorkingDirectory, "sdk", "storage", "azblob");

Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void ResolveSourcePath_NonAzurePackage_Throws()
{
var go = new Go();
go.WorkingDirectory = Path.Combine(Path.DirectorySeparatorChar.ToString(), "tmp", "azure-sdk-for-go");

var ex = Assert.Throws<TargetInvocationException>(() =>
InvokePrivateInstanceString(go, "ResolveSourcePath", "github.com/not-azure/pkg"));

Assert.That(ex, Is.Not.Null);
Assert.That(ex.InnerException, Is.TypeOf<InvalidOperationException>());
Assert.That(ex.InnerException?.Message, Does.Contain("Cannot resolve source path"));
}

private static double InvokePrivateStaticDouble(string methodName, string arg)
{
var method = typeof(Go).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(method, Is.Not.Null, $"Method {methodName} not found");

var result = method!.Invoke(null, new object[] { arg });
Assert.That(result, Is.TypeOf<double>());

return (double)result!;
}

private static string InvokePrivateInstanceString(object instance, string methodName, string arg)
{
var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
Assert.That(method, Is.Not.Null, $"Method {methodName} not found");

var result = method!.Invoke(instance, new object[] { arg });
Assert.That(result, Is.TypeOf<string>());

return (string)result!;
}
}
}
278 changes: 278 additions & 0 deletions tools/perf-automation/Azure.Sdk.Tools.PerfAutomation/Go.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Azure.Sdk.Tools.PerfAutomation.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Azure.Sdk.Tools.PerfAutomation
{
public class Go : LanguageBase
{
protected override Language Language => Language.Go;

private static readonly Regex _opsRegex =
new Regex(@"\(([-\d\.,]+)\s+ops/s", RegexOptions.IgnoreCase | RegexOptions.RightToLeft | RegexOptions.Compiled);

public override async Task<(string output, string error, object context)> SetupAsync(
string project,
string languageVersion,
string primaryPackage,
IDictionary<string, string> packageVersions,
bool debug)
{
var projectDirectory = Path.Combine(WorkingDirectory, project);

var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();

var goModPath = Path.Combine(projectDirectory, "go.mod");
var goSumPath = Path.Combine(projectDirectory, "go.sum");

BackupFile(goModPath);
BackupFile(goSumPath);

// Drop any existing replace directives for all requested packages to avoid stale state.
foreach (var pkg in packageVersions.Keys)
{
await DropReplaceAsync(projectDirectory, pkg, outputBuilder, errorBuilder);
}

foreach (var kvp in packageVersions)
{
var packageName = kvp.Key;
var packageVersion = kvp.Value;

if (string.Equals(packageVersion, Program.PackageVersionSource, StringComparison.OrdinalIgnoreCase))
{
var localPath = ResolveSourcePath(packageName);
await AddReplaceAsync(projectDirectory, packageName, localPath, outputBuilder, errorBuilder);
}
else
{
// Ensure published version resolution for this package.
await Util.RunAsync(
"go",
$"get {packageName}@{packageVersion}",
projectDirectory,
outputBuilder: outputBuilder,
errorBuilder: errorBuilder);
}
}

await Util.RunAsync(
"go",
"mod tidy",
projectDirectory,
outputBuilder: outputBuilder,
errorBuilder: errorBuilder);

return (outputBuilder.ToString(), errorBuilder.ToString(), null);
}

public override async Task<IterationResult> RunAsync(
string project,
string languageVersion,
string primaryPackage,
IDictionary<string, string> packageVersions,
string testName,
string arguments,
bool profile,
string profilerOptions,
object context)
{
var projectDirectory = Path.Combine(WorkingDirectory, project);

var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();

// Go perf runners in this scenario are CLI test apps.
var processResult = await Util.RunAsync(
"go",
$"run . {testName} {arguments}",
projectDirectory,
outputBuilder: outputBuilder,
errorBuilder: errorBuilder,
throwOnError: false);

// Capture combined output for parsing and return.
var combinedOutput = (processResult.StandardOutput ?? string.Empty) + Environment.NewLine + (processResult.StandardError ?? string.Empty);
var opsPerSecond = ParseOpsPerSecond(combinedOutput);
Comment on lines +101 to +103

var runtimePackageVersions = await GetRuntimePackageVersionsAsync(projectDirectory);

return new IterationResult
{
PackageVersions = runtimePackageVersions,
OperationsPerSecond = opsPerSecond,
StandardOutput = processResult.StandardOutput ?? outputBuilder.ToString(),
StandardError = processResult.StandardError ?? errorBuilder.ToString()
};
}

public override async Task CleanupAsync(string project)
{
var projectDirectory = Path.Combine(WorkingDirectory, project);

RestoreBackup(Path.Combine(projectDirectory, "go.mod"));
RestoreBackup(Path.Combine(projectDirectory, "go.sum"));

// Best-effort cleanup to ensure module graph is consistent after restore.
try
{
await Util.RunAsync(
"go",
"mod tidy",
projectDirectory,
throwOnError: false);
}
catch
{
// Ignore cleanup failures.
}
}

public override IDictionary<string, string> FilterRuntimePackageVersions(IDictionary<string, string> runtimePackageVersions)
{
// Keep Azure packages and a minimal runtime signal.
return runtimePackageVersions?
.Where(kvp =>
kvp.Key.StartsWith("github.com/Azure/", StringComparison.OrdinalIgnoreCase) ||
kvp.Key.Equals("go", StringComparison.OrdinalIgnoreCase) ||
kvp.Key.StartsWith("golang.org/x/", StringComparison.OrdinalIgnoreCase))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}

private static double ParseOpsPerSecond(string output)
{
if (string.IsNullOrWhiteSpace(output))
{
return -1d;
}

var match = _opsRegex.Match(output);
if (!match.Success)
{
return -1d;
}

var raw = match.Groups[1].Value.Replace(",", string.Empty).Trim();

if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
{
return value;
}

return -1d;
}

private async Task<IDictionary<string, string>> GetRuntimePackageVersionsAsync(string projectDirectory)
{
var runtimePackageVersions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

try
{
var result = await Util.RunAsync(
"go",
"list -m all",
projectDirectory,
throwOnError: false);

var lines = (result.StandardOutput ?? string.Empty)
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

foreach (var line in lines)
{
// Formats:
// module version
// module version => replacement
// module => replacement (workspace/local edge cases)
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
continue;
}

var module = parts[0];
var version = parts[1];

if (parts.Length > 2 && parts[2] == "=>")
{
version = string.Join(" ", parts.Skip(1));
}
Comment on lines +199 to +205

runtimePackageVersions[module] = version;
}
}
catch
{
// Best effort: return what we have.
}

return runtimePackageVersions;
}

private string ResolveSourcePath(string packageName)
{
const string repoPrefix = "github.com/Azure/azure-sdk-for-go/";
if (!packageName.StartsWith(repoPrefix, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Cannot resolve source path for package '{packageName}'. Expected prefix '{repoPrefix}'.");
}

var relative = packageName.Substring(repoPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(WorkingDirectory, relative);
}

private static async Task DropReplaceAsync(
string projectDirectory,
string packageName,
StringBuilder outputBuilder,
StringBuilder errorBuilder)
{
await Util.RunAsync(
"go",
$"mod edit -dropreplace={packageName}",
projectDirectory,
outputBuilder: outputBuilder,
errorBuilder: errorBuilder,
throwOnError: false);
}

private static async Task AddReplaceAsync(
string projectDirectory,
string packageName,
string localPath,
StringBuilder outputBuilder,
StringBuilder errorBuilder)
{
await Util.RunAsync(
"go",
$"mod edit -replace={packageName}={localPath}",
projectDirectory,
outputBuilder: outputBuilder,
errorBuilder: errorBuilder);
}

private static void BackupFile(string path)
{
if (File.Exists(path))
{
File.Copy(path, path + ".bak", overwrite: true);
}
}

private static void RestoreBackup(string path)
{
var backup = path + ".bak";
if (File.Exists(backup))
{
File.Move(backup, path, overwrite: true);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum Language
Net,
Python,
Cpp,
Rust
Rust,
Go
}
}
Loading