Skip to content
Merged
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ vendor/
!.vscode/cspell.json

# api view file
*.gosource
*.gosource

# Default Test Proxy Assets restore directory
.assets
11 changes: 7 additions & 4 deletions eng/scripts/run_tests.ps1
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#Requires -Version 7.0

Param(
[Parameter(Mandatory = $true)]
[string] $serviceDirectory,
[string] $testTimeout
[string] $testTimeout = "10s"
)

$ErrorActionPreference = 'Stop'

Push-Location sdk/$serviceDirectory
Write-Host "##[command] Executing 'go test -timeout $testTimeout -v -coverprofile coverage.txt ./...' in sdk/$serviceDirectory"

go test -timeout $testTimeout -v -coverprofile coverage.txt ./... | Tee-Object -FilePath outfile.txt
# go test will return a non-zero exit code on test failures so don't skip generating the report in this case
$GOTESTEXITCODE = $LASTEXITCODE

Get-Content outfile.txt | go-junit-report > report.xml
Get-Content -Raw outfile.txt | go-junit-report > report.xml

# if no tests were actually run (e.g. examples) delete the coverage file so it's omitted from the coverage report
if (Select-String -path ./report.xml -pattern '<testsuites></testsuites>' -simplematch -quiet) {
Expand All @@ -32,8 +35,8 @@ if (Select-String -path ./report.xml -pattern '<testsuites></testsuites>' -simpl
Get-Content ./coverage.json | gocov-xml > ./coverage.xml
Get-Content ./coverage.json | gocov-html > ./coverage.html

Move-Item ./coverage.xml $repoRoot
Move-Item ./coverage.html $repoRoot
Move-Item -Force ./coverage.xml $repoRoot
Comment thread
benbp marked this conversation as resolved.
Move-Item -Force ./coverage.html $repoRoot

# use internal tool to fail if coverage is too low
Pop-Location
Expand Down
2 changes: 2 additions & 0 deletions sdk/internal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

Support test recording assets external to repository
Comment thread
jhendrixMSFT marked this conversation as resolved.
Outdated

### Breaking Changes

### Bugs Fixed
Expand Down
124 changes: 110 additions & 14 deletions sdk/internal/recording/recording.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -54,6 +54,7 @@ const (
randomSeedVariableName = "randomSeed"
nowVariableName = "now"
ModeEnvironmentVariableName = "AZURE_TEST_MODE"
recordingAssetConfigName = "assets.json"
)

// Inspired by https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
Expand Down Expand Up @@ -574,7 +575,100 @@ func (r RecordingOptions) baseURL() string {
}

func getTestId(pathToRecordings string, t *testing.T) string {
return path.Join(pathToRecordings, "recordings", t.Name()+".json")
return filepath.Join(pathToRecordings, "recordings", t.Name()+".json")
}

func getGitRoot(fromPath string) (string, error) {
Comment thread
jhendrixMSFT marked this conversation as resolved.
absPath, err := filepath.Abs(fromPath)
if err != nil {
return "", err
}
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
cmd.Dir = absPath

root, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("Unable to find git root for path '%s'", absPath)
}

// Wrap with Abs() to get os-specific path separators to support sub-path matching
return filepath.Abs(strings.TrimSpace(string(root)))
}

// Traverse up from a recording path until an asset config file is found.
// Stop searching when the root of the git repository is reached.
func findAssetsConfigFile(fromPath string) (string, error) {
absPath, err := filepath.Abs(fromPath)
if err != nil {
return "", err
}
assetConfigPath := filepath.Join(absPath, recordingAssetConfigName)
gitDirectoryPath := filepath.Join(absPath, ".git")

if _, err := os.Stat(assetConfigPath); err == nil {
return assetConfigPath, nil
} else if !errors.Is(err, fs.ErrNotExist) {
return "", err
}

if _, err := os.Stat(gitDirectoryPath); err == nil {
Comment thread
benbp marked this conversation as resolved.
Outdated
return "", nil
} else if !errors.Is(err, fs.ErrNotExist) {
return "", err
}

trimmedPath := strings.TrimRight(absPath, string(os.PathSeparator))
parentDir, _ := filepath.Split(trimmedPath)
Comment thread
benbp marked this conversation as resolved.
Outdated
// If the parent directory is the same as current dir, we've reached root
// This shouldn't be hit due to checks in getGitRoot, but it can't hurt to be defensive
if parentDir == trimmedPath {
return "", nil
}

return findAssetsConfigFile(parentDir)
}

// Returns absolute and relative paths to an asset configuration file, or an error.
func getAssetsConfigLocation(pathToRecordings string) (string, string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", "", err
}
gitRoot, err := getGitRoot(cwd)
if err != nil {
return "", "", err
}
abs, err := findAssetsConfigFile(filepath.Join(gitRoot, pathToRecordings))
if err != nil {
return "", "", err
}

// Pass a path relative to the git root to test proxy so that paths
// can be resolved when the repo root is mounted as a volume in a container
rel := strings.Replace(abs, gitRoot, "", 1)
rel = strings.TrimLeft(rel, string(os.PathSeparator))
return abs, rel, nil
}

func requestStart(url string, testId string, assetConfigLocation string) (*http.Response, error) {
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")
reqBody := map[string]string{"x-recording-file": testId}
if assetConfigLocation != "" {
reqBody["x-recording-assets-file"] = assetConfigLocation
}
marshalled, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(marshalled))
req.ContentLength = int64(len(marshalled))

return client.Do(req)
}

// Start tells the test proxy to begin accepting requests for a given test
Expand All @@ -595,25 +689,27 @@ func Start(t *testing.T, pathToRecordings string, options *RecordingOptions) err

testId := getTestId(pathToRecordings, t)

url := fmt.Sprintf("%s/%s/start", options.baseURL(), recordMode)

req, err := http.NewRequest("POST", url, nil)
absAssetLocation, relAssetLocation, err := getAssetsConfigLocation(pathToRecordings)
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")
marshalled, err := json.Marshal(map[string]string{"x-recording-file": testId})
if err != nil {
return err
}
req.Body = io.NopCloser(bytes.NewReader(marshalled))
req.ContentLength = int64(len(marshalled))
url := fmt.Sprintf("%s/%s/start", options.baseURL(), recordMode)

resp, err := client.Do(req)
if err != nil {
var resp *http.Response
if absAssetLocation == "" {
resp, err = requestStart(url, testId, "")
if err != nil {
return err
}
} else if resp, err = requestStart(url, testId, absAssetLocation); err != nil {
return err
} else if resp.StatusCode >= 400 {
if resp, err = requestStart(url, testId, relAssetLocation); err != nil {
return err
}
}

recId := resp.Header.Get(IDHeader)
if recId == "" {
b, err := io.ReadAll(resp.Body)
Expand Down
58 changes: 58 additions & 0 deletions sdk/internal/recording/recording_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,64 @@ func TestHostAndScheme(t *testing.T) {
require.Equal(t, r.host(), "localhost:5000")
}

func TestGitRootDetection(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
gitRoot, err := getGitRoot(cwd)
require.NoError(t, err)

parentDir, _ := filepath.Split(gitRoot)
_, err = getGitRoot(parentDir)
require.Error(t, err)
}

func TestRecordingAssetConfigNotExist(t *testing.T) {
absPath, relPath, err := getAssetsConfigLocation(".")
require.NoError(t, err)
require.Equal(t, "", absPath)
require.Equal(t, "", relPath)
}

func TestRecordingAssetConfigOutOfBounds(t *testing.T) {
absPath, relPath, err := getAssetsConfigLocation("../../../../")
require.NoError(t, err)
require.Equal(t, "", absPath)
require.Equal(t, "", relPath)
}

func TestRecordingAssetConfig(t *testing.T) {
cases := []struct{ expectedDirectory, searchDirectory, testFileLocation string }{
{"sdk/internal/recording", "sdk/internal/recording", recordingAssetConfigName},
{"sdk/internal/recording", "sdk/internal/recording/", recordingAssetConfigName},
{"sdk/internal", "sdk/internal/recording", "../" + recordingAssetConfigName},
{"sdk/internal", "sdk/internal/recording/", "../" + recordingAssetConfigName},
}

cwd, err := os.Getwd()
require.NoError(t, err)
gitRoot, err := getGitRoot(cwd)
require.NoError(t, err)

for _, c := range cases {
_ = os.Remove(c.testFileLocation)
o, err := os.Create(c.testFileLocation)
require.NoError(t, err)
o.Close()

absPath, relPath, err := getAssetsConfigLocation(c.searchDirectory)
// Clean up first in case of an assertion panic
require.NoError(t, os.Remove(c.testFileLocation))
require.NoError(t, err)

expected := c.expectedDirectory + string(os.PathSeparator) + recordingAssetConfigName
expected = strings.ReplaceAll(expected, "/", string(os.PathSeparator))
require.Equal(t, expected, relPath)

absPathExpected := filepath.Join(gitRoot, expected)
require.Equal(t, absPathExpected, absPath)
}
}

func TestFindProxyCertLocation(t *testing.T) {
savedValue, ok := os.LookupEnv("PROXY_CERT")
if ok {
Expand Down