libgo: Update to Go 1.1.1.
[official-gcc.git] / libgo / go / net / http / fs_test.go
blob2c3737653b39ba072b952e3a41e0a8caa231ca59
1 // Copyright 2010 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 package http_test
7 import (
8 "bytes"
9 "errors"
10 "fmt"
11 "io"
12 "io/ioutil"
13 "mime"
14 "mime/multipart"
15 "net"
16 . "net/http"
17 "net/http/httptest"
18 "net/url"
19 "os"
20 "os/exec"
21 "path"
22 "path/filepath"
23 "regexp"
24 "runtime"
25 "strings"
26 "testing"
27 "time"
30 const (
31 testFile = "testdata/file"
32 testFileLen = 11
35 type wantRange struct {
36 start, end int64 // range [start,end)
39 var ServeFileRangeTests = []struct {
40 r string
41 code int
42 ranges []wantRange
44 {r: "", code: StatusOK},
45 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
46 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
47 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
48 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
49 {r: "bytes=20-", code: StatusRequestedRangeNotSatisfiable},
50 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
51 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
52 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
53 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
56 func TestServeFile(t *testing.T) {
57 defer afterTest(t)
58 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
59 ServeFile(w, r, "testdata/file")
60 }))
61 defer ts.Close()
63 var err error
65 file, err := ioutil.ReadFile(testFile)
66 if err != nil {
67 t.Fatal("reading file:", err)
70 // set up the Request (re-used for all tests)
71 var req Request
72 req.Header = make(Header)
73 if req.URL, err = url.Parse(ts.URL); err != nil {
74 t.Fatal("ParseURL:", err)
76 req.Method = "GET"
78 // straight GET
79 _, body := getBody(t, "straight get", req)
80 if !bytes.Equal(body, file) {
81 t.Fatalf("body mismatch: got %q, want %q", body, file)
84 // Range tests
85 Cases:
86 for _, rt := range ServeFileRangeTests {
87 if rt.r != "" {
88 req.Header.Set("Range", rt.r)
90 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req)
91 if resp.StatusCode != rt.code {
92 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
94 if rt.code == StatusRequestedRangeNotSatisfiable {
95 continue
97 wantContentRange := ""
98 if len(rt.ranges) == 1 {
99 rng := rt.ranges[0]
100 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
102 cr := resp.Header.Get("Content-Range")
103 if cr != wantContentRange {
104 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
106 ct := resp.Header.Get("Content-Type")
107 if len(rt.ranges) == 1 {
108 rng := rt.ranges[0]
109 wantBody := file[rng.start:rng.end]
110 if !bytes.Equal(body, wantBody) {
111 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
113 if strings.HasPrefix(ct, "multipart/byteranges") {
114 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
117 if len(rt.ranges) > 1 {
118 typ, params, err := mime.ParseMediaType(ct)
119 if err != nil {
120 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
121 continue
123 if typ != "multipart/byteranges" {
124 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
125 continue
127 if params["boundary"] == "" {
128 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
129 continue
131 if g, w := resp.ContentLength, int64(len(body)); g != w {
132 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
133 continue
135 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
136 for ri, rng := range rt.ranges {
137 part, err := mr.NextPart()
138 if err != nil {
139 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
140 continue Cases
142 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
143 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
144 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
146 body, err := ioutil.ReadAll(part)
147 if err != nil {
148 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
149 continue Cases
151 wantBody := file[rng.start:rng.end]
152 if !bytes.Equal(body, wantBody) {
153 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
156 _, err = mr.NextPart()
157 if err != io.EOF {
158 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
164 var fsRedirectTestData = []struct {
165 original, redirect string
167 {"/test/index.html", "/test/"},
168 {"/test/testdata", "/test/testdata/"},
169 {"/test/testdata/file/", "/test/testdata/file"},
172 func TestFSRedirect(t *testing.T) {
173 defer afterTest(t)
174 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
175 defer ts.Close()
177 for _, data := range fsRedirectTestData {
178 res, err := Get(ts.URL + data.original)
179 if err != nil {
180 t.Fatal(err)
182 res.Body.Close()
183 if g, e := res.Request.URL.Path, data.redirect; g != e {
184 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
189 type testFileSystem struct {
190 open func(name string) (File, error)
193 func (fs *testFileSystem) Open(name string) (File, error) {
194 return fs.open(name)
197 func TestFileServerCleans(t *testing.T) {
198 defer afterTest(t)
199 ch := make(chan string, 1)
200 fs := FileServer(&testFileSystem{func(name string) (File, error) {
201 ch <- name
202 return nil, errors.New("file does not exist")
204 tests := []struct {
205 reqPath, openArg string
207 {"/foo.txt", "/foo.txt"},
208 {"//foo.txt", "/foo.txt"},
209 {"/../foo.txt", "/foo.txt"},
211 req, _ := NewRequest("GET", "http://example.com", nil)
212 for n, test := range tests {
213 rec := httptest.NewRecorder()
214 req.URL.Path = test.reqPath
215 fs.ServeHTTP(rec, req)
216 if got := <-ch; got != test.openArg {
217 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
222 func mustRemoveAll(dir string) {
223 err := os.RemoveAll(dir)
224 if err != nil {
225 panic(err)
229 func TestFileServerImplicitLeadingSlash(t *testing.T) {
230 defer afterTest(t)
231 tempDir, err := ioutil.TempDir("", "")
232 if err != nil {
233 t.Fatalf("TempDir: %v", err)
235 defer mustRemoveAll(tempDir)
236 if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
237 t.Fatalf("WriteFile: %v", err)
239 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
240 defer ts.Close()
241 get := func(suffix string) string {
242 res, err := Get(ts.URL + suffix)
243 if err != nil {
244 t.Fatalf("Get %s: %v", suffix, err)
246 b, err := ioutil.ReadAll(res.Body)
247 if err != nil {
248 t.Fatalf("ReadAll %s: %v", suffix, err)
250 res.Body.Close()
251 return string(b)
253 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
254 t.Logf("expected a directory listing with foo.txt, got %q", s)
256 if s := get("/bar/foo.txt"); s != "Hello world" {
257 t.Logf("expected %q, got %q", "Hello world", s)
261 func TestDirJoin(t *testing.T) {
262 wfi, err := os.Stat("/etc/hosts")
263 if err != nil {
264 t.Skip("skipping test; no /etc/hosts file")
266 test := func(d Dir, name string) {
267 f, err := d.Open(name)
268 if err != nil {
269 t.Fatalf("open of %s: %v", name, err)
271 defer f.Close()
272 gfi, err := f.Stat()
273 if err != nil {
274 t.Fatalf("stat of %s: %v", name, err)
276 if !os.SameFile(gfi, wfi) {
277 t.Errorf("%s got different file", name)
280 test(Dir("/etc/"), "/hosts")
281 test(Dir("/etc/"), "hosts")
282 test(Dir("/etc/"), "../../../../hosts")
283 test(Dir("/etc"), "/hosts")
284 test(Dir("/etc"), "hosts")
285 test(Dir("/etc"), "../../../../hosts")
287 // Not really directories, but since we use this trick in
288 // ServeFile, test it:
289 test(Dir("/etc/hosts"), "")
290 test(Dir("/etc/hosts"), "/")
291 test(Dir("/etc/hosts"), "../")
294 func TestEmptyDirOpenCWD(t *testing.T) {
295 test := func(d Dir) {
296 name := "fs_test.go"
297 f, err := d.Open(name)
298 if err != nil {
299 t.Fatalf("open of %s: %v", name, err)
301 defer f.Close()
303 test(Dir(""))
304 test(Dir("."))
305 test(Dir("./"))
308 func TestServeFileContentType(t *testing.T) {
309 defer afterTest(t)
310 const ctype = "icecream/chocolate"
311 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
312 if r.FormValue("override") == "1" {
313 w.Header().Set("Content-Type", ctype)
315 ServeFile(w, r, "testdata/file")
317 defer ts.Close()
318 get := func(override, want string) {
319 resp, err := Get(ts.URL + "?override=" + override)
320 if err != nil {
321 t.Fatal(err)
323 if h := resp.Header.Get("Content-Type"); h != want {
324 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
326 resp.Body.Close()
328 get("0", "text/plain; charset=utf-8")
329 get("1", ctype)
332 func TestServeFileMimeType(t *testing.T) {
333 defer afterTest(t)
334 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
335 ServeFile(w, r, "testdata/style.css")
337 defer ts.Close()
338 resp, err := Get(ts.URL)
339 if err != nil {
340 t.Fatal(err)
342 resp.Body.Close()
343 want := "text/css; charset=utf-8"
344 if h := resp.Header.Get("Content-Type"); h != want {
345 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
349 func TestServeFileFromCWD(t *testing.T) {
350 defer afterTest(t)
351 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
352 ServeFile(w, r, "fs_test.go")
354 defer ts.Close()
355 r, err := Get(ts.URL)
356 if err != nil {
357 t.Fatal(err)
359 r.Body.Close()
360 if r.StatusCode != 200 {
361 t.Fatalf("expected 200 OK, got %s", r.Status)
365 func TestServeFileWithContentEncoding(t *testing.T) {
366 defer afterTest(t)
367 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
368 w.Header().Set("Content-Encoding", "foo")
369 ServeFile(w, r, "testdata/file")
371 defer ts.Close()
372 resp, err := Get(ts.URL)
373 if err != nil {
374 t.Fatal(err)
376 resp.Body.Close()
377 if g, e := resp.ContentLength, int64(-1); g != e {
378 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
382 func TestServeIndexHtml(t *testing.T) {
383 defer afterTest(t)
384 const want = "index.html says hello\n"
385 ts := httptest.NewServer(FileServer(Dir(".")))
386 defer ts.Close()
388 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
389 res, err := Get(ts.URL + path)
390 if err != nil {
391 t.Fatal(err)
393 b, err := ioutil.ReadAll(res.Body)
394 if err != nil {
395 t.Fatal("reading Body:", err)
397 if s := string(b); s != want {
398 t.Errorf("for path %q got %q, want %q", path, s, want)
400 res.Body.Close()
404 func TestFileServerZeroByte(t *testing.T) {
405 defer afterTest(t)
406 ts := httptest.NewServer(FileServer(Dir(".")))
407 defer ts.Close()
409 res, err := Get(ts.URL + "/..\x00")
410 if err != nil {
411 t.Fatal(err)
413 b, err := ioutil.ReadAll(res.Body)
414 if err != nil {
415 t.Fatal("reading Body:", err)
417 if res.StatusCode == 200 {
418 t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
422 type fakeFileInfo struct {
423 dir bool
424 basename string
425 modtime time.Time
426 ents []*fakeFileInfo
427 contents string
430 func (f *fakeFileInfo) Name() string { return f.basename }
431 func (f *fakeFileInfo) Sys() interface{} { return nil }
432 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
433 func (f *fakeFileInfo) IsDir() bool { return f.dir }
434 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
435 func (f *fakeFileInfo) Mode() os.FileMode {
436 if f.dir {
437 return 0755 | os.ModeDir
439 return 0644
442 type fakeFile struct {
443 io.ReadSeeker
444 fi *fakeFileInfo
445 path string // as opened
448 func (f *fakeFile) Close() error { return nil }
449 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
450 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
451 if !f.fi.dir {
452 return nil, os.ErrInvalid
454 var fis []os.FileInfo
455 for _, fi := range f.fi.ents {
456 fis = append(fis, fi)
458 return fis, nil
461 type fakeFS map[string]*fakeFileInfo
463 func (fs fakeFS) Open(name string) (File, error) {
464 name = path.Clean(name)
465 f, ok := fs[name]
466 if !ok {
467 println("fake filesystem didn't find file", name)
468 return nil, os.ErrNotExist
470 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
473 func TestDirectoryIfNotModified(t *testing.T) {
474 defer afterTest(t)
475 const indexContents = "I am a fake index.html file"
476 fileMod := time.Unix(1000000000, 0).UTC()
477 fileModStr := fileMod.Format(TimeFormat)
478 dirMod := time.Unix(123, 0).UTC()
479 indexFile := &fakeFileInfo{
480 basename: "index.html",
481 modtime: fileMod,
482 contents: indexContents,
484 fs := fakeFS{
485 "/": &fakeFileInfo{
486 dir: true,
487 modtime: dirMod,
488 ents: []*fakeFileInfo{indexFile},
490 "/index.html": indexFile,
493 ts := httptest.NewServer(FileServer(fs))
494 defer ts.Close()
496 res, err := Get(ts.URL)
497 if err != nil {
498 t.Fatal(err)
500 b, err := ioutil.ReadAll(res.Body)
501 if err != nil {
502 t.Fatal(err)
504 if string(b) != indexContents {
505 t.Fatalf("Got body %q; want %q", b, indexContents)
507 res.Body.Close()
509 lastMod := res.Header.Get("Last-Modified")
510 if lastMod != fileModStr {
511 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
514 req, _ := NewRequest("GET", ts.URL, nil)
515 req.Header.Set("If-Modified-Since", lastMod)
517 res, err = DefaultClient.Do(req)
518 if err != nil {
519 t.Fatal(err)
521 if res.StatusCode != 304 {
522 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
524 res.Body.Close()
526 // Advance the index.html file's modtime, but not the directory's.
527 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
529 res, err = DefaultClient.Do(req)
530 if err != nil {
531 t.Fatal(err)
533 if res.StatusCode != 200 {
534 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
536 res.Body.Close()
539 func mustStat(t *testing.T, fileName string) os.FileInfo {
540 fi, err := os.Stat(fileName)
541 if err != nil {
542 t.Fatal(err)
544 return fi
547 func TestServeContent(t *testing.T) {
548 defer afterTest(t)
549 type serveParam struct {
550 name string
551 modtime time.Time
552 content io.ReadSeeker
553 contentType string
554 etag string
556 servec := make(chan serveParam, 1)
557 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
558 p := <-servec
559 if p.etag != "" {
560 w.Header().Set("ETag", p.etag)
562 if p.contentType != "" {
563 w.Header().Set("Content-Type", p.contentType)
565 ServeContent(w, r, p.name, p.modtime, p.content)
567 defer ts.Close()
569 type testCase struct {
570 file string
571 modtime time.Time
572 serveETag string // optional
573 serveContentType string // optional
574 reqHeader map[string]string
575 wantLastMod string
576 wantContentType string
577 wantStatus int
579 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
580 tests := map[string]testCase{
581 "no_last_modified": {
582 file: "testdata/style.css",
583 wantContentType: "text/css; charset=utf-8",
584 wantStatus: 200,
586 "with_last_modified": {
587 file: "testdata/index.html",
588 wantContentType: "text/html; charset=utf-8",
589 modtime: htmlModTime,
590 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
591 wantStatus: 200,
593 "not_modified_modtime": {
594 file: "testdata/style.css",
595 modtime: htmlModTime,
596 reqHeader: map[string]string{
597 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
599 wantStatus: 304,
601 "not_modified_modtime_with_contenttype": {
602 file: "testdata/style.css",
603 serveContentType: "text/css", // explicit content type
604 modtime: htmlModTime,
605 reqHeader: map[string]string{
606 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
608 wantStatus: 304,
610 "not_modified_etag": {
611 file: "testdata/style.css",
612 serveETag: `"foo"`,
613 reqHeader: map[string]string{
614 "If-None-Match": `"foo"`,
616 wantStatus: 304,
618 "range_good": {
619 file: "testdata/style.css",
620 serveETag: `"A"`,
621 reqHeader: map[string]string{
622 "Range": "bytes=0-4",
624 wantStatus: StatusPartialContent,
625 wantContentType: "text/css; charset=utf-8",
627 // An If-Range resource for entity "A", but entity "B" is now current.
628 // The Range request should be ignored.
629 "range_no_match": {
630 file: "testdata/style.css",
631 serveETag: `"A"`,
632 reqHeader: map[string]string{
633 "Range": "bytes=0-4",
634 "If-Range": `"B"`,
636 wantStatus: 200,
637 wantContentType: "text/css; charset=utf-8",
640 for testName, tt := range tests {
641 f, err := os.Open(tt.file)
642 if err != nil {
643 t.Fatalf("test %q: %v", testName, err)
645 defer f.Close()
647 servec <- serveParam{
648 name: filepath.Base(tt.file),
649 content: f,
650 modtime: tt.modtime,
651 etag: tt.serveETag,
652 contentType: tt.serveContentType,
654 req, err := NewRequest("GET", ts.URL, nil)
655 if err != nil {
656 t.Fatal(err)
658 for k, v := range tt.reqHeader {
659 req.Header.Set(k, v)
661 res, err := DefaultClient.Do(req)
662 if err != nil {
663 t.Fatal(err)
665 io.Copy(ioutil.Discard, res.Body)
666 res.Body.Close()
667 if res.StatusCode != tt.wantStatus {
668 t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
670 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
671 t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
673 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
674 t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
679 // verifies that sendfile is being used on Linux
680 func TestLinuxSendfile(t *testing.T) {
681 defer afterTest(t)
682 if runtime.GOOS != "linux" {
683 t.Skip("skipping; linux-only test")
685 if _, err := exec.LookPath("strace"); err != nil {
686 t.Skip("skipping; strace not found in path")
689 ln, err := net.Listen("tcp", "127.0.0.1:0")
690 if err != nil {
691 t.Fatal(err)
693 lnf, err := ln.(*net.TCPListener).File()
694 if err != nil {
695 t.Fatal(err)
697 defer ln.Close()
699 var buf bytes.Buffer
700 child := exec.Command("strace", "-f", "-q", "-e", "trace=sendfile,sendfile64", os.Args[0], "-test.run=TestLinuxSendfileChild")
701 child.ExtraFiles = append(child.ExtraFiles, lnf)
702 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
703 child.Stdout = &buf
704 child.Stderr = &buf
705 if err := child.Start(); err != nil {
706 t.Skipf("skipping; failed to start straced child: %v", err)
709 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
710 if err != nil {
711 t.Fatalf("http client error: %v", err)
713 _, err = io.Copy(ioutil.Discard, res.Body)
714 if err != nil {
715 t.Fatalf("client body read error: %v", err)
717 res.Body.Close()
719 // Force child to exit cleanly.
720 Get(fmt.Sprintf("http://%s/quit", ln.Addr()))
721 child.Wait()
723 rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`)
724 rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`)
725 out := buf.String()
726 if !rx.MatchString(out) && !rxResume.MatchString(out) {
727 t.Errorf("no sendfile system call found in:\n%s", out)
731 func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
732 r, err := DefaultClient.Do(&req)
733 if err != nil {
734 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
736 b, err := ioutil.ReadAll(r.Body)
737 if err != nil {
738 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
740 return r, b
743 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
744 // for TestLinuxSendfile.
745 func TestLinuxSendfileChild(*testing.T) {
746 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
747 return
749 defer os.Exit(0)
750 fd3 := os.NewFile(3, "ephemeral-port-listener")
751 ln, err := net.FileListener(fd3)
752 if err != nil {
753 panic(err)
755 mux := NewServeMux()
756 mux.Handle("/", FileServer(Dir("testdata")))
757 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
758 os.Exit(0)
760 s := &Server{Handler: mux}
761 err = s.Serve(ln)
762 if err != nil {
763 panic(err)