github.com/varialus/godfly@v0.0.0-20130904042352-1934f9f095ab/src/pkg/net/http/fs_test.go (about)

     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.
     4  
     5  package http_test
     6  
     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"
    28  )
    29  
    30  const (
    31  	testFile    = "testdata/file"
    32  	testFileLen = 11
    33  )
    34  
    35  type wantRange struct {
    36  	start, end int64 // range [start,end)
    37  }
    38  
    39  var ServeFileRangeTests = []struct {
    40  	r      string
    41  	code   int
    42  	ranges []wantRange
    43  }{
    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
    54  }
    55  
    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()
    62  
    63  	var err error
    64  
    65  	file, err := ioutil.ReadFile(testFile)
    66  	if err != nil {
    67  		t.Fatal("reading file:", err)
    68  	}
    69  
    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)
    75  	}
    76  	req.Method = "GET"
    77  
    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)
    82  	}
    83  
    84  	// Range tests
    85  Cases:
    86  	for _, rt := range ServeFileRangeTests {
    87  		if rt.r != "" {
    88  			req.Header.Set("Range", rt.r)
    89  		}
    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)
    93  		}
    94  		if rt.code == StatusRequestedRangeNotSatisfiable {
    95  			continue
    96  		}
    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)
   101  		}
   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)
   105  		}
   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)
   112  			}
   113  			if strings.HasPrefix(ct, "multipart/byteranges") {
   114  				t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
   115  			}
   116  		}
   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
   122  			}
   123  			if typ != "multipart/byteranges" {
   124  				t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
   125  				continue
   126  			}
   127  			if params["boundary"] == "" {
   128  				t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
   129  				continue
   130  			}
   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
   134  			}
   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
   141  				}
   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)
   145  				}
   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
   150  				}
   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)
   154  				}
   155  			}
   156  			_, err = mr.NextPart()
   157  			if err != io.EOF {
   158  				t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
   159  			}
   160  		}
   161  	}
   162  }
   163  
   164  var fsRedirectTestData = []struct {
   165  	original, redirect string
   166  }{
   167  	{"/test/index.html", "/test/"},
   168  	{"/test/testdata", "/test/testdata/"},
   169  	{"/test/testdata/file/", "/test/testdata/file"},
   170  }
   171  
   172  func TestFSRedirect(t *testing.T) {
   173  	defer afterTest(t)
   174  	ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
   175  	defer ts.Close()
   176  
   177  	for _, data := range fsRedirectTestData {
   178  		res, err := Get(ts.URL + data.original)
   179  		if err != nil {
   180  			t.Fatal(err)
   181  		}
   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)
   185  		}
   186  	}
   187  }
   188  
   189  type testFileSystem struct {
   190  	open func(name string) (File, error)
   191  }
   192  
   193  func (fs *testFileSystem) Open(name string) (File, error) {
   194  	return fs.open(name)
   195  }
   196  
   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")
   203  	}})
   204  	tests := []struct {
   205  		reqPath, openArg string
   206  	}{
   207  		{"/foo.txt", "/foo.txt"},
   208  		{"//foo.txt", "/foo.txt"},
   209  		{"/../foo.txt", "/foo.txt"},
   210  	}
   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)
   218  		}
   219  	}
   220  }
   221  
   222  func mustRemoveAll(dir string) {
   223  	err := os.RemoveAll(dir)
   224  	if err != nil {
   225  		panic(err)
   226  	}
   227  }
   228  
   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)
   234  	}
   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)
   238  	}
   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)
   245  		}
   246  		b, err := ioutil.ReadAll(res.Body)
   247  		if err != nil {
   248  			t.Fatalf("ReadAll %s: %v", suffix, err)
   249  		}
   250  		res.Body.Close()
   251  		return string(b)
   252  	}
   253  	if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
   254  		t.Logf("expected a directory listing with foo.txt, got %q", s)
   255  	}
   256  	if s := get("/bar/foo.txt"); s != "Hello world" {
   257  		t.Logf("expected %q, got %q", "Hello world", s)
   258  	}
   259  }
   260  
   261  func TestDirJoin(t *testing.T) {
   262  	if runtime.GOOS == "windows" {
   263  		t.Skip("skipping test on windows")
   264  	}
   265  	wfi, err := os.Stat("/etc/hosts")
   266  	if err != nil {
   267  		t.Skip("skipping test; no /etc/hosts file")
   268  	}
   269  	test := func(d Dir, name string) {
   270  		f, err := d.Open(name)
   271  		if err != nil {
   272  			t.Fatalf("open of %s: %v", name, err)
   273  		}
   274  		defer f.Close()
   275  		gfi, err := f.Stat()
   276  		if err != nil {
   277  			t.Fatalf("stat of %s: %v", name, err)
   278  		}
   279  		if !os.SameFile(gfi, wfi) {
   280  			t.Errorf("%s got different file", name)
   281  		}
   282  	}
   283  	test(Dir("/etc/"), "/hosts")
   284  	test(Dir("/etc/"), "hosts")
   285  	test(Dir("/etc/"), "../../../../hosts")
   286  	test(Dir("/etc"), "/hosts")
   287  	test(Dir("/etc"), "hosts")
   288  	test(Dir("/etc"), "../../../../hosts")
   289  
   290  	// Not really directories, but since we use this trick in
   291  	// ServeFile, test it:
   292  	test(Dir("/etc/hosts"), "")
   293  	test(Dir("/etc/hosts"), "/")
   294  	test(Dir("/etc/hosts"), "../")
   295  }
   296  
   297  func TestEmptyDirOpenCWD(t *testing.T) {
   298  	test := func(d Dir) {
   299  		name := "fs_test.go"
   300  		f, err := d.Open(name)
   301  		if err != nil {
   302  			t.Fatalf("open of %s: %v", name, err)
   303  		}
   304  		defer f.Close()
   305  	}
   306  	test(Dir(""))
   307  	test(Dir("."))
   308  	test(Dir("./"))
   309  }
   310  
   311  func TestServeFileContentType(t *testing.T) {
   312  	defer afterTest(t)
   313  	const ctype = "icecream/chocolate"
   314  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   315  		if r.FormValue("override") == "1" {
   316  			w.Header().Set("Content-Type", ctype)
   317  		}
   318  		ServeFile(w, r, "testdata/file")
   319  	}))
   320  	defer ts.Close()
   321  	get := func(override, want string) {
   322  		resp, err := Get(ts.URL + "?override=" + override)
   323  		if err != nil {
   324  			t.Fatal(err)
   325  		}
   326  		if h := resp.Header.Get("Content-Type"); h != want {
   327  			t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   328  		}
   329  		resp.Body.Close()
   330  	}
   331  	get("0", "text/plain; charset=utf-8")
   332  	get("1", ctype)
   333  }
   334  
   335  func TestServeFileMimeType(t *testing.T) {
   336  	defer afterTest(t)
   337  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   338  		ServeFile(w, r, "testdata/style.css")
   339  	}))
   340  	defer ts.Close()
   341  	resp, err := Get(ts.URL)
   342  	if err != nil {
   343  		t.Fatal(err)
   344  	}
   345  	resp.Body.Close()
   346  	want := "text/css; charset=utf-8"
   347  	if h := resp.Header.Get("Content-Type"); h != want {
   348  		t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   349  	}
   350  }
   351  
   352  func TestServeFileFromCWD(t *testing.T) {
   353  	defer afterTest(t)
   354  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   355  		ServeFile(w, r, "fs_test.go")
   356  	}))
   357  	defer ts.Close()
   358  	r, err := Get(ts.URL)
   359  	if err != nil {
   360  		t.Fatal(err)
   361  	}
   362  	r.Body.Close()
   363  	if r.StatusCode != 200 {
   364  		t.Fatalf("expected 200 OK, got %s", r.Status)
   365  	}
   366  }
   367  
   368  func TestServeFileWithContentEncoding(t *testing.T) {
   369  	defer afterTest(t)
   370  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   371  		w.Header().Set("Content-Encoding", "foo")
   372  		ServeFile(w, r, "testdata/file")
   373  	}))
   374  	defer ts.Close()
   375  	resp, err := Get(ts.URL)
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	resp.Body.Close()
   380  	if g, e := resp.ContentLength, int64(-1); g != e {
   381  		t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
   382  	}
   383  }
   384  
   385  func TestServeIndexHtml(t *testing.T) {
   386  	defer afterTest(t)
   387  	const want = "index.html says hello\n"
   388  	ts := httptest.NewServer(FileServer(Dir(".")))
   389  	defer ts.Close()
   390  
   391  	for _, path := range []string{"/testdata/", "/testdata/index.html"} {
   392  		res, err := Get(ts.URL + path)
   393  		if err != nil {
   394  			t.Fatal(err)
   395  		}
   396  		b, err := ioutil.ReadAll(res.Body)
   397  		if err != nil {
   398  			t.Fatal("reading Body:", err)
   399  		}
   400  		if s := string(b); s != want {
   401  			t.Errorf("for path %q got %q, want %q", path, s, want)
   402  		}
   403  		res.Body.Close()
   404  	}
   405  }
   406  
   407  func TestFileServerZeroByte(t *testing.T) {
   408  	defer afterTest(t)
   409  	ts := httptest.NewServer(FileServer(Dir(".")))
   410  	defer ts.Close()
   411  
   412  	res, err := Get(ts.URL + "/..\x00")
   413  	if err != nil {
   414  		t.Fatal(err)
   415  	}
   416  	b, err := ioutil.ReadAll(res.Body)
   417  	if err != nil {
   418  		t.Fatal("reading Body:", err)
   419  	}
   420  	if res.StatusCode == 200 {
   421  		t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
   422  	}
   423  }
   424  
   425  type fakeFileInfo struct {
   426  	dir      bool
   427  	basename string
   428  	modtime  time.Time
   429  	ents     []*fakeFileInfo
   430  	contents string
   431  }
   432  
   433  func (f *fakeFileInfo) Name() string       { return f.basename }
   434  func (f *fakeFileInfo) Sys() interface{}   { return nil }
   435  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   436  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   437  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   438  func (f *fakeFileInfo) Mode() os.FileMode {
   439  	if f.dir {
   440  		return 0755 | os.ModeDir
   441  	}
   442  	return 0644
   443  }
   444  
   445  type fakeFile struct {
   446  	io.ReadSeeker
   447  	fi   *fakeFileInfo
   448  	path string // as opened
   449  }
   450  
   451  func (f *fakeFile) Close() error               { return nil }
   452  func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
   453  func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
   454  	if !f.fi.dir {
   455  		return nil, os.ErrInvalid
   456  	}
   457  	var fis []os.FileInfo
   458  	for _, fi := range f.fi.ents {
   459  		fis = append(fis, fi)
   460  	}
   461  	return fis, nil
   462  }
   463  
   464  type fakeFS map[string]*fakeFileInfo
   465  
   466  func (fs fakeFS) Open(name string) (File, error) {
   467  	name = path.Clean(name)
   468  	f, ok := fs[name]
   469  	if !ok {
   470  		println("fake filesystem didn't find file", name)
   471  		return nil, os.ErrNotExist
   472  	}
   473  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   474  }
   475  
   476  func TestDirectoryIfNotModified(t *testing.T) {
   477  	defer afterTest(t)
   478  	const indexContents = "I am a fake index.html file"
   479  	fileMod := time.Unix(1000000000, 0).UTC()
   480  	fileModStr := fileMod.Format(TimeFormat)
   481  	dirMod := time.Unix(123, 0).UTC()
   482  	indexFile := &fakeFileInfo{
   483  		basename: "index.html",
   484  		modtime:  fileMod,
   485  		contents: indexContents,
   486  	}
   487  	fs := fakeFS{
   488  		"/": &fakeFileInfo{
   489  			dir:     true,
   490  			modtime: dirMod,
   491  			ents:    []*fakeFileInfo{indexFile},
   492  		},
   493  		"/index.html": indexFile,
   494  	}
   495  
   496  	ts := httptest.NewServer(FileServer(fs))
   497  	defer ts.Close()
   498  
   499  	res, err := Get(ts.URL)
   500  	if err != nil {
   501  		t.Fatal(err)
   502  	}
   503  	b, err := ioutil.ReadAll(res.Body)
   504  	if err != nil {
   505  		t.Fatal(err)
   506  	}
   507  	if string(b) != indexContents {
   508  		t.Fatalf("Got body %q; want %q", b, indexContents)
   509  	}
   510  	res.Body.Close()
   511  
   512  	lastMod := res.Header.Get("Last-Modified")
   513  	if lastMod != fileModStr {
   514  		t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
   515  	}
   516  
   517  	req, _ := NewRequest("GET", ts.URL, nil)
   518  	req.Header.Set("If-Modified-Since", lastMod)
   519  
   520  	res, err = DefaultClient.Do(req)
   521  	if err != nil {
   522  		t.Fatal(err)
   523  	}
   524  	if res.StatusCode != 304 {
   525  		t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
   526  	}
   527  	res.Body.Close()
   528  
   529  	// Advance the index.html file's modtime, but not the directory's.
   530  	indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
   531  
   532  	res, err = DefaultClient.Do(req)
   533  	if err != nil {
   534  		t.Fatal(err)
   535  	}
   536  	if res.StatusCode != 200 {
   537  		t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
   538  	}
   539  	res.Body.Close()
   540  }
   541  
   542  func mustStat(t *testing.T, fileName string) os.FileInfo {
   543  	fi, err := os.Stat(fileName)
   544  	if err != nil {
   545  		t.Fatal(err)
   546  	}
   547  	return fi
   548  }
   549  
   550  func TestServeContent(t *testing.T) {
   551  	defer afterTest(t)
   552  	type serveParam struct {
   553  		name        string
   554  		modtime     time.Time
   555  		content     io.ReadSeeker
   556  		contentType string
   557  		etag        string
   558  	}
   559  	servec := make(chan serveParam, 1)
   560  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   561  		p := <-servec
   562  		if p.etag != "" {
   563  			w.Header().Set("ETag", p.etag)
   564  		}
   565  		if p.contentType != "" {
   566  			w.Header().Set("Content-Type", p.contentType)
   567  		}
   568  		ServeContent(w, r, p.name, p.modtime, p.content)
   569  	}))
   570  	defer ts.Close()
   571  
   572  	type testCase struct {
   573  		// One of file or content must be set:
   574  		file    string
   575  		content io.ReadSeeker
   576  
   577  		modtime          time.Time
   578  		serveETag        string // optional
   579  		serveContentType string // optional
   580  		reqHeader        map[string]string
   581  		wantLastMod      string
   582  		wantContentType  string
   583  		wantStatus       int
   584  	}
   585  	htmlModTime := mustStat(t, "testdata/index.html").ModTime()
   586  	tests := map[string]testCase{
   587  		"no_last_modified": {
   588  			file:            "testdata/style.css",
   589  			wantContentType: "text/css; charset=utf-8",
   590  			wantStatus:      200,
   591  		},
   592  		"with_last_modified": {
   593  			file:            "testdata/index.html",
   594  			wantContentType: "text/html; charset=utf-8",
   595  			modtime:         htmlModTime,
   596  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   597  			wantStatus:      200,
   598  		},
   599  		"not_modified_modtime": {
   600  			file:    "testdata/style.css",
   601  			modtime: htmlModTime,
   602  			reqHeader: map[string]string{
   603  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   604  			},
   605  			wantStatus: 304,
   606  		},
   607  		"not_modified_modtime_with_contenttype": {
   608  			file:             "testdata/style.css",
   609  			serveContentType: "text/css", // explicit content type
   610  			modtime:          htmlModTime,
   611  			reqHeader: map[string]string{
   612  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   613  			},
   614  			wantStatus: 304,
   615  		},
   616  		"not_modified_etag": {
   617  			file:      "testdata/style.css",
   618  			serveETag: `"foo"`,
   619  			reqHeader: map[string]string{
   620  				"If-None-Match": `"foo"`,
   621  			},
   622  			wantStatus: 304,
   623  		},
   624  		"not_modified_etag_no_seek": {
   625  			content:   panicOnSeek{nil}, // should never be called
   626  			serveETag: `"foo"`,
   627  			reqHeader: map[string]string{
   628  				"If-None-Match": `"foo"`,
   629  			},
   630  			wantStatus: 304,
   631  		},
   632  		"range_good": {
   633  			file:      "testdata/style.css",
   634  			serveETag: `"A"`,
   635  			reqHeader: map[string]string{
   636  				"Range": "bytes=0-4",
   637  			},
   638  			wantStatus:      StatusPartialContent,
   639  			wantContentType: "text/css; charset=utf-8",
   640  		},
   641  		// An If-Range resource for entity "A", but entity "B" is now current.
   642  		// The Range request should be ignored.
   643  		"range_no_match": {
   644  			file:      "testdata/style.css",
   645  			serveETag: `"A"`,
   646  			reqHeader: map[string]string{
   647  				"Range":    "bytes=0-4",
   648  				"If-Range": `"B"`,
   649  			},
   650  			wantStatus:      200,
   651  			wantContentType: "text/css; charset=utf-8",
   652  		},
   653  	}
   654  	for testName, tt := range tests {
   655  		var content io.ReadSeeker
   656  		if tt.file != "" {
   657  			f, err := os.Open(tt.file)
   658  			if err != nil {
   659  				t.Fatalf("test %q: %v", testName, err)
   660  			}
   661  			defer f.Close()
   662  			content = f
   663  		} else {
   664  			content = tt.content
   665  		}
   666  
   667  		servec <- serveParam{
   668  			name:        filepath.Base(tt.file),
   669  			content:     content,
   670  			modtime:     tt.modtime,
   671  			etag:        tt.serveETag,
   672  			contentType: tt.serveContentType,
   673  		}
   674  		req, err := NewRequest("GET", ts.URL, nil)
   675  		if err != nil {
   676  			t.Fatal(err)
   677  		}
   678  		for k, v := range tt.reqHeader {
   679  			req.Header.Set(k, v)
   680  		}
   681  		res, err := DefaultClient.Do(req)
   682  		if err != nil {
   683  			t.Fatal(err)
   684  		}
   685  		io.Copy(ioutil.Discard, res.Body)
   686  		res.Body.Close()
   687  		if res.StatusCode != tt.wantStatus {
   688  			t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
   689  		}
   690  		if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
   691  			t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
   692  		}
   693  		if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
   694  			t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
   695  		}
   696  	}
   697  }
   698  
   699  // verifies that sendfile is being used on Linux
   700  func TestLinuxSendfile(t *testing.T) {
   701  	defer afterTest(t)
   702  	if runtime.GOOS != "linux" {
   703  		t.Skip("skipping; linux-only test")
   704  	}
   705  	if _, err := exec.LookPath("strace"); err != nil {
   706  		t.Skip("skipping; strace not found in path")
   707  	}
   708  
   709  	ln, err := net.Listen("tcp", "127.0.0.1:0")
   710  	if err != nil {
   711  		t.Fatal(err)
   712  	}
   713  	lnf, err := ln.(*net.TCPListener).File()
   714  	if err != nil {
   715  		t.Fatal(err)
   716  	}
   717  	defer ln.Close()
   718  
   719  	var buf bytes.Buffer
   720  	child := exec.Command("strace", "-f", "-q", "-e", "trace=sendfile,sendfile64", os.Args[0], "-test.run=TestLinuxSendfileChild")
   721  	child.ExtraFiles = append(child.ExtraFiles, lnf)
   722  	child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
   723  	child.Stdout = &buf
   724  	child.Stderr = &buf
   725  	if err := child.Start(); err != nil {
   726  		t.Skipf("skipping; failed to start straced child: %v", err)
   727  	}
   728  
   729  	res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
   730  	if err != nil {
   731  		t.Fatalf("http client error: %v", err)
   732  	}
   733  	_, err = io.Copy(ioutil.Discard, res.Body)
   734  	if err != nil {
   735  		t.Fatalf("client body read error: %v", err)
   736  	}
   737  	res.Body.Close()
   738  
   739  	// Force child to exit cleanly.
   740  	Get(fmt.Sprintf("http://%s/quit", ln.Addr()))
   741  	child.Wait()
   742  
   743  	rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`)
   744  	rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`)
   745  	out := buf.String()
   746  	if !rx.MatchString(out) && !rxResume.MatchString(out) {
   747  		t.Errorf("no sendfile system call found in:\n%s", out)
   748  	}
   749  }
   750  
   751  func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
   752  	r, err := DefaultClient.Do(&req)
   753  	if err != nil {
   754  		t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
   755  	}
   756  	b, err := ioutil.ReadAll(r.Body)
   757  	if err != nil {
   758  		t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
   759  	}
   760  	return r, b
   761  }
   762  
   763  // TestLinuxSendfileChild isn't a real test. It's used as a helper process
   764  // for TestLinuxSendfile.
   765  func TestLinuxSendfileChild(*testing.T) {
   766  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
   767  		return
   768  	}
   769  	defer os.Exit(0)
   770  	fd3 := os.NewFile(3, "ephemeral-port-listener")
   771  	ln, err := net.FileListener(fd3)
   772  	if err != nil {
   773  		panic(err)
   774  	}
   775  	mux := NewServeMux()
   776  	mux.Handle("/", FileServer(Dir("testdata")))
   777  	mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
   778  		os.Exit(0)
   779  	})
   780  	s := &Server{Handler: mux}
   781  	err = s.Serve(ln)
   782  	if err != nil {
   783  		panic(err)
   784  	}
   785  }
   786  
   787  type panicOnSeek struct{ io.ReadSeeker }