Files
Ryan El Kochta b9c4d4be11 Compatibility test suite: add WordPress, Gitea, Nextcloud
The first stage of the gVisor application compatibility test suite. This
commit adds:

- A compatibility testing helper library, containing e.g. logic to write
  service config files, wait for a service to be up, and make HTTP
  requests
- Initial tests for WordPress 7.0.0, Gitea 1.26.2, and Nextcloud 34.0.0.

A large library of test stacks is ready, however I am splitting them
into smaller commits in the interest of review size.

Tests first consist of a stack of images under images/compatibility/.
This is done both for consistency with the existing image tests and so
that the Dockerfiles can be modified, for example to install client or
benchmarking tools to aid with testing.

The tests themselves are written in Go and placed in test/compatibility/.

These tests are kept separate from the existing image tests for a few
reasons. First, the image tests mainly exist to find gVisor regressions;
however, a core goal of the compatibility tests is to detect when
updates to *third-party applications* cause them to stop working in
gVisor. Second, it's probably not a good idea to include the
compatibility tests in our CI/CD pipeline (at least, not yet) due to
concerns about flaky tests and/or increased iteration time. (Maybe they
should be run before a release is cut, though?)

Once the compatibility tests are merged, I'll submit the website portion
which will list the applications we have tested to be compatible. Each
entry will contain a link to its corresponding compatibility test in our
GitHub repository.

test/compatibility/README.md is included with instructions on running
the tests.
2026-07-07 17:46:35 -07:00

157 lines
4.5 KiB
Go

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package compatibility provides shared helpers for gVisor application
// compatibility tests.
package compatibility
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// WriteConfigFile writes content to a file named name in a fresh temporary
// directory and returns the file's absolute path.
func WriteConfigFile(t *testing.T, name, content string) string {
t.Helper()
dir, err := os.MkdirTemp("", "compat-config")
if err != nil {
t.Fatalf("failed to create temp config dir: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
p := filepath.Join(dir, name)
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
t.Fatalf("failed to write config file %s: %v", name, err)
}
return p
}
var httpClient = &http.Client{Timeout: 30 * time.Second}
// Poll repeatedly calls cond until it returns nil, failing the test (via
// t.Fatalf) if it has not succeeded within timeout. It waits interval between
// attempts.
//
// This is the preferred way to wait for a stack to become ready: poll for a
// readiness signal (a health endpoint, an accepted connection, etc).
//
// desc is a short human-readable description of what is being waited for, used in
// the timeout message (e.g. "gitea API to be ready").
func Poll(ctx context.Context, t *testing.T, desc string, timeout, interval time.Duration, cond func() error) {
t.Helper()
deadline := time.Now().Add(timeout)
var lastErr error
for {
if lastErr = cond(); lastErr == nil {
return
}
if time.Now().After(deadline) {
t.Fatalf("timed out after %v waiting for %s: %v", timeout, desc, lastErr)
}
select {
case <-ctx.Done():
t.Fatalf("context cancelled while waiting for %s: %v (last error: %v)", desc, ctx.Err(), lastErr)
case <-time.After(interval):
}
}
}
// Request describes an HTTP request issued by a compatibility test.
type Request struct {
Method string // defaults to GET.
URL string
Body string
ContentType string
Host string // overrides the Host header if non-empty.
Username string // enables HTTP basic auth if non-empty.
Password string
Headers map[string]string
// Timeout overrides the default client timeout for this request.
// Zero uses the default from httpClient.
Timeout time.Duration
}
func (r Request) method() string {
if r.Method == "" {
return http.MethodGet
}
return r.Method
}
// Do issues the request and returns the response status code and body. A
// transport-level failure is returned as an error; an HTTP error status is
// not an error.
func (r Request) Do() (int, string, error) {
var body io.Reader
if r.Body != "" {
body = strings.NewReader(r.Body)
}
req, err := http.NewRequest(r.method(), r.URL, body)
if err != nil {
return 0, "", err
}
if r.ContentType != "" {
req.Header.Set("Content-Type", r.ContentType)
}
for k, v := range r.Headers {
req.Header.Set(k, v)
}
if r.Host != "" {
req.Host = r.Host
}
if r.Username != "" {
req.SetBasicAuth(r.Username, r.Password)
}
client := httpClient
if r.Timeout > 0 {
client = &http.Client{Timeout: r.Timeout}
}
resp, err := client.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, "", fmt.Errorf("reading body of %s %s: %w", r.method(), r.URL, err)
}
return resp.StatusCode, string(respBody), nil
}
// DoOrFatal issues the request, failing the test on a transport error or if the
// status code is not wantStatus, and returns the response body.
func (r Request) DoOrFatal(t *testing.T, wantStatus int) string {
t.Helper()
status, body, err := r.Do()
if err != nil {
t.Fatalf("%s %s failed: %v", r.method(), r.URL, err)
}
if status != wantStatus {
t.Fatalf("%s %s: got status %d, want %d; body: %s", r.method(), r.URL, status, wantStatus, body)
}
return body
}
// Get is a convenience wrapper for a plain GET.
func Get(url string) (int, string, error) {
return Request{URL: url}.Do()
}