Skip to content
This repository has been archived by the owner on Apr 23, 2023. It is now read-only.

Commit

Permalink
playground: support multiple input files in txtar format
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
bradfitz committed May 15, 2019
1 parent 1b5b098 commit a7b4d4c
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 47 deletions.
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ RUN mkdir /gocache
ENV GOCACHE /gocache
ENV GO111MODULE on

COPY go.mod /go/src/playground/go.mod
COPY go.sum /go/src/playground/go.sum
WORKDIR /go/src/playground

# Pre-build some packages to speed final install later.
RUN go install cloud.google.com/go/compute/metadata
RUN go install cloud.google.com/go/datastore
RUN go install github.com/bradfitz/gomemcache/memcache
RUN go install golang.org/x/tools/godoc/static
RUN go install golang.org/x/tools/imports
RUN go install github.com/rogpeppe/go-internal/txtar

# Add and compile playground daemon
COPY . /go/src/playground/
Expand Down
51 changes: 33 additions & 18 deletions fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,40 @@ type fmtResponse struct {
}

func handleFmt(w http.ResponseWriter, r *http.Request) {
var (
in = []byte(r.FormValue("body"))
out []byte
err error
)
if r.FormValue("imports") != "" {
out, err = imports.Process(progName, in, nil)
} else {
out, err = format.Source(in)
}
var resp fmtResponse
w.Header().Set("Content-Type", "application/json")

fs, err := splitFiles([]byte(r.FormValue("body")))
if err != nil {
resp.Error = err.Error()
// Prefix the error returned by format.Source.
if !strings.HasPrefix(resp.Error, progName) {
resp.Error = fmt.Sprintf("%v:%v", progName, resp.Error)
json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()})
return
}

fixImports := r.FormValue("imports") != ""
for _, f := range fs.files {
if !strings.HasSuffix(f, ".go") {
continue
}
} else {
resp.Body = string(out)
var out []byte
var err error
in := fs.m[f]
if fixImports {
// TODO: pass options to imports.Process so it
// can find symbols in sibling files.
out, err = imports.Process(progName, in, nil)
} else {
out, err = format.Source(in)
}
if err != nil {
errMsg := err.Error()
// Prefix the error returned by format.Source.
if !strings.HasPrefix(errMsg, f) {
errMsg = fmt.Sprintf("%v:%v", f, errMsg)
}
json.NewEncoder(w).Encode(fmtResponse{Error: errMsg})
return
}
fs.AddFile(f, out)
}
json.NewEncoder(w).Encode(resp)

json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())})
}
84 changes: 84 additions & 0 deletions fmt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
"encoding/json"
"net/http/httptest"
"net/url"
"strings"
"testing"
)

func TestHandleFmt(t *testing.T) {
for _, tt := range []struct {
name string
body string
imports bool
want string
wantErr string
}{
{
name: "classic",
body: " package main\n func main( ) { }\n",
want: "package main\n\nfunc main() {}\n",
},
{
name: "classic_goimports",
body: " package main\nvar _ = fmt.Printf",
imports: true,
want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n",
},
{
name: "single_go_with_header",
body: "-- prog.go --\n package main",
want: "-- prog.go --\npackage main\n",
},
{
name: "multi_go_with_header",
body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5",
want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n",
},
{
name: "multi_go_without_header",
body: " package main\n\n\n-- two.go --\n package main\n var X = 5",
want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n",
},
{
name: "only_format_go",
body: " package main\n\n\n-- go.mod --\n module foo\n",
want: "package main\n-- go.mod --\n module foo\n",
},
} {
t.Run(tt.name, func(t *testing.T) {
rec := httptest.NewRecorder()
form := url.Values{}
form.Set("body", tt.body)
if tt.imports {
form.Set("imports", "true")
}
req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
handleFmt(rec, req)
resp := rec.Result()
if resp.StatusCode != 200 {
t.Fatalf("code = %v", resp.Status)
}
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
t.Fatalf("Content-Type = %q; want application/json", ct)
}
var got fmtResponse
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if got.Body != tt.want {
t.Errorf("wrong output\n got: %q\nwant: %q\n", got.Body, tt.want)
}
if got.Error != tt.wantErr {
t.Errorf("wrong error\n got err: %q\nwant err: %q\n", got.Error, tt.wantErr)
}
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.12
require (
cloud.google.com/go v0.38.0
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
github.com/rogpeppe/go-internal v1.3.0
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect
golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down Expand Up @@ -69,5 +75,7 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCP
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
61 changes: 42 additions & 19 deletions sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,33 +311,57 @@ func compileAndRun(req *request) (*response, error) {
}
defer os.RemoveAll(tmpDir)

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

fset := token.NewFileSet()
var testParam string
var buildPkgArg = "."
if files.Num() == 1 && len(files.Data(progName)) > 0 {
buildPkgArg = progName
src := files.Data(progName)
if code := getTestProg(src); code != nil {
testParam = "-test.v"
files.AddFile(progName, code)
}
}

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

var testParam string
if code := getTestProg(src); code != nil {
testParam = "-test.v"
if err := ioutil.WriteFile(in, code, 0400); err != nil {
for f, src := range files.m {
// Before multi-file support we required that the
// program be in package main, so continue to do that
// for now. But permit anything in subdirectories to have other
// packages.
if !strings.Contains(f, "/") {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly)
if err == nil && f.Name.Name != "main" {
return &response{Errors: "package name must be main"}, nil
}
}

in := filepath.Join(tmpDir, f)
if strings.Contains(f, "/") {
if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil {
return nil, err
}
}
if err := ioutil.WriteFile(in, src, 0644); err != nil {
return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
}
}

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

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

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

// allowModuleDownloads reports whether the code snippet in src should be allowed
// to download modules.
func allowModuleDownloads(src []byte) bool {
if bytes.Contains(src, []byte(`"code.google.com/p/go-tour/`)) {
func allowModuleDownloads(files *fileSet) bool {
if files.Num() == 1 && bytes.Contains(files.Data(progName), []byte(`"code.google.com/p/go-tour/`)) {
// This domain doesn't exist anymore but we want old snippets using
// these packages to still run, so the Dockerfile adds these packages
// at this name in $GOPATH. Any snippets using this old name wouldn't
Expand Down
34 changes: 34 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
Expand All @@ -10,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
)

Expand Down Expand Up @@ -240,3 +242,35 @@ func TestCommandHandler(t *testing.T) {
}
}
}

func TestAllowModuleDownloads(t *testing.T) {
const envKey = "ALLOW_PLAY_MODULE_DOWNLOADS"
defer func(old string) { os.Setenv(envKey, old) }(os.Getenv(envKey))

tests := []struct {
src string
env string
want bool
}{
{src: "package main", want: true},
{src: "package main", env: "false", want: false},
{src: `import "code.google.com/p/go-tour/"`, want: false},
}
for i, tt := range tests {
if tt.env != "" {
os.Setenv(envKey, tt.env)
} else {
os.Setenv(envKey, "true")
}
files, err := splitFiles([]byte(tt.src))
if err != nil {
t.Errorf("%d. splitFiles = %v", i, err)
continue
}
got := allowModuleDownloads(files)
if got != tt.want {
t.Errorf("%d. allow = %v; want %v; files:\n%s", i, got, tt.want, filesAsString(files))
}
}

}
Loading

0 comments on commit a7b4d4c

Please sign in to comment.