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 }