Skip to content

Add support for recording requests in integration acceptance tests #2720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 68 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
bf2fc1f
acc: Refactor starting test server in a separate file
shreyas-goenka Apr 14, 2025
4f781ab
-
shreyas-goenka Apr 14, 2025
2f5aca3
-
shreyas-goenka Apr 14, 2025
0892552
-
shreyas-goenka Apr 14, 2025
0e7806b
-
shreyas-goenka Apr 14, 2025
465d74d
-
shreyas-goenka Apr 14, 2025
714226c
-
shreyas-goenka Apr 23, 2025
875ac17
-
shreyas-goenka Apr 23, 2025
530f572
Merge remote-tracking branch 'origin' into proxy-integration
shreyas-goenka Apr 23, 2025
b8b22d4
fix lint
shreyas-goenka Apr 23, 2025
9dec739
mvp
shreyas-goenka Apr 23, 2025
34e5159
got this to a working state after refactor; TODO: cleanup and more te…
shreyas-goenka Apr 23, 2025
a030b3b
lint
shreyas-goenka Apr 23, 2025
fe141a5
-
shreyas-goenka Apr 24, 2025
cb0fde7
fix integration test users
shreyas-goenka Apr 24, 2025
12ed683
-
shreyas-goenka Apr 24, 2025
b4fb7db
Merge remote-tracking branch 'origin' into proxy-integration
shreyas-goenka Apr 24, 2025
d1c13dd
cleanup
shreyas-goenka Apr 24, 2025
7ddb576
add crud test
shreyas-goenka Apr 24, 2025
a2eb106
split tests and add workspace-io test
shreyas-goenka Apr 24, 2025
b32da96
add volume io testcase
shreyas-goenka Apr 24, 2025
9b36628
-
shreyas-goenka Apr 24, 2025
18f08e8
fix test on windows
shreyas-goenka Apr 24, 2025
c5deda8
-
shreyas-goenka Apr 24, 2025
cd8ebc6
fix windows base64
shreyas-goenka Apr 25, 2025
ef2ec73
fix windows newline in raw body
shreyas-goenka Apr 25, 2025
6e5fa12
lint
shreyas-goenka Apr 25, 2025
ee83d69
fix windows newline in raw body again
shreyas-goenka Apr 25, 2025
c95c2b8
pass process env to child process
shreyas-goenka Apr 25, 2025
ab5a324
-
shreyas-goenka Apr 25, 2025
94aaa4a
-
shreyas-goenka Apr 25, 2025
bdddde6
-
shreyas-goenka Apr 25, 2025
609fb7e
cleanup
shreyas-goenka Apr 25, 2025
b9f52cb
-
shreyas-goenka Apr 25, 2025
f2faf04
cleanup
shreyas-goenka Apr 25, 2025
c4e5aa8
final cleanup
shreyas-goenka Apr 25, 2025
3ca94bc
Merge remote-tracking branch 'origin' into proxy-integration
shreyas-goenka Apr 25, 2025
c72a6d7
always record requests in proxy
shreyas-goenka May 5, 2025
55767f1
common code for current-user in PrepareServerAndClient
shreyas-goenka May 5, 2025
573e82c
-
shreyas-goenka May 5, 2025
4861e27
merge
shreyas-goenka May 6, 2025
0830321
merge
shreyas-goenka May 6, 2025
4bd1083
have start functions return host and token directly
shreyas-goenka May 6, 2025
e94556d
undo local server changse
shreyas-goenka May 6, 2025
e61ecc6
reduce diff
shreyas-goenka May 6, 2025
bc39366
-
shreyas-goenka May 6, 2025
38e8901
-
shreyas-goenka May 6, 2025
8e37d99
separate package for the proxy server
shreyas-goenka May 6, 2025
b86da3c
-
shreyas-goenka May 6, 2025
ac6ab14
-
shreyas-goenka May 6, 2025
a2639f0
-
shreyas-goenka May 6, 2025
147077f
Merge remote-tracking branch 'origin' into proxy-integration
shreyas-goenka May 7, 2025
ca98d88
-
shreyas-goenka May 7, 2025
0f5a61c
-
shreyas-goenka May 7, 2025
f326379
-
shreyas-goenka May 7, 2025
a748038
-
shreyas-goenka May 7, 2025
6932a81
-
shreyas-goenka May 7, 2025
a52d28e
-
shreyas-goenka May 7, 2025
74cc3a7
-
shreyas-goenka May 7, 2025
958da1f
-
shreyas-goenka May 7, 2025
51a7e26
-
shreyas-goenka May 7, 2025
5a38af7
-
shreyas-goenka May 7, 2025
9111f6f
-
shreyas-goenka May 7, 2025
027d512
-
shreyas-goenka May 7, 2025
f9ecc83
use http server
shreyas-goenka May 7, 2025
bcfa2d2
-
shreyas-goenka May 7, 2025
9a5e10e
address comments
shreyas-goenka May 7, 2025
084a5c3
print requests inline
shreyas-goenka May 7, 2025
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
6 changes: 6 additions & 0 deletions .wsignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ experimental/python/docs/images/databricks-logo.svg
**/*.zip
**/*.whl

# new lines are recorded differently on windows and unix.
# In unix: "raw_body": "hello, world\n"
# In windows: "raw_body": "hello, world\r\n"
# In order to prevent that difference, hello.txt does not have a trailing newline.
acceptance/selftest/record_cloud/volume-io/hello.txt
Copy link
Contributor Author

@shreyas-goenka shreyas-goenka Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried out [[Repls]], but they did not seem to work. I could not reproduce this locally on Windows, and it only seems to happen in the Windows CI runner.

I've already spent 1-2 hours trying to fix / patch this, so the easiest thing to do at this point is to ignore the lint rule.
I'm not sure we want to normalize in the raw_body since it is a part of the HTTP payload.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is needed, I don't see acceptance/selftest/record_cloud/volume-io/hello.txt having no trailing newline? (github highlights such files).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file definitely does not have a trailing newline (at least in my local checkout). I'm not sure why it's not being highlighted. Let's see if the latest integration tests pass, in which case maybe github is not rendering it properly.


# "bundle run" has trailing whitespace:
acceptance/bundle/integration_whl/*/output.txt

Expand Down
20 changes: 11 additions & 9 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"slices"
"sort"
"strings"
"sync"
"testing"
"time"
"unicode/utf8"
Expand Down Expand Up @@ -242,15 +241,15 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
if len(expanded[0]) > 0 {
t.Logf("Running test with env %v", expanded[0])
}
runTest(t, dir, coverDir, repls.Clone(), config, configPath, expanded[0])
runTest(t, dir, coverDir, repls.Clone(), config, configPath, expanded[0], inprocessMode)
} else {
for _, envset := range expanded {
envname := strings.Join(envset, "/")
t.Run(envname, func(t *testing.T) {
if !inprocessMode {
t.Parallel()
}
runTest(t, dir, coverDir, repls.Clone(), config, configPath, envset)
runTest(t, dir, coverDir, repls.Clone(), config, configPath, envset, inprocessMode)
})
}
}
Expand Down Expand Up @@ -342,7 +341,14 @@ func getSkipReason(config *internal.TestConfig, configPath string) string {
return ""
}

func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsContext, config internal.TestConfig, configPath string, customEnv []string) {
func runTest(t *testing.T,
dir, coverDir string,
repls testdiff.ReplacementsContext,
config internal.TestConfig,
configPath string,
customEnv []string,
inprocessMode bool,
) {
if LogConfig {
configBytes, err := json.MarshalIndent(config, "", " ")
require.NoError(t, err)
Expand Down Expand Up @@ -395,16 +401,12 @@ func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsCont
} else if isRunningOnCloud {
timeout = max(timeout, config.TimeoutCloud)
}

ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
args := []string{"bash", "-euo", "pipefail", EntryPointScript}
cmd := exec.CommandContext(ctx, args[0], args[1:]...)

// This mutex is used to synchronize recording requests
var serverMutex sync.Mutex

cfg, user := internal.PrepareServerAndClient(t, config, LogRequests, tmpDir, &serverMutex)
cfg, user := internal.PrepareServerAndClient(t, config, LogRequests, tmpDir)
testdiff.PrepareReplacementsUser(t, &repls, user)
testdiff.PrepareReplacementsWorkspaceConfig(t, &repls, cfg)

Expand Down
143 changes: 97 additions & 46 deletions acceptance/internal/prepare_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
"time"
"unicode/utf8"

sdkconfig "github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/iam"

"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/testproxy"
"github.com/databricks/cli/libs/testserver"
"github.com/databricks/databricks-sdk-go"
sdkconfig "github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -40,81 +40,115 @@ func isTruePtr(value *bool) bool {
return value != nil && *value
}

func PrepareServerAndClient(t *testing.T, config TestConfig, logRequests bool, outputDir string, mu *sync.Mutex) (*sdkconfig.Config, iam.User) {
func PrepareServerAndClient(t *testing.T, config TestConfig, logRequests bool, outputDir string) (*sdkconfig.Config, iam.User) {
cloudEnv := os.Getenv("CLOUD_ENV")
recordRequests := isTruePtr(config.RecordRequests)

// If we are running on a cloud environment, use the host configured in the
// environment.
if cloudEnv != "" {
w, err := databricks.NewWorkspaceClient()
require.NoError(t, err)

user, err := w.CurrentUser.Me(context.Background())
require.NoError(t, err, "Failed to get current user")

return w.Config, *user
}
cfg := w.Config

recordRequests := isTruePtr(config.RecordRequests)
// If we are running in a cloud environment AND we are recording requests,
// start a dedicated server to act as a reverse proxy to a real Databricks workspace.
if recordRequests {
host, token := startProxyServer(t, logRequests, config.IncludeRequestHeaders, outputDir)
cfg = &sdkconfig.Config{
Host: host,
Token: token,
}
}

tokenSuffix := strings.ReplaceAll(uuid.NewString(), "-", "")
token := "dbapi" + tokenSuffix
return cfg, *user
}

// If we are not recording requests, and no custom server server stubs are configured,
// If we are not recording requests, and no custom server stubs are configured,
// use the default shared server.
if len(config.Server) == 0 && !recordRequests {
return &sdkconfig.Config{
// Use a unique token for each test. This allows us to maintain
// separate state for each test in fake workspaces.
tokenSuffix := strings.ReplaceAll(uuid.NewString(), "-", "")
token := "dbapi" + tokenSuffix

cfg := &sdkconfig.Config{
Host: os.Getenv("DATABRICKS_DEFAULT_HOST"),
Token: token,
}, TestUser
}
}

host := startDedicatedServer(t, config.Server, recordRequests, logRequests, config.IncludeRequestHeaders, outputDir, mu)
return cfg, TestUser
}

return &sdkconfig.Config{
// Default case. Start a dedicated local server for the test with the server stubs configured
// as overrides.
host, token := startLocalServer(t, config.Server, recordRequests, logRequests, config.IncludeRequestHeaders, outputDir)
cfg := &sdkconfig.Config{
Host: host,
Token: token,
}, TestUser
}

// For the purposes of replacements, use testUser for local runs.
// Note, users might have overriden /api/2.0/preview/scim/v2/Me but that should not affect the replacement:
return cfg, TestUser
}

func startDedicatedServer(t *testing.T,
func recordRequestsCallback(t *testing.T, includeHeaders []string, outputDir string) func(request *testserver.Request) {
mu := sync.Mutex{}

return func(request *testserver.Request) {
mu.Lock()
defer mu.Unlock()

req := getLoggedRequest(request, includeHeaders)
reqJson, err := json.MarshalIndent(req, "", " ")
assert.NoErrorf(t, err, "Failed to json-encode: %#v", req)

requestsPath := filepath.Join(outputDir, "out.requests.txt")
f, err := os.OpenFile(requestsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
assert.NoError(t, err)
defer f.Close()

_, err = f.WriteString(string(reqJson) + "\n")
assert.NoError(t, err)
}
}

func logResponseCallback(t *testing.T) func(request *testserver.Request, response *testserver.EncodedResponse) {
mu := sync.Mutex{}

return func(request *testserver.Request, response *testserver.EncodedResponse) {
mu.Lock()
defer mu.Unlock()

t.Logf("%d %s %s\n%s\n%s",
response.StatusCode, request.Method, request.URL,
formatHeadersAndBody("> ", request.Headers, request.Body),
formatHeadersAndBody("# ", response.Headers, response.Body),
)
}
}

func startLocalServer(t *testing.T,
stubs []ServerStub,
recordRequests bool,
logRequests bool,
includeHeaders []string,
outputDir string,
mu *sync.Mutex,
) string {
) (string, string) {
s := testserver.New(t)

// Record API requests in out.requests.txt if RecordRequests is true
// in test.toml
if recordRequests {
requestsPath := filepath.Join(outputDir, "out.requests.txt")
s.RequestCallback = func(request *testserver.Request) {
req := getLoggedRequest(request, includeHeaders)
reqJson, err := json.MarshalIndent(req, "", " ")

mu.Lock()
defer mu.Unlock()

assert.NoErrorf(t, err, "Failed to json-encode: %#v", req)

f, err := os.OpenFile(requestsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
assert.NoError(t, err)
defer f.Close()

_, err = f.WriteString(string(reqJson) + "\n")
assert.NoError(t, err)
}
s.RequestCallback = recordRequestsCallback(t, includeHeaders, outputDir)
}

// Log API responses if the -logrequests flag is set.
if logRequests {
s.ResponseCallback = func(request *testserver.Request, response *testserver.EncodedResponse) {
t.Logf("%d %s %s\n%s\n%s",
response.StatusCode, request.Method, request.URL,
formatHeadersAndBody("> ", request.Headers, request.Body),
formatHeadersAndBody("# ", response.Headers, response.Body),
)
}
s.ResponseCallback = logResponseCallback(t)
}

for ind := range stubs {
Expand All @@ -132,8 +166,25 @@ func startDedicatedServer(t *testing.T,

// The earliest handlers take precedence, add default handlers last
addDefaultHandlers(s)
return s.URL, "dbapi123"
}

func startProxyServer(t *testing.T,
logRequests bool,
includeHeaders []string,
outputDir string,
) (string, string) {
s := testproxy.New(t)

// Always record requests for a proxy server.
s.RequestCallback = recordRequestsCallback(t, includeHeaders, outputDir)

// Log API responses if the -logrequests flag is set.
if logRequests {
s.ResponseCallback = logResponseCallback(t)
}

return s.URL
return s.URL, "dbapi1234"
}

type LoggedRequest struct {
Expand Down
4 changes: 4 additions & 0 deletions acceptance/selftest/record_cloud/basic/out.requests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"method": "GET",
"path": "/api/2.0/preview/scim/v2/Me"
}
3 changes: 3 additions & 0 deletions acceptance/selftest/record_cloud/basic/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

>>> [CLI] current-user me
"[USERNAME]"
2 changes: 2 additions & 0 deletions acceptance/selftest/record_cloud/basic/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Proxy server successfully records a requests and returns a response.
trace $CLI current-user me | jq .name.givenName
4 changes: 4 additions & 0 deletions acceptance/selftest/record_cloud/error/out.requests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"method": "GET",
"path": "/api/2.2/jobs/get"
}
5 changes: 5 additions & 0 deletions acceptance/selftest/record_cloud/error/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

>>> [CLI] jobs get 1234
Error: Job 1234 does not exist.

Exit code: 1
2 changes: 2 additions & 0 deletions acceptance/selftest/record_cloud/error/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Proxy server should successfully return non 200 responses.
errcode trace $CLI jobs get 1234
80 changes: 80 additions & 0 deletions acceptance/selftest/record_cloud/pipeline-crud/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

=== Create a pipeline

>>> print_requests
{
"method": "POST",
"path": "/api/2.0/pipelines",
"body": {
"allow_duplicate_names": true,
"libraries": [
{
"file": {
"path": "/whatever.py"
}
}
],
"name": "test-pipeline-1"
}
}

=== Get the pipeline
>>> [CLI] pipelines get [UUID]
"test-pipeline-1"

>>> print_requests
{
"method": "GET",
"path": "/api/2.0/pipelines/[UUID]"
}

=== Update the pipeline
>>> [CLI] pipelines update [UUID] --json @pipeline2.json

>>> print_requests
{
"method": "PUT",
"path": "/api/2.0/pipelines/[UUID]",
"body": {
"allow_duplicate_names": true,
"libraries": [
{
"file": {
"path": "/whatever.py"
}
}
],
"name": "test-pipeline-2"
}
}

=== Verify the update
>>> [CLI] pipelines get [UUID]
"test-pipeline-2"

>>> print_requests
{
"method": "GET",
"path": "/api/2.0/pipelines/[UUID]"
}

=== Delete the pipeline
>>> [CLI] pipelines delete [UUID]

>>> print_requests
{
"method": "DELETE",
"path": "/api/2.0/pipelines/[UUID]"
}

=== Verify the deletion
>>> [CLI] pipelines get [UUID]
Error: The specified pipeline [UUID] was not found.

Exit code: 1

>>> print_requests
{
"method": "GET",
"path": "/api/2.0/pipelines/[UUID]"
}
11 changes: 11 additions & 0 deletions acceptance/selftest/record_cloud/pipeline-crud/pipeline1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "test-pipeline-1",
"allow_duplicate_names": true,
"libraries": [
{
"file": {
"path": "/whatever.py"
}
}
]
}
Loading
Loading