Skip to content

Commit a7b4d4c

Browse files
committed
playground: support multiple input files in txtar format
Updates golang/go#32040 Updates golang/go#31944 (Notably, you can now include a go.mod file) Change-Id: I56846e86d3d98fdf4cac388b5b284dbc187e3b36 Reviewed-on: https://go-review.googlesource.com/c/playground/+/177043 Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 1b5b098 commit a7b4d4c

11 files changed

+540
-47
lines changed

Diff for: Dockerfile

+5
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ RUN mkdir /gocache
4141
ENV GOCACHE /gocache
4242
ENV GO111MODULE on
4343

44+
COPY go.mod /go/src/playground/go.mod
45+
COPY go.sum /go/src/playground/go.sum
46+
WORKDIR /go/src/playground
47+
4448
# Pre-build some packages to speed final install later.
4549
RUN go install cloud.google.com/go/compute/metadata
4650
RUN go install cloud.google.com/go/datastore
4751
RUN go install github.com/bradfitz/gomemcache/memcache
4852
RUN go install golang.org/x/tools/godoc/static
4953
RUN go install golang.org/x/tools/imports
54+
RUN go install github.com/rogpeppe/go-internal/txtar
5055

5156
# Add and compile playground daemon
5257
COPY . /go/src/playground/

Diff for: fmt.go

+33-18
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,40 @@ type fmtResponse struct {
2020
}
2121

2222
func handleFmt(w http.ResponseWriter, r *http.Request) {
23-
var (
24-
in = []byte(r.FormValue("body"))
25-
out []byte
26-
err error
27-
)
28-
if r.FormValue("imports") != "" {
29-
out, err = imports.Process(progName, in, nil)
30-
} else {
31-
out, err = format.Source(in)
32-
}
33-
var resp fmtResponse
23+
w.Header().Set("Content-Type", "application/json")
24+
25+
fs, err := splitFiles([]byte(r.FormValue("body")))
3426
if err != nil {
35-
resp.Error = err.Error()
36-
// Prefix the error returned by format.Source.
37-
if !strings.HasPrefix(resp.Error, progName) {
38-
resp.Error = fmt.Sprintf("%v:%v", progName, resp.Error)
27+
json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()})
28+
return
29+
}
30+
31+
fixImports := r.FormValue("imports") != ""
32+
for _, f := range fs.files {
33+
if !strings.HasSuffix(f, ".go") {
34+
continue
3935
}
40-
} else {
41-
resp.Body = string(out)
36+
var out []byte
37+
var err error
38+
in := fs.m[f]
39+
if fixImports {
40+
// TODO: pass options to imports.Process so it
41+
// can find symbols in sibling files.
42+
out, err = imports.Process(progName, in, nil)
43+
} else {
44+
out, err = format.Source(in)
45+
}
46+
if err != nil {
47+
errMsg := err.Error()
48+
// Prefix the error returned by format.Source.
49+
if !strings.HasPrefix(errMsg, f) {
50+
errMsg = fmt.Sprintf("%v:%v", f, errMsg)
51+
}
52+
json.NewEncoder(w).Encode(fmtResponse{Error: errMsg})
53+
return
54+
}
55+
fs.AddFile(f, out)
4256
}
43-
json.NewEncoder(w).Encode(resp)
57+
58+
json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())})
4459
}

Diff for: fmt_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2019 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 main
6+
7+
import (
8+
"encoding/json"
9+
"net/http/httptest"
10+
"net/url"
11+
"strings"
12+
"testing"
13+
)
14+
15+
func TestHandleFmt(t *testing.T) {
16+
for _, tt := range []struct {
17+
name string
18+
body string
19+
imports bool
20+
want string
21+
wantErr string
22+
}{
23+
{
24+
name: "classic",
25+
body: " package main\n func main( ) { }\n",
26+
want: "package main\n\nfunc main() {}\n",
27+
},
28+
{
29+
name: "classic_goimports",
30+
body: " package main\nvar _ = fmt.Printf",
31+
imports: true,
32+
want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n",
33+
},
34+
{
35+
name: "single_go_with_header",
36+
body: "-- prog.go --\n package main",
37+
want: "-- prog.go --\npackage main\n",
38+
},
39+
{
40+
name: "multi_go_with_header",
41+
body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5",
42+
want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n",
43+
},
44+
{
45+
name: "multi_go_without_header",
46+
body: " package main\n\n\n-- two.go --\n package main\n var X = 5",
47+
want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n",
48+
},
49+
{
50+
name: "only_format_go",
51+
body: " package main\n\n\n-- go.mod --\n module foo\n",
52+
want: "package main\n-- go.mod --\n module foo\n",
53+
},
54+
} {
55+
t.Run(tt.name, func(t *testing.T) {
56+
rec := httptest.NewRecorder()
57+
form := url.Values{}
58+
form.Set("body", tt.body)
59+
if tt.imports {
60+
form.Set("imports", "true")
61+
}
62+
req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode()))
63+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
64+
handleFmt(rec, req)
65+
resp := rec.Result()
66+
if resp.StatusCode != 200 {
67+
t.Fatalf("code = %v", resp.Status)
68+
}
69+
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
70+
t.Fatalf("Content-Type = %q; want application/json", ct)
71+
}
72+
var got fmtResponse
73+
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
74+
t.Fatal(err)
75+
}
76+
if got.Body != tt.want {
77+
t.Errorf("wrong output\n got: %q\nwant: %q\n", got.Body, tt.want)
78+
}
79+
if got.Error != tt.wantErr {
80+
t.Errorf("wrong error\n got err: %q\nwant err: %q\n", got.Error, tt.wantErr)
81+
}
82+
})
83+
}
84+
}

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.12
55
require (
66
cloud.google.com/go v0.38.0
77
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
8+
github.com/rogpeppe/go-internal v1.3.0
89
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect
910
golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73
1011
)

Diff for: go.sum

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
2222
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
2323
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
2424
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
25+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
26+
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
27+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
28+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
29+
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
30+
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
2531
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
2632
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
2733
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -69,5 +75,7 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCP
6975
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
7076
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
7177
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
78+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
79+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
7280
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
7381
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

Diff for: sandbox.go

+42-19
Original file line numberDiff line numberDiff line change
@@ -311,33 +311,57 @@ func compileAndRun(req *request) (*response, error) {
311311
}
312312
defer os.RemoveAll(tmpDir)
313313

314-
src := []byte(req.Body)
315-
in := filepath.Join(tmpDir, progName)
316-
if err := ioutil.WriteFile(in, src, 0400); err != nil {
317-
return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
314+
files, err := splitFiles([]byte(req.Body))
315+
if err != nil {
316+
return &response{Errors: err.Error()}, nil
318317
}
319318

320-
fset := token.NewFileSet()
319+
var testParam string
320+
var buildPkgArg = "."
321+
if files.Num() == 1 && len(files.Data(progName)) > 0 {
322+
buildPkgArg = progName
323+
src := files.Data(progName)
324+
if code := getTestProg(src); code != nil {
325+
testParam = "-test.v"
326+
files.AddFile(progName, code)
327+
}
328+
}
321329

322-
f, err := parser.ParseFile(fset, in, nil, parser.PackageClauseOnly)
323-
if err == nil && f.Name.Name != "main" {
324-
return &response{Errors: "package name must be main"}, nil
330+
useModules := allowModuleDownloads(files)
331+
if !files.Contains("go.mod") && useModules {
332+
files.AddFile("go.mod", []byte("module play\n"))
325333
}
326334

327-
var testParam string
328-
if code := getTestProg(src); code != nil {
329-
testParam = "-test.v"
330-
if err := ioutil.WriteFile(in, code, 0400); err != nil {
335+
for f, src := range files.m {
336+
// Before multi-file support we required that the
337+
// program be in package main, so continue to do that
338+
// for now. But permit anything in subdirectories to have other
339+
// packages.
340+
if !strings.Contains(f, "/") {
341+
fset := token.NewFileSet()
342+
f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly)
343+
if err == nil && f.Name.Name != "main" {
344+
return &response{Errors: "package name must be main"}, nil
345+
}
346+
}
347+
348+
in := filepath.Join(tmpDir, f)
349+
if strings.Contains(f, "/") {
350+
if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil {
351+
return nil, err
352+
}
353+
}
354+
if err := ioutil.WriteFile(in, src, 0644); err != nil {
331355
return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
332356
}
333357
}
334358

335359
exe := filepath.Join(tmpDir, "a.out")
336360
goCache := filepath.Join(tmpDir, "gocache")
337-
cmd := exec.Command("go", "build", "-o", exe, in)
361+
cmd := exec.Command("go", "build", "-o", exe, buildPkgArg)
362+
cmd.Dir = tmpDir
338363
var goPath string
339364
cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache}
340-
useModules := allowModuleDownloads(src)
341365
if useModules {
342366
// Create a GOPATH just for modules to be downloaded
343367
// into GOPATH/pkg/mod.
@@ -356,9 +380,8 @@ func compileAndRun(req *request) (*response, error) {
356380
if _, ok := err.(*exec.ExitError); ok {
357381
// Return compile errors to the user.
358382

359-
// Rewrite compiler errors to refer to progName
360-
// instead of '/tmp/sandbox1234/prog.go'.
361-
errs := strings.Replace(string(out), in, progName, -1)
383+
// Rewrite compiler errors to strip the tmpDir name.
384+
errs := strings.Replace(string(out), tmpDir+"/", "", -1)
362385

363386
// "go build", invoked with a file name, puts this odd
364387
// message before any compile errors; strip it.
@@ -422,8 +445,8 @@ func compileAndRun(req *request) (*response, error) {
422445

423446
// allowModuleDownloads reports whether the code snippet in src should be allowed
424447
// to download modules.
425-
func allowModuleDownloads(src []byte) bool {
426-
if bytes.Contains(src, []byte(`"code.google.com/p/go-tour/`)) {
448+
func allowModuleDownloads(files *fileSet) bool {
449+
if files.Num() == 1 && bytes.Contains(files.Data(progName), []byte(`"code.google.com/p/go-tour/`)) {
427450
// This domain doesn't exist anymore but we want old snippets using
428451
// these packages to still run, so the Dockerfile adds these packages
429452
// at this name in $GOPATH. Any snippets using this old name wouldn't

Diff for: server_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2017 The Go Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
4+
45
package main
56

67
import (
@@ -10,6 +11,7 @@ import (
1011
"io/ioutil"
1112
"net/http"
1213
"net/http/httptest"
14+
"os"
1315
"testing"
1416
)
1517

@@ -240,3 +242,35 @@ func TestCommandHandler(t *testing.T) {
240242
}
241243
}
242244
}
245+
246+
func TestAllowModuleDownloads(t *testing.T) {
247+
const envKey = "ALLOW_PLAY_MODULE_DOWNLOADS"
248+
defer func(old string) { os.Setenv(envKey, old) }(os.Getenv(envKey))
249+
250+
tests := []struct {
251+
src string
252+
env string
253+
want bool
254+
}{
255+
{src: "package main", want: true},
256+
{src: "package main", env: "false", want: false},
257+
{src: `import "code.google.com/p/go-tour/"`, want: false},
258+
}
259+
for i, tt := range tests {
260+
if tt.env != "" {
261+
os.Setenv(envKey, tt.env)
262+
} else {
263+
os.Setenv(envKey, "true")
264+
}
265+
files, err := splitFiles([]byte(tt.src))
266+
if err != nil {
267+
t.Errorf("%d. splitFiles = %v", i, err)
268+
continue
269+
}
270+
got := allowModuleDownloads(files)
271+
if got != tt.want {
272+
t.Errorf("%d. allow = %v; want %v; files:\n%s", i, got, tt.want, filesAsString(files))
273+
}
274+
}
275+
276+
}

0 commit comments

Comments
 (0)