Add HW 3
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
package hw3test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RunTestEmptyWorkDir will check that server does exit(1) if working directory is empty.
|
||||
func RunTestEmptyWorkDir(t *TC, seed int64, runner Runner) {
|
||||
t.RunBySeed(seed, func(t *TC) {
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
|
||||
port, err := GetFreePort()
|
||||
require.NoError(t, err, "failed to get free port for the server")
|
||||
runOpts := RunOpts{
|
||||
Port: port,
|
||||
WorkingDirectory: "",
|
||||
ServerDomain: "localhost",
|
||||
ListenAddr: "0.0.0.0",
|
||||
ExitCode: make(chan int),
|
||||
}
|
||||
|
||||
// Start the solution.
|
||||
runOpts.GenerateRunConfig(t, r, &EnvGen{})
|
||||
stop, err := runner.Run(t, runOpts)
|
||||
require.NoError(t, err, "failed to start solution")
|
||||
defer stop()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 10):
|
||||
require.FailNow(t, "Server didn't exit(1) in 10 seconds")
|
||||
case ec := <-runOpts.ExitCode:
|
||||
require.Equal(t, 1, ec, "Server exited with wrong code")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func RunTests(t *TC, seed int64, runner Runner, envGen *EnvGen, queriesGen *QueriesGen) {
|
||||
failed := !t.RunBySeed(seed, func(t *TC) {
|
||||
env, err := envGen.Generate(seed)
|
||||
require.NoError(t, err, "failed to generate environment")
|
||||
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
|
||||
queriesSeeds := make([]int64, queriesGen.Count)
|
||||
for i := range queriesSeeds {
|
||||
queriesSeeds[i] = r.Int63n(100000)
|
||||
}
|
||||
|
||||
envDirName := GenFilename(r, 16)
|
||||
envDir := filepath.Join(envGen.TempDirectory, envDirName)
|
||||
|
||||
runOpts := RunOpts{
|
||||
WorkingDirectory: envDir,
|
||||
ServerDomain: "localhost",
|
||||
ListenAddr: "0.0.0.0",
|
||||
}
|
||||
if queriesGen.AllHeaders {
|
||||
possibleDomains := []string{
|
||||
"localhost",
|
||||
"cs.hse.ru",
|
||||
"example.com",
|
||||
"z0r.de",
|
||||
"distsys-course.homework.net",
|
||||
}
|
||||
runOpts.ServerDomain = possibleDomains[r.Intn(len(possibleDomains))]
|
||||
}
|
||||
|
||||
queries := queriesGen.Generate(t, env, envGen, queriesSeeds, runOpts, seed)
|
||||
if envGen.SparseUnusedFiles {
|
||||
needed := make(map[string]bool)
|
||||
for _, query := range queries {
|
||||
if query.Method == http.MethodGet || query.Method == http.MethodPut {
|
||||
needed[path.Clean(query.Path)] = true
|
||||
}
|
||||
}
|
||||
err = env.RootDir.WriteToDiskSelected(envDir, needed)
|
||||
} else {
|
||||
err = env.RootDir.WriteToDisk(envDir)
|
||||
}
|
||||
require.NoError(t, err, "failed to write environment to disk")
|
||||
defer os.RemoveAll(envDir)
|
||||
port, err := GetFreePort()
|
||||
require.NoError(t, err, "failed to get free port for the server")
|
||||
runOpts.Port = port
|
||||
|
||||
// Start the solution.
|
||||
runOpts.GenerateRunConfig(t, r, envGen)
|
||||
stop, err := runner.Run(t, runOpts)
|
||||
require.NoError(t, err, "failed to start solution")
|
||||
defer stop()
|
||||
|
||||
// Await server to bind to port.
|
||||
err = WaitForServer(t, runOpts)
|
||||
require.NoError(t, err, "failed to wait for server")
|
||||
|
||||
// Run the queries.
|
||||
for i, query := range queries {
|
||||
query := query
|
||||
ok := t.RunBySeed(query.Seed, func(t *TC) {
|
||||
RunQuery(t, env, envDir, runOpts, query)
|
||||
})
|
||||
shouldAbort := !ok
|
||||
if shouldAbort {
|
||||
Warn(t, "Skipping next queries because of the failed query", zap.Int("skipped", len(queries)-1-i), zap.String("failed", fmt.Sprintf("%s/%v", t.Name(), query.Seed)))
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
shouldAbort := failed
|
||||
if shouldAbort {
|
||||
Warn(t, "Skipping next tests in a group because last test has failed")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func WaitForServer(t *TC, opts RunOpts) error {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
var lastErr error
|
||||
for attempt := 0; time.Until(deadline) > 0; attempt++ {
|
||||
probeTimeout := time.Until(deadline)
|
||||
if probeTimeout > time.Second {
|
||||
probeTimeout = time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(t, probeTimeout)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", opts.Address(), nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("failed to create context request: %w", err)
|
||||
}
|
||||
req.Host = opts.ServerDomain
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
cancel()
|
||||
lastErr = err
|
||||
if attempt == 0 || attempt%10 == 0 {
|
||||
Debug(t, "Waiting for server startup", zap.Int("attempt", attempt), zap.Error(err))
|
||||
}
|
||||
if remaining := time.Until(deadline); remaining > 0 {
|
||||
if remaining > 100*time.Millisecond {
|
||||
remaining = 100 * time.Millisecond
|
||||
}
|
||||
time.Sleep(remaining)
|
||||
}
|
||||
}
|
||||
Warn(t, "Server didn't get up in time, aborting", zap.Error(lastErr))
|
||||
return fmt.Errorf("server did not start in 10 seconds: %w", lastErr)
|
||||
}
|
||||
|
||||
// RunQuery runs a single query. Server address and its configuration is taken from runOpts.
|
||||
// Initial environment is described by env, but it may be changed with queries. Actual
|
||||
// environment is located in envDir. Query describes the query itself.
|
||||
func RunQuery(t *TC, env *Env, envDir string, opts RunOpts, query Query) {
|
||||
queryURL := fmt.Sprintf("%s/%s", opts.Address(), query.Path)
|
||||
Debug(
|
||||
t,
|
||||
"Sending query",
|
||||
zap.String("method", query.Method),
|
||||
zap.String("path", query.Path),
|
||||
zap.String("url", queryURL),
|
||||
)
|
||||
|
||||
action := query.Action(env, &opts)
|
||||
if action != nil {
|
||||
action.VerifyBefore(t, envDir)
|
||||
}
|
||||
|
||||
req := query.CreateRequest(t, queryURL)
|
||||
req.Close = true
|
||||
conn, err := net.DialTimeout("tcp", req.URL.Host, 10*time.Second)
|
||||
require.NoError(t, err, "failed to connect to server")
|
||||
defer conn.Close()
|
||||
writeDone := make(chan error, 1)
|
||||
writer := &idleDeadlineWriter{Conn: conn, Timeout: 15 * time.Second}
|
||||
go func() { writeDone <- req.Write(writer) }()
|
||||
reader := bufio.NewReader(&idleDeadlineReader{Conn: conn, Timeout: 30 * time.Second})
|
||||
|
||||
resp, err := http.ReadResponse(reader, req)
|
||||
require.NoError(t, err, "failed to run query on server")
|
||||
defer resp.Body.Close()
|
||||
body := &countingBody{ReadCloser: resp.Body}
|
||||
resp.Body = body
|
||||
|
||||
query.CommonValidate(t, req, resp)
|
||||
|
||||
if action != nil {
|
||||
action.VerifyResponse(t, req, resp)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, resp.Body)
|
||||
require.NoError(t, err, "failed to read response body")
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
require.Equal(t, resp.ContentLength, body.Size, "Content-Length differs from response body size")
|
||||
}
|
||||
endDone := make(chan error, 1)
|
||||
go func() { endDone <- checkResponseEnd(reader, req.Body != nil) }()
|
||||
var endErr, writeErr error
|
||||
writeCompleted := false
|
||||
select {
|
||||
case endErr = <-endDone:
|
||||
// The server closed the connection before the request writer finished.
|
||||
case writeErr = <-writeDone:
|
||||
writeCompleted = true
|
||||
var netErr net.Error
|
||||
// http.Request.Write wraps body write errors without preserving Unwrap.
|
||||
// Inspect the original socket error captured by the writer instead.
|
||||
if errors.As(writer.lastError, &netErr) && netErr.Timeout() {
|
||||
require.NoError(t, writer.lastError, "request upload stalled")
|
||||
}
|
||||
// A server may finish reading a large request after sending its response.
|
||||
// Start the close timeout only after the request writer has finished.
|
||||
closeTimeout := 2 * time.Second
|
||||
if req.Body != nil {
|
||||
closeTimeout = 10 * time.Second
|
||||
}
|
||||
require.NoError(t, conn.SetReadDeadline(time.Now().Add(closeTimeout)))
|
||||
endErr = <-endDone
|
||||
}
|
||||
require.NoError(t, endErr, "response contains bytes beyond Content-Length or connection was not closed")
|
||||
// The server may reply without consuming the whole request body. In that
|
||||
// case the response and the file-system checks determine correctness.
|
||||
if req.Body == nil {
|
||||
if !writeCompleted {
|
||||
writeErr = <-writeDone
|
||||
}
|
||||
require.NoError(t, writeErr, "failed to send query to server")
|
||||
}
|
||||
if query.VerifyHeaders && body.Size > 0 {
|
||||
_, err = parseContentType(resp.Header.Get("Content-Type"))
|
||||
require.NoError(t, err, "expected valid content type")
|
||||
}
|
||||
if action != nil {
|
||||
action.VerifyAfter(t, envDir)
|
||||
action.ApplyEnv(t, env)
|
||||
}
|
||||
}
|
||||
|
||||
// idleDeadlineWriter bounds pauses in sending a request without limiting the
|
||||
// total time for a large request that continues to make progress.
|
||||
type idleDeadlineWriter struct {
|
||||
net.Conn
|
||||
Timeout time.Duration
|
||||
lastError error
|
||||
}
|
||||
|
||||
func (w *idleDeadlineWriter) Write(p []byte) (int, error) {
|
||||
if err := w.Conn.SetWriteDeadline(time.Now().Add(w.Timeout)); err != nil {
|
||||
w.lastError = err
|
||||
return 0, err
|
||||
}
|
||||
n, err := w.Conn.Write(p)
|
||||
if err != nil {
|
||||
w.lastError = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func checkResponseEnd(reader *bufio.Reader, requestHasBody bool) error {
|
||||
_, err := reader.ReadByte()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
// Closing a connection with an unread request body can produce a TCP reset
|
||||
// after a complete error response. Its error code differs by platform.
|
||||
var netErr *net.OpError
|
||||
if requestHasBody && errors.As(err, &netErr) && netErr.Op == "read" && !netErr.Timeout() {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("unexpected byte after response body")
|
||||
}
|
||||
|
||||
type countingBody struct {
|
||||
io.ReadCloser
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (b *countingBody) Read(p []byte) (int, error) {
|
||||
n, err := b.ReadCloser.Read(p)
|
||||
b.Size += int64(n)
|
||||
return n, err
|
||||
}
|
||||
Reference in New Issue
Block a user