Skip to content

Commit 2bb282f

Browse files
committed
Add automatic test generation after code generation
Introduces a new workflow that generates tests for newly created Azure CLI modules/extensions using LLM sampling. Updates README to reflect new features, adds the generate_tests function to helpers.py, and integrates test generation into the main code generation flow in main.py.
1 parent 3d6d39c commit 2bb282f

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

tools/aaz-flow/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ Please note that AAZ Flow is currently in early development. The functionality a
88

99
## Implemetation
1010
1. Performs elicitation for user input to perform workflow
11-
2. Generates elicitation questions using llm prompts
12-
3. Executes AAZ Flow commands directly
11+
2. Generates content using llm sampling
12+
3. Executes AAZ Flow commands directly
13+
4. Generates tests using llm sampling
14+
5. Uses tool transformation to make the internal tooling more friendly for llms

tools/aaz-flow/helpers.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from pathlib import Path
23
import sys
34
from typing import Literal
45
from fastmcp import Context
@@ -7,6 +8,13 @@
78
import subprocess as sp
89
from models import AAZRequest
910

11+
paths = {
12+
"aaz": os.getenv("AAZ_PATH", "/workspaces-src/aaz"),
13+
"cli": "/workspaces/azure-cli",
14+
"cli_extension": os.getenv("CLI_EXTENSION_PATH", "/workspaces-src/azure-cli-extensions"),
15+
"swagger_path": os.getenv("SWAGGER_PATH", "/workspaces-src/azure-rest-api-specs")
16+
}
17+
1018
async def fetch_available_services():
1119
"""Fetch available services from azure-rest-api-specs repository."""
1220
url = "https://api.github.com/repos/a0x1ab/azure-rest-api-specs/contents/specification"
@@ -22,12 +30,6 @@ async def fetch_available_services():
2230

2331
async def validate_paths(ctx: Context) -> dict:
2432
"""Validate and get correct paths for required directories."""
25-
paths = {
26-
"aaz": os.getenv("AAZ_PATH", "/workspaces-src/aaz"),
27-
"cli": "/workspaces/azure-cli",
28-
"cli_extension": os.getenv("CLI_EXTENSION_PATH", "/workspaces-src/azure-cli-extensions"),
29-
"swagger_path": os.getenv("SWAGGER_PATH", "/workspaces-src/azure-rest-api-specs")
30-
}
3133

3234
await ctx.info("az_cli : Validating local paths...")
3335
await ctx.report_progress(progress=5, total=100)
@@ -182,7 +184,6 @@ async def browse_specs(ctx: Context, base_path: str):
182184
return result
183185

184186
async def run_command(ctx: Context, command: str, step_name: str, progress_start: int, progress_end: int):
185-
"""Run a shell command and report progress."""
186187
await ctx.info(f"az_cli : Starting: {step_name}")
187188
process = await asyncio.create_subprocess_shell(
188189
command,
@@ -197,20 +198,23 @@ async def run_command(ctx: Context, command: str, step_name: str, progress_start
197198
while True:
198199
line = await process.stdout.readline()
199200
if not line:
200-
break
201+
if process.returncode is not None:
202+
break
203+
await asyncio.sleep(0.1)
204+
continue
201205
lines_count += 1
202206
await ctx.info(f"az_cli : {line.decode().rstrip()}")
203207
progress = progress_start + min(progress_range, int((lines_count / total_lines_estimate) * progress_range))
204208
await ctx.report_progress(progress, 100)
205209

206210
await process.wait()
211+
207212
if process.returncode != 0:
208213
raise RuntimeError(f"{step_name} failed: {command}")
209214

210215
await ctx.report_progress(progress_end, 100)
211216
await ctx.info(f"az_cli : Completed: {step_name}")
212217

213-
214218
async def execute_commands(ctx: Context, paths: dict, request: AAZRequest):
215219
cmd1 = (
216220
f"aaz-dev command-model generate-from-swagger "
@@ -241,3 +245,41 @@ async def execute_commands(ctx: Context, paths: dict, request: AAZRequest):
241245
return f"Code generation failed: {str(e)}"
242246

243247
return "Azure CLI code generation completed successfully!"
248+
249+
async def generate_tests(ctx: "Context"):
250+
await ctx.info("Starting test generation workflow.")
251+
252+
module_name = getattr(ctx, "generated_module", None)
253+
if not module_name:
254+
response = await ctx.elicit("Enter the module/extension name to generate tests for:")
255+
if not response.action == "accept" or not response.data:
256+
return "Test generation cancelled."
257+
module_name = response.data
258+
else:
259+
await ctx.info(f"Detected generated module: {module_name}")
260+
261+
aaz_path = Path(f"{paths['cli']}/src/azure-cli/azure/cli/command_modules/{module_name}/aaz")
262+
if not aaz_path.exists():
263+
return f"AAZ path not found for module '{module_name}'"
264+
265+
commands = []
266+
for file in aaz_path.rglob("*.py"):
267+
with open(file, "r", encoding="utf-8") as f:
268+
for line in f:
269+
if line.strip().startswith("def "):
270+
commands.append(line.strip().replace("def ", "").split("(")[0])
271+
272+
test_dir = Path(f"{paths['cli']}/src/azure-cli/azure/cli/command_modules/{module_name}/tests/latest")
273+
test_dir.mkdir(parents=True, exist_ok=True)
274+
test_file = test_dir / f"test_{module_name}.py"
275+
276+
with open(test_file, "w", encoding="utf-8") as f:
277+
f.write("import unittest\n")
278+
f.write("from azure.cli.testsdk import ScenarioTest\n\n")
279+
f.write(f"class {module_name.capitalize()}ScenarioTest(ScenarioTest):\n\n")
280+
for cmd in commands:
281+
f.write(f" def test_{cmd}(self):\n")
282+
f.write(f" self.cmd('az {module_name} {cmd} --resource-name test-resource')\n\n")
283+
284+
await ctx.info(f"Generated test file: {test_file}")
285+
return f"Test generation completed for module '{module_name}'."

tools/aaz-flow/main.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
2+
from pathlib import Path
23
from fastmcp import FastMCP, Context
34
from models import AAZRequest
4-
from helpers import execute_commands, validate_paths, get_extension_name, get_swagger_config
5+
from helpers import generate_tests, execute_commands, validate_paths, get_extension_name, get_swagger_config
56

67
mcp = FastMCP("AAZ Flow")
78

@@ -53,7 +54,18 @@ async def generate_code(ctx: Context):
5354

5455
await execute_commands(ctx, paths, request)
5556
await ctx.report_progress(100, 100)
56-
return f"Code generation completed for extension/module '{extension_name}'."
57+
await ctx.info(f"Code generation completed for extension/module '{extension_name}'.")
58+
59+
ctx.generated_module = extension_name
60+
61+
await ctx.info("Automatically generating tests for the newly generated module...")
62+
try:
63+
test_result = await generate_tests(ctx)
64+
await ctx.info(f"Automatic test generation result: {test_result}")
65+
except Exception as e:
66+
await ctx.info(f"Automatic test generation failed: {str(e)}")
67+
68+
return f"Code generation and test generation completed for extension/module '{extension_name}'."
5769

5870
if __name__ == "__main__":
5971
mcp.run(transport="stdio")

0 commit comments

Comments
 (0)