This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
This guide covers conventions, patterns, and requirements for modifying or creating developer scenarios in the HOBL codebase. For general HOBL usage (lab setup, DUT configuration, running tests), see README.md.
- Scenario Structure
- Platform Parity
- PowerShell Coding Standards (Windows)
- Shell Script Standards (macOS)
- Timing and Metrics
- Prep Version Bumping
- Error Handling
- Path Handling
- Architecture Awareness
- ETW Phase Markers (Windows)
- Python Path Resolution
- Visual Studio Integration
- Checklist Before Submitting a PR
Each developer scenario has both a Windows and macOS variant. The folder layout follows this pattern:
scenarios/
windows/<scenario>/
<scenario>.py # Python class (orchestrator)
__init__.py
<scenario>_resources/
<scenario>_prep.ps1 # One-time setup (install deps, clone repos)
<scenario>_run.ps1 # Timed workload execution
<scenario>_teardown.ps1 # Cleanup (optional)
MacOS/mac_<scenario>/
mac_<scenario>.py
__init__.py
mac_<scenario>_resources/
mac_<scenario>_prep.sh
mac_<scenario>_run.sh
The Python file defines a class that inherits from scenarios.app_scenario.Scenario and controls the scenario lifecycle:
| Method | Purpose |
|---|---|
setUp() |
Checks prep_version, uploads resources to DUT, runs prep script if needed |
runTest() |
Executes the run script in a loop (default iteration count set via Params.setDefault) |
tearDown() |
Calls base tearDown, then runs teardown script if one exists |
kill() |
Emergency cleanup of processes |
Key class attributes:
module = __module__.split('.')[-1] # Auto-derived scenario name
prep_version = "2" # Controls prep re-execution (see below)
resources = module + "_resources" # Resource folder name- Prep script — Installs dependencies (SDKs, tools, repos). Runs once per
prep_version. Should be idempotent where possible. - Run script — The timed workload. Called multiple times (loop iterations). Must produce metrics CSV.
- Teardown script — Optional cleanup after all iterations complete.
Windows and macOS scripts for the same scenario must be functionally equivalent:
- Time the same phases (e.g., if Windows times
build, macOS must also timebuild) - Use the same CSV key names (e.g., both use
build_time, notbuild_realon one andbuild_timeon the other) - Compute
scenario_runtimethe same way — always wall-clock time - macOS scripts may capture extra detail (e.g.,
user,sys,cputimevia/usr/bin/time -p), butscenario_runtimemust match semantically
Before modifying one platform, check the other platform's script to keep them aligned.
Every Windows prep/run script should follow this structure:
param(
[string]$logFile = ""
)
$scriptDrive = Split-Path -Qualifier $PSScriptRoot
if (-not $logFile) { $logFile = "$scriptDrive\hobl_data\<scenario>_run.log" }
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function log {
[CmdletBinding()] Param([Parameter(ValueFromPipeline)] $msg)
process {
if ($msg -Match " ERROR - ") {
Write-Host $msg -ForegroundColor Red
} else {
Write-Host $msg
}
Add-Content -Path $logFile -encoding utf8 "$msg"
}
}
function check {
param($code)
if ($code -ne 0) {
" ERROR - Last command failed." | log
Exit $code
}
}
function checkWinget {
param($code)
if ($code -eq 0) {
"Winget command succeeded" | log
} elseif ($code -eq -1978335189) {
"Package already installed (this is OK)" | log
} elseif ($code -eq -1978335215) {
"No applicable upgrade found (this is OK)" | log
} else {
" ERROR - Winget command failed with exit code: $code" | log
Exit $code
}
}Scripts that use pyenv install (which calls Expand-Archive) must set execution policy early:
$executionPolicy = Get-ExecutionPolicy -Scope Process
if ($executionPolicy -eq "Restricted" -or $executionPolicy -eq "Undefined") {
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force -ErrorAction Stop
}macOS scripts use /bin/sh and follow this pattern:
#!/bin/sh
LOG_DIR="/Users/Shared/hobl_data"
METRICS_FILE="$LOG_DIR/mac_<scenario>_results.csv"
check_status() {
if [ $? -ne 0 ]; then
echo " ERROR - $1 failed"
exit 1
fi
echo "✓ $1 successful"
}
check_command() {
if command -v "$1" >/dev/null 2>&1; then
echo "✓ $1 is available"
return 0
else
echo " ERROR - $1 is not available"
return 1
fi
}Every run script writes a <scenario>_results.csv to the hobl_data directory in key,value format:
scenario_runtime,45.23
build_time,45.23scenario_runtimeis required — it represents total wall-clock time of the timed workload- Additional phase timings are encouraged (e.g.,
build_time,test_time)
$buildDuration = Measure-Command {
dotnet build Aspire.slnx --no-restore > "$logDir\build.log" 2>&1
}
check($lastexitcode)
$buildTime = [math]::Round($buildDuration.TotalSeconds, 2)build_start=$(date +%s)
# ... build commands ...
build_end=$(date +%s)
build_time=$((build_end - build_start))Print a human-readable summary at the end of each run:
========================================
.NET Aspire Metrics Summary
========================================
Build Time: 45.23s
scenario_runtime (total): 45.23s
========================================
Each scenario's Python file contains a prep_version string:
prep_version = "2"This controls whether the prep script re-runs on the DUT. The HOBL framework checks this value and only re-runs prep when the version changes.
- Whenever prep or run scripts are modified in a PR, increment
prep_versionby 1 in the corresponding scenario's Python file - Increment for both platforms if both were changed, or just the affected one
- This only needs to happen once per PR, not per commit
- Forgetting to bump means DUTs won't pick up your script changes until someone manually clears the prep status
All error messages must use this exact format:
ERROR - <description>
Note the spacing: leading space, ERROR, space, dash, space. This is required because the log function pattern-matches on " ERROR - " to colorize output.
✅ Correct:
" ERROR - Last command failed." | log
" ERROR - dotnet not found on PATH" | log
Write-Host " ERROR - Unsupported architecture" -ForegroundColor Red❌ Wrong:
"ERROR: something failed" | log
"Error - bad path" | logUse the standard helpers consistently:
| Function | Use when |
|---|---|
check($lastexitcode) |
After any command where $LASTEXITCODE indicates success/failure |
checkWinget($lastexitcode) |
After winget install (handles "already installed" codes) |
checkCmd($result) |
After commands returning "True"/"False" strings |
checkGitClone $code $path |
After git clone (handles "already exists" case) |
checkSetLocation $path |
Before Set-Location to verify path exists |
check_status "description" |
macOS equivalent of check |
Never hardcode C:\ or any drive letter. Scripts may run from any drive.
# Derive drive from script location
$scriptDrive = Split-Path -Qualifier $PSScriptRoot
# Use for all HOBL paths
$logFile = "$scriptDrive\hobl_data\scenario.log"
$repoDir = "$scriptDrive\aspire"System paths are the exception — $env:ProgramFiles, $env:USERPROFILE, $env:TEMP, etc., resolve correctly on their own.
macOS scripts use /Users/Shared/hobl_data and /Users/Shared/hobl_bin as standard locations.
Windows scenarios must detect and handle both x64 and ARM64:
$osInfo = Get-CimInstance Win32_OperatingSystem
$arch = $osInfo.OSArchitecture
$processorArch = $env:PROCESSOR_ARCHITECTURE
if ($arch -eq "64-bit" -and $processorArch -eq "AMD64") {
$isARM64 = $false
$logSuffix = "x64"
} elseif ($arch -match "ARM" -or $processorArch -match "ARM") {
$isARM64 = $true
$logSuffix = "ARM64"
} else {
" ERROR - Unsupported architecture: $arch" | log
Exit 1
}Use $isARM64 to conditional-branch where binaries, workarounds, or configurations differ between architectures.
HOBL includes a manifest for the scenario phase marker provider at:
scenarios/windows/_dev_tools/HOBL-Scenario-Phases.man
This manifest maps provider GUID 9f0f6e2e-8d06-4d2f-b8f5-6f1f2d5a1c01 event ID 1 to two Unicode payload fields:
MarkerScenario
Run these commands in an elevated PowerShell terminal:
$manifestPath = "<repo_root>\scenarios\windows\_dev_tools\HOBL-Scenario-Phases.man"
wevtutil um $manifestPath
wevtutil im $manifestPath
wevtutil gp "HOBL-Scenario-Phases" /ge:true /gm:trueThen reopen WPA and reload the ETL file. Generic Events should decode payload text for this provider when metadata matches.
Use this standard marker format:
phase.run_prep.start/phase.run_prep.endphase.run_build.start/phase.run_build.endphase.run_test.start/phase.run_test.end
This keeps phase attribution consistent across all developer scenarios and avoids naming collisions with prep/teardown script concepts.
For Windows scenario run scripts, define an ETW EventSource with:
- Name:
HOBL-Scenario-Phases - GUID:
9f0f6e2e-8d06-4d2f-b8f5-6f1f2d5a1c01 - Event ID:
1 - Payload: marker string and scenario name
Emit marker pairs immediately around each timed run phase. Example:
Write-RunPhaseMarker "phase.run_build.start"
# build work
Write-RunPhaseMarker "phase.run_build.end"HOBL uses pyenv-win for Python version management. Do not replace it with winget install of Python.
# WRONG — returns the pyenv shim, not the real .exe
$python = Get-Command python
# CORRECT — returns the actual python.exe path
$pythonExeRaw = pyenv which python 2>$null
$pythonExe = $pythonExeRaw.Trim()This matters when passing paths to tools like CMake (-DPython3_EXECUTABLE) which require the real executable.
Use vswhere.exe to discover Visual Studio — never hardcode installation paths.
$vsInfo = getVSVersion -product $vsProduct
$actualVSPath = $vsInfo.Path
$vsDevCmd = Join-Path $actualVSPath "Common7\Tools\VsDevCmd.bat"- Scripts use drive-relative paths (
$scriptDrive), no hardcodedC:\ - Error messages use the
" ERROR - "format - Standard helper functions (
check,log, etc.) are used consistently -
prep_versionincremented in affected scenario Python files (both platforms if both changed) - Run script produces
<scenario>_results.csvwithscenario_runtime - Timing is consistent between Windows and macOS variants
- CSV key names match across platforms
- Architecture detection included for Windows scenarios
- Paths verified with
Test-Pathbefore use -
pyenv which pythonused instead ofGet-Command python