Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- @azure Rule - Use Azure Best Practices: When generating code for Azure, running terminal commands for Azure, or performing operations related to Azure, invoke your `azure_development-get_best_practices` tool if available.
- @azure Rule - Use Azure Best Practices: When generating code for Azure, running terminal commands for Azure, or performing operations related to Azure, invoke your `azure_development-get_best_practices` tool if available.
11 changes: 11 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x

- name: Build azsdk cli
run: |
set -ex
dotnet publish -f net8.0 -c Release -r linux-x64 -p:PublishSingleFile=true --self-contained --output ./artifacts/linux-x64 ./tools/azsdk-cli/Azure.Sdk.Tools.Cli/Azure.Sdk.Tools.Cli.csproj
cp ./artifacts/linux-x64/azsdk /home/runner/azsdk
/home/runner/azsdk --help
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
foobar
# Azure SDK Tools

This repository contains useful tools that the Azure SDK team utilizes across their infrastructure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class EnforceToolsExceptionHandlingAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
Id,
"McpServerTool methods must wrap body in try/catch, see the README within the tools directory for examples",
"Method '{0}' must have its entire body inside try{{}}catch(Exception)",
"Method '{0}' must have its entire body inside 'try {} catch(Exception) {}'",
"Reliability",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
Expand Down Expand Up @@ -57,21 +57,27 @@ private static void AnalyzeMethod(SyntaxNodeAnalysisContext ctx)
return;
}

// check that there is one statement surrounding the body
// check that try statements surround the entire body
var stmts = body.Statements;
if (stmts.Count != 1 || !(stmts[0] is TryStatementSyntax tryStmt))
foreach (var stmt in stmts)
{
ctx.ReportDiagnostic(Diagnostic.Create(Rule, md.Identifier.GetLocation(), md.Identifier.Text));
return;
if (stmt is LocalDeclarationStatementSyntax)
{
continue;
}
if (!(stmt is TryStatementSyntax tryStmt))
{
ctx.ReportDiagnostic(Diagnostic.Create(Rule, md.Identifier.GetLocation(), md.Identifier.Text));
continue;
}
// verify there’s a catch(Exception)
bool hasExCatch = tryStmt.Catches.Any(c => c.Declaration?.Type.ToString() == "Exception");
if (!hasExCatch)
{
ctx.ReportDiagnostic(Diagnostic.Create(Rule, md.Identifier.GetLocation(), md.Identifier.Text));
}
}

// verify there’s a catch(Exception)
bool hasExCatch = tryStmt.Catches
.Any(c => c.Declaration?.Type.ToString() == "Exception");
if (!hasExCatch)
{
ctx.ReportDiagnostic(Diagnostic.Create(Rule, md.Identifier.GetLocation(), md.Identifier.Text));
}
}
}
}
12 changes: 7 additions & 5 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ namespace Azure.Sdk.Tools.Cli.Commands
{
public static class SharedOptions
{
public static readonly List<Type> ToolsList = new List<Type>(){
typeof(AnalyzePipelinesTool),
typeof(PipelineDetailsTool),
public static readonly List<Type> ToolsList = [
typeof(CleanupTool),
typeof(LogAnalysisTool),
typeof(HostServerTool),
typeof(PipelineAnalysisTool),
typeof(PipelineDetailsTool),
typeof(PipelineTestsTool),
typeof(ReleasePlanTool),
typeof(ReleaseReadinessTool),
typeof(SpecCommonTools),
typeof(SpecPullRequestTools),
typeof(SpecWorkflowTool),
typeof(SpecValidationTools),
typeof(ReleaseReadinessTool),
typeof(TestAnalysisTool),
#if DEBUG
// only add this tool in debug mode
typeof(HelloWorldTool),
#endif
};
];

public static Option<string> ToolOption = new("--tools")
{
Expand Down
29 changes: 23 additions & 6 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/LogAnalysisHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Security.Policy;
using Azure.Sdk.Tools.Cli.Models;

namespace Azure.Sdk.Tools.Cli.Helpers;
Expand Down Expand Up @@ -39,6 +40,9 @@ public class LogAnalysisHelper(ILogger<LogAnalysisHelper> logger) : ILogAnalysis
{
private readonly ILogger<LogAnalysisHelper> logger = logger;

public const int DEFAULT_BEFORE_LINES = 20;
public const int DEFAULT_AFTER_LINES = 20;

// Built-in error keywords for robust error detection
private static readonly HashSet<Keyword> defaultErrorKeywords =
[
Expand Down Expand Up @@ -75,6 +79,12 @@ public class LogAnalysisHelper(ILogger<LogAnalysisHelper> logger) : ILogAnalysis
];

public async Task<List<LogEntry>> AnalyzeLogContent(string filePath, List<string>? keywordOverrides, int? beforeLines, int? afterLines)
{
using var stream = new StreamReader(filePath);
return await AnalyzeLogContent(stream, keywordOverrides, beforeLines, afterLines, filePath: filePath);
}

public async Task<List<LogEntry>> AnalyzeLogContent(StreamReader reader, List<string>? keywordOverrides, int? beforeLines, int? afterLines, string url = "", string filePath = "")
{
var keywords = defaultErrorKeywords;
if (keywordOverrides?.Count > 0)
Expand All @@ -86,14 +96,13 @@ public async Task<List<LogEntry>> AnalyzeLogContent(string filePath, List<string
}
}

beforeLines ??= 3;
afterLines ??= 20;
beforeLines ??= DEFAULT_BEFORE_LINES;
afterLines ??= DEFAULT_AFTER_LINES;
var before = new Queue<string>((int)beforeLines);
var after = new Queue<string>((int)afterLines);
var maxAfterLines = afterLines ?? 100;

var errors = new List<LogEntry>();
using var reader = new StreamReader(filePath);

var lineNumber = 0;
string? line;
Expand Down Expand Up @@ -127,12 +136,20 @@ public async Task<List<LogEntry>> AnalyzeLogContent(string filePath, List<string
var fullContext = before.Concat(after).ToList();
before.Clear();
after.Clear();
errors.Add(new LogEntry
var entry = new LogEntry
{
File = filePath,
Line = lineNumber,
Message = string.Join(Environment.NewLine, fullContext)
});
};
if (!string.IsNullOrEmpty(url))
{
entry.Url = url;
}
if (!string.IsNullOrEmpty(filePath))
{
entry.File = filePath;
}
errors.Add(entry);
}
}

Expand Down
93 changes: 93 additions & 0 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Xml;
using Azure.Sdk.Tools.Cli.Models;

namespace Azure.Sdk.Tools.Cli.Services;

public interface ITestHelper
{
Task<List<string>> GetFailedTestCases(string trxFilePath, string filterTitle = "");
Task<FailedTestRunResponse> GetFailedTestCaseData(string trxFilePath, string testCaseTitle);
Task<List<FailedTestRunResponse>> GetFailedTestRunDataFromTrx(string trxFilePath);
}

public class TestHelper(IOutputService output, ILogger<TestHelper> logger) : ITestHelper
{
private readonly IOutputService output = output;
private readonly ILogger<TestHelper> logger = logger;

public async Task<List<string>> GetFailedTestCases(string trxFilePath, string filterTitle = "")
{
var failedTestRuns = await GetFailedTestRunDataFromTrx(trxFilePath);
return failedTestRuns
.Where(run => string.IsNullOrEmpty(filterTitle) || run.TestCaseTitle.Contains(filterTitle, StringComparison.OrdinalIgnoreCase))
.Select(run => run.TestCaseTitle)
.ToList();
}

public async Task<FailedTestRunResponse> GetFailedTestCaseData(string trxFilePath, string testCaseTitle)
{
var failedTestRuns = await GetFailedTestRunDataFromTrx(trxFilePath);
return failedTestRuns.FirstOrDefault(run => run.TestCaseTitle.Equals(testCaseTitle, StringComparison.OrdinalIgnoreCase));
}

public async Task<List<FailedTestRunResponse>> GetFailedTestRunDataFromTrx(string trxFilePath)
{
var failedTestRuns = new List<FailedTestRunResponse>();
if (!File.Exists(trxFilePath))
{
logger.LogError("TRX file not found: {trxFilePath}", trxFilePath);
return failedTestRuns;
}

var xmlContent = await File.ReadAllTextAsync(trxFilePath);
var doc = new XmlDocument();
doc.LoadXml(xmlContent);
var unitTestResults = doc.GetElementsByTagName("UnitTestResult");
foreach (XmlNode resultNode in unitTestResults)
{
var outcome = resultNode.Attributes?["outcome"]?.Value ?? "";
if (!string.Equals(outcome, "Failed", StringComparison.OrdinalIgnoreCase))
{
continue;
}

var testId = resultNode.Attributes?["testId"]?.Value ?? "";
var testName = resultNode.Attributes?["testName"]?.Value ?? "";
var errorMessage = "";
var stackTrace = "";

var outputNode = resultNode.ChildNodes.OfType<XmlNode>().FirstOrDefault(n => n.Name == "Output");
if (outputNode != null)
{
var errorInfoNode = outputNode.ChildNodes.OfType<XmlNode>().FirstOrDefault(n => n.Name == "ErrorInfo");
if (errorInfoNode != null)
{
var messageNode = errorInfoNode.ChildNodes.OfType<XmlNode>().FirstOrDefault(n => n.Name == "Message");
if (messageNode != null)
{
errorMessage = messageNode.InnerText ?? "";
}

var stackTraceNode = errorInfoNode.ChildNodes.OfType<XmlNode>().FirstOrDefault(n => n.Name == "StackTrace");
if (stackTraceNode != null)
{
stackTrace = stackTraceNode.InnerText ?? "";
}
}
}

failedTestRuns.Add(new FailedTestRunResponse
{
TestCaseTitle = testName,
ErrorMessage = errorMessage,
StackTrace = stackTrace,
Outcome = outcome,
Uri = $"file://{trxFilePath}"
});
}

return failedTestRuns;
}
}
12 changes: 6 additions & 6 deletions tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/TokenUsageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ public void LogCost()
Console.WriteLine("--------------------------------------------------------------------------------");
}

public static TokenUsageHelper operator +(TokenUsageHelper a, TokenUsageHelper b) => new()
public static TokenUsageHelper operator +(TokenUsageHelper a, TokenUsageHelper? b) => new()
{
Models = a.Models.Union(b.Models).ToList(),
PromptTokens = a.PromptTokens + b.PromptTokens,
CompletionTokens = a.CompletionTokens + b.CompletionTokens,
InputCost = a.InputCost + b.InputCost,
OutputCost = a.OutputCost + b.OutputCost,
Models = a.Models.Union(b?.Models ?? []).ToList(),
PromptTokens = a.PromptTokens + (b?.PromptTokens ?? 0),
CompletionTokens = a.CompletionTokens + (b?.CompletionTokens ?? 0),
InputCost = a.InputCost + (b?.InputCost ?? 0),
OutputCost = a.OutputCost + (b?.OutputCost ?? 0),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,45 @@ namespace Azure.Sdk.Tools.Cli.Models;
public class FailedTestRunResponse : Response
{
[JsonPropertyName("run_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int RunId { get; set; } = 0;

[JsonPropertyName("test_case_title")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string TestCaseTitle { get; set; }

[JsonPropertyName("error_message")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string ErrorMessage { get; set; }

[JsonPropertyName("stack_trace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string StackTrace { get; set; }

[JsonPropertyName("outcome")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Outcome { get; set; }

[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("uri")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Uri { get; set; }

public override string ToString()
{
var output = $"### Run ID: {RunId}" + Environment.NewLine +
$"### Test Case Title: {TestCaseTitle}" + Environment.NewLine +
$"### Outcome: {Outcome}" + Environment.NewLine +
$"### URL: {Url}" +
$"### Stack Trace:{Environment.NewLine}{StackTrace}" + Environment.NewLine +
$"### Error Message:{Environment.NewLine}{ErrorMessage}" + Environment.NewLine;
var output = "";
output += $"## {TestCaseTitle}{Environment.NewLine}";
if (RunId != 0)
{
output += $"Run ID: {RunId}{Environment.NewLine}";
}
output += $"Outcome: {Outcome}{Environment.NewLine}";
if (!string.IsNullOrEmpty(Uri))
{
output += $"URI: {Uri}{Environment.NewLine}";
}
output += $"{Environment.NewLine}### Stack Trace{Environment.NewLine}{StackTrace}{Environment.NewLine}";
output += $"### Error Message{Environment.NewLine}{ErrorMessage}{Environment.NewLine}";

return ToString(output);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public override string ToString()
output += $"### Suggested Fix:" + Environment.NewLine +
$"{SuggestedFix}" + Environment.NewLine + Environment.NewLine +
$"### Errors:" + Environment.NewLine +
$"{string.Join(Environment.NewLine+Environment.NewLine, Errors.Select(e => $"--> {e.File}:{e.Line}{Environment.NewLine}{e.Message}"))}" +
$"{string.Join(Environment.NewLine + Environment.NewLine, Errors.Select(e => $"--> {e.File}:{e.Line}{Environment.NewLine}{e.Message}"))}" +
Environment.NewLine;
}

Expand All @@ -49,7 +49,10 @@ public class LogEntry
{
[JsonPropertyName("file")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string File { get; set; } = string.Empty;
public string File { get; set; }
[JsonPropertyName("url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Url { get; set; }
[JsonPropertyName("line")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Line { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Azure.Sdk.Tools.Cli.Models;

// Basic helper response class for when object types should always be output as JSON
public class ObjectCommandResponse : Response
{
private static readonly JsonSerializerOptions serializerOptions = new()
{
WriteIndented = true
};

[JsonPropertyName("result")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Result { get; set; }

public override string ToString()
{
var output = JsonSerializer.Serialize(Result, serializerOptions);
return ToString(output);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Azure.Sdk.Tools.Cli.Models.Responses
Expand Down
Loading