Skip to content

Commit 2827e1e

Browse files
authored
Add support for making recordings external to the repository (Azure#19322)
* Improve error and pipeline handling in run_tests.ps1 * Add test proxy default assets directory to gitignore * Support test proxy external asset mode for test recordings * Handle recording asset sync better for different go test working directories * Handle relative and absolute paths depending on test proxy working directory * Use git executable to detect git root in test recording handler * Improve os-aware path handling in test recording framework asset sync * Cast git response to string * Simplify git and parent path checks * Bump sdk/internal package version
1 parent f6111b1 commit 2827e1e

File tree

6 files changed

+182
-26
lines changed

6 files changed

+182
-26
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,7 @@ vendor/
4949
!.vscode/cspell.json
5050

5151
# api view file
52-
*.gosource
52+
*.gosource
53+
54+
# Default Test Proxy Assets restore directory
55+
.assets

eng/scripts/run_tests.ps1

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
#Requires -Version 7.0
22

33
Param(
4+
[Parameter(Mandatory = $true)]
45
[string] $serviceDirectory,
5-
[string] $testTimeout
6+
[string] $testTimeout = "10s"
67
)
78

9+
$ErrorActionPreference = 'Stop'
10+
811
Push-Location sdk/$serviceDirectory
912
Write-Host "##[command] Executing 'go test -timeout $testTimeout -v -coverprofile coverage.txt ./...' in sdk/$serviceDirectory"
1013

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

15-
Get-Content outfile.txt | go-junit-report > report.xml
18+
Get-Content -Raw outfile.txt | go-junit-report > report.xml
1619

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

35-
Move-Item ./coverage.xml $repoRoot
36-
Move-Item ./coverage.html $repoRoot
38+
Move-Item -Force ./coverage.xml $repoRoot
39+
Move-Item -Force ./coverage.html $repoRoot
3740

3841
# use internal tool to fail if coverage is too low
3942
Pop-Location

sdk/internal/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 1.0.2 (Unreleased)
3+
## 1.1.0 (2022-10-20)
44

55
### Features Added
66

7-
### Breaking Changes
8-
9-
### Bugs Fixed
10-
11-
### Other Changes
7+
* Support test recording assets external to repository
128

139
## 1.0.1 (2022-08-22)
1410

sdk/internal/recording/recording.go

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import (
1414
"errors"
1515
"fmt"
1616
"io"
17+
"io/fs"
1718
"log"
1819
"math/rand"
1920
"net/http"
2021
"os"
2122
"os/exec"
22-
"path"
2323
"path/filepath"
2424
"runtime"
2525
"strconv"
@@ -54,6 +54,7 @@ const (
5454
randomSeedVariableName = "randomSeed"
5555
nowVariableName = "now"
5656
ModeEnvironmentVariableName = "AZURE_TEST_MODE"
57+
recordingAssetConfigName = "assets.json"
5758
)
5859

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

576577
func getTestId(pathToRecordings string, t *testing.T) string {
577-
return path.Join(pathToRecordings, "recordings", t.Name()+".json")
578+
return filepath.Join(pathToRecordings, "recordings", t.Name()+".json")
579+
}
580+
581+
func getGitRoot(fromPath string) (string, error) {
582+
absPath, err := filepath.Abs(fromPath)
583+
if err != nil {
584+
return "", err
585+
}
586+
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
587+
cmd.Dir = absPath
588+
589+
root, err := cmd.CombinedOutput()
590+
if err != nil {
591+
return "", fmt.Errorf("Unable to find git root for path '%s'", absPath)
592+
}
593+
594+
// Wrap with Abs() to get os-specific path separators to support sub-path matching
595+
return filepath.Abs(strings.TrimSpace(string(root)))
596+
}
597+
598+
// Traverse up from a recording path until an asset config file is found.
599+
// Stop searching when the root of the git repository is reached.
600+
func findAssetsConfigFile(fromPath string, untilPath string) (string, error) {
601+
absPath, err := filepath.Abs(fromPath)
602+
if err != nil {
603+
return "", err
604+
}
605+
assetConfigPath := filepath.Join(absPath, recordingAssetConfigName)
606+
607+
if _, err := os.Stat(assetConfigPath); err == nil {
608+
return assetConfigPath, nil
609+
} else if !errors.Is(err, fs.ErrNotExist) {
610+
return "", err
611+
}
612+
613+
if absPath == untilPath {
614+
return "", nil
615+
}
616+
617+
parentDir := filepath.Dir(absPath)
618+
// This shouldn't be hit due to checks in getGitRoot, but it can't hurt to be defensive
619+
if parentDir == absPath || parentDir == "." {
620+
return "", nil
621+
}
622+
623+
return findAssetsConfigFile(parentDir, untilPath)
624+
}
625+
626+
// Returns absolute and relative paths to an asset configuration file, or an error.
627+
func getAssetsConfigLocation(pathToRecordings string) (string, string, error) {
628+
cwd, err := os.Getwd()
629+
if err != nil {
630+
return "", "", err
631+
}
632+
gitRoot, err := getGitRoot(cwd)
633+
if err != nil {
634+
return "", "", err
635+
}
636+
abs, err := findAssetsConfigFile(filepath.Join(gitRoot, pathToRecordings), gitRoot)
637+
if err != nil {
638+
return "", "", err
639+
}
640+
641+
// Pass a path relative to the git root to test proxy so that paths
642+
// can be resolved when the repo root is mounted as a volume in a container
643+
rel := strings.Replace(abs, gitRoot, "", 1)
644+
rel = strings.TrimLeft(rel, string(os.PathSeparator))
645+
return abs, rel, nil
646+
}
647+
648+
func requestStart(url string, testId string, assetConfigLocation string) (*http.Response, error) {
649+
req, err := http.NewRequest("POST", url, nil)
650+
if err != nil {
651+
return nil, err
652+
}
653+
654+
req.Header.Set("Content-Type", "application/json")
655+
reqBody := map[string]string{"x-recording-file": testId}
656+
if assetConfigLocation != "" {
657+
reqBody["x-recording-assets-file"] = assetConfigLocation
658+
}
659+
marshalled, err := json.Marshal(reqBody)
660+
if err != nil {
661+
return nil, err
662+
}
663+
req.Body = io.NopCloser(bytes.NewReader(marshalled))
664+
req.ContentLength = int64(len(marshalled))
665+
666+
return client.Do(req)
578667
}
579668

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

596685
testId := getTestId(pathToRecordings, t)
597686

598-
url := fmt.Sprintf("%s/%s/start", options.baseURL(), recordMode)
599-
600-
req, err := http.NewRequest("POST", url, nil)
687+
absAssetLocation, relAssetLocation, err := getAssetsConfigLocation(pathToRecordings)
601688
if err != nil {
602689
return err
603690
}
604691

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

613-
resp, err := client.Do(req)
614-
if err != nil {
694+
var resp *http.Response
695+
if absAssetLocation == "" {
696+
resp, err = requestStart(url, testId, "")
697+
if err != nil {
698+
return err
699+
}
700+
} else if resp, err = requestStart(url, testId, absAssetLocation); err != nil {
615701
return err
702+
} else if resp.StatusCode >= 400 {
703+
if resp, err = requestStart(url, testId, relAssetLocation); err != nil {
704+
return err
705+
}
616706
}
707+
617708
recId := resp.Header.Get(IDHeader)
618709
if recId == "" {
619710
b, err := io.ReadAll(resp.Body)

sdk/internal/recording/recording_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,69 @@ func TestHostAndScheme(t *testing.T) {
550550
require.Equal(t, r.host(), "localhost:5000")
551551
}
552552

553+
func TestGitRootDetection(t *testing.T) {
554+
cwd, err := os.Getwd()
555+
require.NoError(t, err)
556+
gitRoot, err := getGitRoot(cwd)
557+
require.NoError(t, err)
558+
559+
parentDir := filepath.Dir(gitRoot)
560+
_, err = getGitRoot(parentDir)
561+
require.Error(t, err)
562+
}
563+
564+
func TestRecordingAssetConfigNotExist(t *testing.T) {
565+
absPath, relPath, err := getAssetsConfigLocation(".")
566+
require.NoError(t, err)
567+
require.Equal(t, "", absPath)
568+
require.Equal(t, "", relPath)
569+
}
570+
571+
func TestRecordingAssetConfigOutOfBounds(t *testing.T) {
572+
cwd, err := os.Getwd()
573+
require.NoError(t, err)
574+
gitRoot, err := getGitRoot(cwd)
575+
require.NoError(t, err)
576+
parentDir := filepath.Dir(gitRoot)
577+
578+
absPath, err := findAssetsConfigFile(parentDir, gitRoot)
579+
require.NoError(t, err)
580+
require.Equal(t, "", absPath)
581+
}
582+
583+
func TestRecordingAssetConfig(t *testing.T) {
584+
cases := []struct{ expectedDirectory, searchDirectory, testFileLocation string }{
585+
{"sdk/internal/recording", "sdk/internal/recording", recordingAssetConfigName},
586+
{"sdk/internal/recording", "sdk/internal/recording/", recordingAssetConfigName},
587+
{"sdk/internal", "sdk/internal/recording", "../" + recordingAssetConfigName},
588+
{"sdk/internal", "sdk/internal/recording/", "../" + recordingAssetConfigName},
589+
}
590+
591+
cwd, err := os.Getwd()
592+
require.NoError(t, err)
593+
gitRoot, err := getGitRoot(cwd)
594+
require.NoError(t, err)
595+
596+
for _, c := range cases {
597+
_ = os.Remove(c.testFileLocation)
598+
o, err := os.Create(c.testFileLocation)
599+
require.NoError(t, err)
600+
o.Close()
601+
602+
absPath, relPath, err := getAssetsConfigLocation(c.searchDirectory)
603+
// Clean up first in case of an assertion panic
604+
require.NoError(t, os.Remove(c.testFileLocation))
605+
require.NoError(t, err)
606+
607+
expected := c.expectedDirectory + string(os.PathSeparator) + recordingAssetConfigName
608+
expected = strings.ReplaceAll(expected, "/", string(os.PathSeparator))
609+
require.Equal(t, expected, relPath)
610+
611+
absPathExpected := filepath.Join(gitRoot, expected)
612+
require.Equal(t, absPathExpected, absPath)
613+
}
614+
}
615+
553616
func TestFindProxyCertLocation(t *testing.T) {
554617
savedValue, ok := os.LookupEnv("PROXY_CERT")
555618
if ok {

sdk/internal/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ const (
1111
Module = "internal"
1212

1313
// Version is the semantic version (see http://semver.org) of this module.
14-
Version = "v1.0.2"
14+
Version = "v1.1.0"
1515
)

0 commit comments

Comments
 (0)