Repository navigation
Expand file tree
/
Copy pathserve_readiness_test.go
More file actions
165 lines (146 loc) · 6.52 KB
/
Copy pathserve_readiness_test.go
File metadata and controls
165 lines (146 loc) · 6.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package main
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
runtimev0 "github.com/codefly-dev/core/generated/go/codefly/services/runtime/v0"
"github.com/stretchr/testify/require"
)
// closedAddress returns a loopback address nothing listens on.
func closedAddress(t *testing.T) string {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
address := listener.Addr().String()
require.NoError(t, listener.Close())
return address
}
// TestStartedMeansTheEndpointAnswers pins the contract a dependent is started
// on: STARTED is not returned while the endpoint refuses connections, and is
// returned once it answers, whatever the status.
func TestStartedMeansTheEndpointAnswers(t *testing.T) {
runtime, env := startTestRuntime(t)
address := closedAddress(t)
runtime.serveAddress = "http://" + address
served := make(chan struct{})
go func() {
time.Sleep(700 * time.Millisecond)
listener, err := net.Listen("tcp", address)
if err != nil {
t.Errorf("cannot bind the late listener: %v", err)
return
}
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}), ReadHeaderTimeout: time.Second}
t.Cleanup(func() { _ = server.Close() })
close(served)
_ = server.Serve(listener)
}()
began := time.Now()
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_STARTED, resp.GetStatus().GetState(), resp.GetStatus().GetMessage())
select {
case <-served:
default:
t.Fatal("STARTED was returned before anything listened on the endpoint")
}
require.GreaterOrEqual(t, time.Since(began), 600*time.Millisecond)
require.Len(t, uvicornProcs(env), 1)
require.False(t, uvicornProcs(env)[0].isStopped())
}
// TestStartAcceptsAnyHTTPAnswer: a redirect or an error status is still an
// endpoint serving HTTP.
func TestStartAcceptsAnyHTTPAnswer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/docs", http.StatusTemporaryRedirect)
}))
t.Cleanup(server.Close)
runtime, _ := startTestRuntime(t)
// The carrier may hold host:port without a scheme.
runtime.serveAddress = strings.TrimPrefix(server.URL, "http://")
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_STARTED, resp.GetStatus().GetState(), resp.GetStatus().GetMessage())
}
// TestStartFailsWhenTheAppExitsBeforeServing: a process that dies before its
// endpoint answers is a failed Start carrying what it printed, not a STARTED
// the supervisor revokes later.
func TestStartFailsWhenTheAppExitsBeforeServing(t *testing.T) {
runtime, env := startTestRuntime(t)
runtime.serveAddress = "http://" + closedAddress(t)
go func() {
for len(uvicornProcs(env)) == 0 || !uvicornProcs(env)[0].isStarted() {
time.Sleep(10 * time.Millisecond)
}
proc := uvicornProcs(env)[0]
_, _ = proc.output.Write([]byte("ModuleNotFoundError: No module named 'src.main'\n"))
proc.die(errors.New("exit status 1"))
}()
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_ERROR, resp.GetStatus().GetState())
require.Contains(t, resp.GetStatus().GetMessage(), "exited before its HTTP endpoint")
require.Contains(t, resp.GetStatus().GetMessage(), "ModuleNotFoundError")
require.Nil(t, currentRunner(runtime), "a process that never served must not stay the service's runner")
}
// TestStartFailsWhenTheEndpointNeverAnswers: a live process whose endpoint
// never answers is stopped and reported, bounded by the serve timeout.
func TestStartFailsWhenTheEndpointNeverAnswers(t *testing.T) {
runtime, env := startTestRuntime(t)
runtime.serveAddress = "http://" + closedAddress(t)
runtime.serveTimeout = 600 * time.Millisecond
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_ERROR, resp.GetStatus().GetState())
require.Contains(t, resp.GetStatus().GetMessage(), "did not answer within")
require.Len(t, uvicornProcs(env), 1)
require.True(t, uvicornProcs(env)[0].isStopped(), "the process that never served must be stopped")
require.Nil(t, currentRunner(runtime))
// The next Start is a fresh attempt, not an answer from the failed one.
runtime.serveAddress = ""
resp, err = runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_STARTED, resp.GetStatus().GetState(), resp.GetStatus().GetMessage())
require.Len(t, uvicornProcs(env), 2)
}
// TestStartRefusesAConnectFaceNobodyServes: with connect-endpoint on, an app
// whose HTTP endpoint answers but whose main never starts the Connect face (a
// src/main.py scaffolded before the generated main started it) is a failed
// Start naming the setting and the fix, not a STARTED behind a refused port.
func TestStartRefusesAConnectFaceNobodyServes(t *testing.T) {
server := httptest.NewServer(http.NotFoundHandler())
t.Cleanup(server.Close)
runtime, env := startTestRuntime(t)
runtime.serveAddress = server.URL
runtime.connectAddress = closedAddress(t)
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_ERROR, resp.GetStatus().GetState())
require.Contains(t, resp.GetStatus().GetMessage(), "connect-endpoint is on but nothing serves the Connect face")
require.Contains(t, resp.GetStatus().GetMessage(), "connect_gen.start")
require.True(t, env.started()[0].isStopped(), "the app that does not serve its face must be stopped")
require.Nil(t, currentRunner(runtime))
}
// TestStartWaitsForTheConnectFace: a face that answers, whatever the status
// (it answers a GET with 405), is a STARTED service.
func TestStartWaitsForTheConnectFace(t *testing.T) {
server := httptest.NewServer(http.NotFoundHandler())
t.Cleanup(server.Close)
face := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}))
t.Cleanup(face.Close)
runtime, _ := startTestRuntime(t)
runtime.serveAddress = server.URL
runtime.connectAddress = strings.TrimPrefix(face.URL, "http://")
resp, err := runtime.Start(context.Background(), &runtimev0.StartRequest{})
require.NoError(t, err)
require.Equal(t, runtimev0.StartStatus_STARTED, resp.GetStatus().GetState(), resp.GetStatus().GetMessage())
}