Skip to content
Merged
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
5 changes: 5 additions & 0 deletions cli/azd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ versioninfo.json
azd.sln

**/target

# Coverage artifacts (generated by mage coverage:* targets)
cover-local.out
cover-ci-combined.out
coverage.html
4 changes: 4 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ overrides:
- filename: pkg/infra/provisioning/bicep/local_preflight.go
words:
- actioned
- filename: docs/code-coverage-guide.md
words:
- covdata
- GOWORK
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
Expand Down
22 changes: 22 additions & 0 deletions cli/azd/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ go fix -diff ./...
If `go fix -diff` reports any changes, apply them with `go fix ./...` and commit the result.
CI enforces this check — PRs with pending `go fix` suggestions will fail the lint workflow.

### Code Coverage

azd collects coverage from both unit tests and integration/functional tests. Several modes are available depending on your needs.

> **Working directory**: Run all coverage commands from the **repository root** (not `cli/azd/`). The scripts handle `cd cli/azd` internally.

| Mode | Command | Mage Target | Prerequisites | Speed |
|------|---------|-------------|--------------|-------|
| **Unit only** (recommended) | `./eng/scripts/Get-LocalCoverageReport.ps1 -ShowReport -UnitOnly` | `mage coverage:unit` | None | ~5-10 min |
| **Hybrid** (local unit + CI integration) | `./eng/scripts/Get-LocalCoverageReport.ps1 -ShowReport -MergeWithCI` | `mage coverage:hybrid` | `az login` | ~6-11 min |
| **Full local** (unit + integration) | `./eng/scripts/Get-LocalCoverageReport.ps1 -ShowReport` | `mage coverage:full` | Azure subscription + service principal | ~30-60 min |
| **CI baseline** (latest main) | `./eng/scripts/Get-CICoverageReport.ps1 -ShowReport` | `mage coverage:ci` | `az login` | ~1 min |

Additional mage targets: `mage coverage:html` (HTML report), `mage coverage:check` (enforce 50% unit-only threshold; CI gate is 55% combined).
Override the threshold with: `COVERAGE_MIN=55 mage coverage:check`.

**Typical workflow**: Use *Unit only* during development for fast feedback. After pushing a PR, use *Hybrid* or check your PR's CI coverage with `Get-CICoverageReport.ps1 -PullRequestId <N> -ShowReport`.

For HTML reports: add `-Html` to any local command. For threshold checks: add `-MinCoverage <N>`.

See [Code Coverage Guide](./docs/code-coverage-guide.md) for architecture details, prerequisites, and troubleshooting.

> Note: On Windows you may need to add `C:\Program Files\Git\usr\bin` to `%PATH%`

### Debugging (with VSCode)
Expand Down
68 changes: 68 additions & 0 deletions cli/azd/cmd/actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"testing"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStringPtr(t *testing.T) {
t.Parallel()

t.Run("initial_empty", func(t *testing.T) {
t.Parallel()
p := stringPtr{}
assert.Equal(t, "", p.String())
assert.Equal(t, "string", p.Type())
})

t.Run("set_value", func(t *testing.T) {
t.Parallel()
p := stringPtr{}
err := p.Set("hello")
require.NoError(t, err)
assert.Equal(t, "hello", p.String())
})

t.Run("set_empty_string", func(t *testing.T) {
t.Parallel()
p := stringPtr{}
err := p.Set("")
require.NoError(t, err)
assert.Equal(t, "", p.String())
assert.NotNil(t, p.ptr)
})
}

func TestBoolPtr(t *testing.T) {
t.Parallel()

t.Run("initial_false", func(t *testing.T) {
t.Parallel()
p := boolPtr{}
assert.Equal(t, "false", p.String())
assert.Equal(t, "", p.Type())
})

t.Run("set_true", func(t *testing.T) {
t.Parallel()
p := boolPtr{}
err := p.Set("true")
require.NoError(t, err)
assert.Equal(t, "true", p.String())
})
}

func TestUploadAction_Run_NilTelemetry(t *testing.T) {
t.Parallel()

action := newUploadAction(&internal.GlobalCommandOptions{})
result, err := action.Run(t.Context())
require.NoError(t, err)
require.Nil(t, result)
}
125 changes: 125 additions & 0 deletions cli/azd/cmd/auth_login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"errors"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/require"
)

func TestRunningOnCodespacesBrowser(t *testing.T) {
t.Parallel()

tests := []struct {
name string
stdout string
runErr error
expected bool
}{
{
name: "browser_environment_detected",
stdout: "The --status argument is not yet supported in browsers",
expected: true,
},
{
name: "desktop_environment",
stdout: "Version: 1.85.0\nCommit: abc123",
expected: false,
},
{
name: "empty_output",
stdout: "",
expected: false,
},
{
name: "command_fails",
runErr: errors.New("code: command not found"),
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockContext := mocks.NewMockContext(context.Background())

if tt.runErr != nil {
mockContext.CommandRunner.When(func(args exec.RunArgs, commandName string) bool {
return args.Cmd == "code"
}).SetError(tt.runErr)
} else {
mockContext.CommandRunner.When(func(args exec.RunArgs, commandName string) bool {
return args.Cmd == "code"
}).Respond(exec.RunResult{
Stdout: tt.stdout,
})
}

result := runningOnCodespacesBrowser(t.Context(), mockContext.CommandRunner)
require.Equal(t, tt.expected, result)
})
}
}

func TestParseUseDeviceCode(t *testing.T) {
t.Parallel()

tests := []struct {
name string
flagPtr *string
expected bool
expectError bool
}{{
name: "flag_true",
flagPtr: new("true"),
expected: true,
},
{
name: "flag_false",
flagPtr: new("false"),
expected: false,
},
{
name: "flag_invalid",
flagPtr: new("notabool"),
expectError: true,
},
{
name: "flag_not_set",
flagPtr: nil,
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockContext := mocks.NewMockContext(context.Background())

// Mock code --status to return non-browser env
mockContext.CommandRunner.When(func(args exec.RunArgs, commandName string) bool {
return args.Cmd == "code"
}).Respond(exec.RunResult{
Stdout: "Version: 1.85.0",
})

flag := boolPtr{ptr: tt.flagPtr}
result, err := parseUseDeviceCode(t.Context(), flag, mockContext.CommandRunner)

if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), "unexpected boolean input")
return
}

require.NoError(t, err)
require.Equal(t, tt.expected, result)
})
}
}
Loading
Loading