github.com/vugu/vugu@v0.3.5/wasm-test-suite/wasm-suite-util_test.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"html/template"
    12  	"io"
    13  	"io/ioutil"
    14  	"log"
    15  	"mime/multipart"
    16  	"net/http"
    17  	"os"
    18  	"path"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/chromedp/cdproto/cdp"
    25  	"github.com/chromedp/chromedp"
    26  
    27  	"github.com/vugu/vugu/devutil"
    28  	"github.com/vugu/vugu/distutil"
    29  	"github.com/vugu/vugu/gen"
    30  	"github.com/vugu/vugu/simplehttp"
    31  )
    32  
    33  func queryNode(ref string, assert func(n *cdp.Node)) chromedp.QueryAction {
    34  	return chromedp.QueryAfter(ref, func(ctx context.Context, nodes ...*cdp.Node) error {
    35  		if len(nodes) == 0 {
    36  			return fmt.Errorf("no %s element found", ref)
    37  		}
    38  		assert(nodes[0])
    39  		return nil
    40  	})
    41  }
    42  
    43  func queryAttributes(ref string, assert func(attributes map[string]string)) chromedp.QueryAction {
    44  	return chromedp.QueryAfter(ref, func(ctx context.Context, nodes ...*cdp.Node) error {
    45  		attributes := make(map[string]string)
    46  		if err := chromedp.Attributes(ref, &attributes).Do(ctx); err != nil {
    47  			return err
    48  		}
    49  		assert(attributes)
    50  		return nil
    51  	})
    52  }
    53  
    54  // WaitInnerTextTrimEq will wait for the innerText of the specified element to match a specific string after whitespace trimming.
    55  func WaitInnerTextTrimEq(sel, innerText string) chromedp.QueryAction {
    56  
    57  	return chromedp.Query(sel, func(s *chromedp.Selector) {
    58  
    59  		chromedp.WaitFunc(func(ctx context.Context, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
    60  
    61  			nodes := make([]*cdp.Node, len(ids))
    62  			cur.RLock()
    63  			for i, id := range ids {
    64  				nodes[i] = cur.Nodes[id]
    65  				if nodes[i] == nil {
    66  					cur.RUnlock()
    67  					// not yet ready
    68  					return nil, nil
    69  				}
    70  			}
    71  			cur.RUnlock()
    72  
    73  			var ret string
    74  			err := chromedp.EvaluateAsDevTools("document.querySelector('"+sel+"').innerText", &ret).Do(ctx)
    75  			if err != nil {
    76  				return nodes, err
    77  			}
    78  			if strings.TrimSpace(ret) != innerText {
    79  				// log.Printf("found text: %s", ret)
    80  				return nodes, errors.New("unexpected value: " + ret)
    81  			}
    82  
    83  			// log.Printf("NodeValue: %#v", nodes[0])
    84  
    85  			// return nil, errors.New("not ready yet")
    86  			return nodes, nil
    87  		})(s)
    88  
    89  	})
    90  
    91  }
    92  
    93  // returns absdir
    94  func mustUseDir(reldir string) (newdir, olddir string) {
    95  
    96  	odir, err := os.Getwd()
    97  	if err != nil {
    98  		panic(err)
    99  	}
   100  	olddir = odir
   101  
   102  	dir, err := filepath.Abs(reldir)
   103  	if err != nil {
   104  		panic(err)
   105  	}
   106  
   107  	must(os.Chdir(dir))
   108  
   109  	newdir = dir
   110  
   111  	return
   112  }
   113  
   114  func mustGen(absdir string) {
   115  
   116  	os.Remove(filepath.Join(absdir, "main_wasm.go")) // ensure it gets re-generated
   117  	pp := gen.NewParserGoPkg(absdir, nil)
   118  	err := pp.Run()
   119  	if err != nil {
   120  		panic(err)
   121  	}
   122  
   123  }
   124  
   125  func mustTGGen(absdir string) {
   126  
   127  	os.Remove(filepath.Join(absdir, "main_wasm.go")) // ensure it gets re-generated
   128  	pp := gen.NewParserGoPkg(absdir, &gen.ParserGoPkgOpts{TinyGo: true})
   129  	err := pp.Run()
   130  	if err != nil {
   131  		panic(err)
   132  	}
   133  
   134  }
   135  
   136  func mustGenBuildAndLoad(absdir string) string {
   137  	mustGen(absdir)
   138  	return mustBuildAndLoad(absdir)
   139  }
   140  
   141  // returns path suffix
   142  func mustBuildAndLoad(absdir string) string {
   143  
   144  	fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "mod", "tidy"))
   145  	fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(absdir, "main.wasm"), "."))
   146  
   147  	mustWriteSupportFiles(absdir, true)
   148  
   149  	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   150  	// log.Printf("uploadPath = %q", uploadPath)
   151  
   152  	return uploadPath
   153  }
   154  
   155  // // like mustBuildAndLoad but with tinygo
   156  // func mustBuildAndLoadTinygo(absdir string) string {
   157  
   158  // 	fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(absdir, "main.wasm"), "."))
   159  
   160  // 	mustWriteSupportFiles(absdir)
   161  
   162  // 	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   163  // 	// log.Printf("uploadPath = %q", uploadPath)
   164  
   165  // 	return uploadPath
   166  // }
   167  
   168  func mustChromeCtx() (context.Context, context.CancelFunc) {
   169  
   170  	debugURL := func() string {
   171  		resp, err := http.Get("http://localhost:9222/json/version")
   172  		if err != nil {
   173  			panic(err)
   174  		}
   175  
   176  		var result map[string]interface{}
   177  
   178  		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
   179  			panic(err)
   180  		}
   181  		return result["webSocketDebuggerUrl"].(string)
   182  	}()
   183  
   184  	// t.Log(debugURL)
   185  
   186  	allocCtx, _ := chromedp.NewRemoteAllocator(context.Background(), debugURL)
   187  	// defer cancel()
   188  
   189  	ctx, _ := chromedp.NewContext(allocCtx) // , chromedp.WithLogf(log.Printf))
   190  	// defer cancel()
   191  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   192  	// defer cancel()
   193  
   194  	return ctx, cancel
   195  }
   196  
   197  func must(err error) {
   198  	if err != nil {
   199  		panic(err)
   200  	}
   201  }
   202  
   203  func mustCleanDir(dir string) {
   204  	must(os.Chdir(dir))
   205  	b, err := ioutil.ReadFile(".gitignore")
   206  	if err != nil {
   207  		panic(err)
   208  	}
   209  	ss := strings.Split(string(b), "\n")
   210  	for _, s := range ss {
   211  		s = strings.TrimSpace(s)
   212  		if s == "" || s == "." || s == ".." || strings.HasPrefix(s, "#") {
   213  			continue
   214  		}
   215  		// log.Printf("removing: %s", s)
   216  		os.Remove(s)
   217  	}
   218  
   219  }
   220  
   221  // mustWriteSupportFiles will write index.html and wasm_exec.js to a directory
   222  func mustWriteSupportFiles(dir string, doWasmExec bool) {
   223  	if doWasmExec {
   224  		distutil.MustCopyFile(distutil.MustWasmExecJsPath(), filepath.Join(dir, "wasm_exec.js"))
   225  	}
   226  	// distutil.MustCopyFile(distutil.(), filepath.Join(dir, "wasm_exec.js"))
   227  	// log.Println(simplehttp.DefaultPageTemplateSource)
   228  
   229  	// "/wasm_exec.js"
   230  
   231  	var buf bytes.Buffer
   232  
   233  	req, _ := http.NewRequest("GET", "/index.html", nil)
   234  	outf, err := os.OpenFile(filepath.Join(dir, "index.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   235  	distutil.Must(err)
   236  	defer outf.Close()
   237  	template.Must(template.New("_page_").Parse(simplehttp.DefaultPageTemplateSource)).Execute(&buf, map[string]interface{}{"Request": req})
   238  	// HACK: fix wasm_exec.js path, unti we can come up with a better way to do this
   239  	outf.Write(
   240  		bytes.Replace(
   241  			bytes.Replace(buf.Bytes(), []byte(`"/wasm_exec.js"`), []byte(`"wasm_exec.js"`), 1),
   242  			[]byte("/main.wasm"), []byte("main.wasm"), 1,
   243  		),
   244  	)
   245  
   246  }
   247  
   248  // mustUploadDir tar+gz's the given directory and posts that file to the specified endpoint,
   249  // returning the path of where to access the files
   250  func mustUploadDir(dir, endpoint string) string {
   251  
   252  	var buf bytes.Buffer
   253  	gw := gzip.NewWriter(&buf)
   254  	tw := tar.NewWriter(gw)
   255  
   256  	absDir, err := filepath.Abs(dir)
   257  	if err != nil {
   258  		panic(err)
   259  	}
   260  
   261  	// var hdr tar.Header
   262  	err = filepath.Walk(dir, filepath.WalkFunc(func(fpath string, fi os.FileInfo, err error) error {
   263  		if err != nil {
   264  			return err
   265  		}
   266  
   267  		absPath, err := filepath.Abs(fpath)
   268  		if err != nil {
   269  			panic(err)
   270  		}
   271  
   272  		relPath := path.Clean("/" + strings.TrimPrefix(absPath, absDir))
   273  
   274  		// log.Printf("path = %q, fi.Name = %q", path, fi.Name())
   275  		hdr, err := tar.FileInfoHeader(fi, "")
   276  		hdr.Name = relPath
   277  		// hdr = tar.Header{
   278  		// 	Name: fi.Name(),
   279  		// 	Mode: 0644,
   280  		// 	Size: fi.Size(),
   281  		// }
   282  		err = tw.WriteHeader(hdr)
   283  		if err != nil {
   284  			return err
   285  		}
   286  
   287  		// no body to write for directories
   288  		if fi.IsDir() {
   289  			return nil
   290  		}
   291  
   292  		inf, err := os.Open(fpath)
   293  		if err != nil {
   294  			return err
   295  		}
   296  		defer inf.Close()
   297  		_, err = io.Copy(tw, inf)
   298  
   299  		if err != nil {
   300  			return err
   301  		}
   302  
   303  		return nil
   304  	}))
   305  	if err != nil {
   306  		panic(err)
   307  	}
   308  
   309  	tw.Close()
   310  	gw.Close()
   311  
   312  	var body bytes.Buffer
   313  	writer := multipart.NewWriter(&body)
   314  	part, err := writer.CreateFormFile("archive", filepath.Base(dir)+".tar.gz")
   315  	if err != nil {
   316  		panic(err)
   317  	}
   318  	_, err = io.Copy(part, &buf)
   319  	if err != nil {
   320  		panic(err)
   321  	}
   322  	err = writer.Close()
   323  	if err != nil {
   324  		panic(err)
   325  	}
   326  
   327  	// for key, val := range params {
   328  	// 	_ = writer.WriteField(key, val)
   329  	// }
   330  	// err = writer.Close()
   331  	// if err != nil {
   332  	// 	return nil, err
   333  	// }
   334  
   335  	req, err := http.NewRequest("POST", endpoint, &body)
   336  	if err != nil {
   337  		panic(err)
   338  	}
   339  	req.Header.Set("Content-Type", writer.FormDataContentType())
   340  
   341  	res, err := http.DefaultClient.Do(req)
   342  	if err != nil {
   343  		panic(err)
   344  	}
   345  	defer res.Body.Close()
   346  
   347  	var retData struct {
   348  		Path string `json:"path"`
   349  	}
   350  	err = json.NewDecoder(res.Body).Decode(&retData)
   351  	if err != nil {
   352  		panic(err)
   353  	}
   354  	return retData.Path
   355  }
   356  
   357  // mustTGTempGopathSetup makes a temp dir and recursively copies from testPjtDir into
   358  // filepath.Join(tmpDir, outRelPath) and returns the temp dir, which can be used
   359  // as the GOPATH for a tinygo build
   360  func mustTGTempGopathSetup(testPjtDir, outRelPath string) string {
   361  	// buildGopath := mustTGTempGopathSetup(dir, "src/main")
   362  
   363  	tmpParent, err := filepath.Abs(filepath.Join(testPjtDir, "../tmp"))
   364  	if err != nil {
   365  		panic(err)
   366  	}
   367  
   368  	name, err := ioutil.TempDir(tmpParent, "tggopath")
   369  	if err != nil {
   370  		panic(err)
   371  	}
   372  
   373  	log.Printf("testPjtdir=%s, name=%s", testPjtDir, name)
   374  
   375  	// copy vugu package files
   376  
   377  	// HACK: for now we use specific files names in order to avoid recursive stuff getting out of control - we should figure out something better
   378  	srcDir := filepath.Join(testPjtDir, "../..")
   379  	dstDir := filepath.Join(name, "src/github.com/vugu/vugu")
   380  	must(os.MkdirAll(dstDir, 0755))
   381  	fis, err := ioutil.ReadDir(srcDir)
   382  	must(err)
   383  	for _, fi := range fis {
   384  		if fi.IsDir() {
   385  			continue
   386  		}
   387  		distutil.MustCopyFile(filepath.Join(srcDir, fi.Name()), filepath.Join(dstDir, fi.Name()))
   388  	}
   389  
   390  	// for _, n := range []string{
   391  	// 	"build-env.go",
   392  	// 	"change-counter.go",
   393  	// 	"change-counter_test.go",
   394  	// 	"comp-key.go",
   395  	// 	"comp-key_test.go",
   396  	// 	"component.go",
   397  	// 	"doc.go",
   398  	// 	"events-component.go",
   399  	// 	"events-dom.go",
   400  	// 	"mod-check-common.go",
   401  	// 	"mod-check-default.go",
   402  	// 	"mod-check-tinygo.go",
   403  	// 	"mod-check_test.go",
   404  	// 	"vgnode.go",
   405  	// 	"vgnode_test.go",
   406  	// } {
   407  	// 	distutil.MustCopyFile(filepath.Join(srcDir, n), filepath.Join(dstDir, n))
   408  	// }
   409  
   410  	allPattern := regexp.MustCompile(`.*`)
   411  	for _, n := range []string{"domrender", "internal", "js"} {
   412  		distutil.MustCopyDirFiltered(
   413  			filepath.Join(srcDir, n),
   414  			filepath.Join(name, "src/github.com/vugu/vugu", n),
   415  			allPattern)
   416  	}
   417  
   418  	// now finally copy the actual test program
   419  	distutil.MustCopyDirFiltered(
   420  		testPjtDir,
   421  		filepath.Join(name, "src/tgtestpgm"),
   422  		allPattern)
   423  
   424  	// distutil.MustCopyDirFiltered(srcDir, filepath.Join(name, "src/github.com/vugu/vugu"),
   425  	// 	regexp.MustCompile(`^.*\.go$`),
   426  	// 	// regexp.MustCompile(`^((.*\.go)|internal|domrender|js)$`),
   427  	// )
   428  
   429  	// log.Printf("TODO: copy vugu source into place")
   430  
   431  	return name
   432  }
   433  
   434  // mustTGGoGet runs `go get` on the packages you give it with GO111MODULE=off and GOPATH set to the path you give.
   435  // This can be used to
   436  func mustTGGoGet(buildGopath string, pkgNames ...string) {
   437  	// mustTGGoGet(buildGopath, "github.com/vugu/xxhash", "github.com/vugu/vjson")
   438  
   439  	// oldDir, err := os.Getwd()
   440  	// if err != nil {
   441  	// 	panic(err)
   442  	// }
   443  	// defer os.Chdir(oldDir)
   444  
   445  	// os.Chdir(buildGopath)
   446  
   447  	var args []string
   448  	args = append(args, "get")
   449  	args = append(args, pkgNames...)
   450  	fmt.Print(distutil.MustEnvExec([]string{"GO111MODULE=off", "GOPATH=" + buildGopath}, "go", args...))
   451  }
   452  
   453  // mustTGBuildAndLoad does a build and load - absdir is the original program path (the "test-NNN-desc" folder),
   454  // and buildGopath is the temp dir where everything was copied in order to make non-module version that tinygo can compile
   455  func mustTGBuildAndLoad(absdir, buildGopath string) string {
   456  	// pathSuffix := mustTGBuildAndLoad(dir, buildGopath)
   457  
   458  	// fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(absdir, "main.wasm"), "."))
   459  
   460  	// mustWriteSupportFiles(absdir)
   461  
   462  	// FROM tinygo/tinygo-dev:latest
   463  
   464  	args := []string{
   465  		"run",
   466  		"--rm", // remove after run
   467  		// "-it",                            // connect console
   468  		"-v", buildGopath + "/src:/go/src", // map src from buildGopath
   469  		"-v", absdir + ":/out", // map original dir as /out so it can just write the .wasm file
   470  		"-e", "GOPATH=/go", // set GOPATH so it picks up buildGopath/src
   471  		"vugu/tinygo-dev:latest",                                                  // use latest dev (for now)
   472  		"tinygo", "build", "-o", "/out/main.wasm", "-target", "wasm", "tgtestpgm", // tinygo command line
   473  	}
   474  
   475  	log.Printf("Executing: docker %v", args)
   476  
   477  	fmt.Print(distutil.MustExec("docker", args...))
   478  
   479  	fmt.Println("TODO: tinygo support files")
   480  
   481  	// docker run --rm -it -v `pwd`/tinygo-dev:/go/src/testpgm -e "GOPATH=/go" tinygotest \
   482  	// tinygo build -o /go/src/testpgm/testpgm.wasm -target wasm testpgm
   483  
   484  	// # copy wasm_exec.js out
   485  	// if ! [ -f tinygo-dev/wasm_exec.js ]; then
   486  	// echo "Copying wasm_exec.js"
   487  	// docker run --rm -it -v `pwd`/tinygo-dev:/go/src/testpgm tinygotest /bin/bash -c "cp /usr/local/tinygo/targets/wasm_exec.js /go/src/testpgm/"
   488  	// fi
   489  
   490  	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   491  	// log.Printf("uploadPath = %q", uploadPath)
   492  
   493  	return uploadPath
   494  }
   495  
   496  func mustTGGenBuildAndLoad(absdir string, useDocker bool) string {
   497  
   498  	mustTGGen(absdir)
   499  
   500  	wc := devutil.MustNewTinygoCompiler().SetDir(absdir)
   501  	defer wc.Close()
   502  
   503  	if !useDocker {
   504  		wc = wc.NoDocker()
   505  	}
   506  
   507  	outfile, err := wc.Execute()
   508  	if err != nil {
   509  		panic(err)
   510  	}
   511  	defer os.Remove(outfile)
   512  
   513  	must(distutil.CopyFile(outfile, filepath.Join(absdir, "main.wasm")))
   514  
   515  	wasmExecJSR, err := wc.WasmExecJS()
   516  	must(err)
   517  	wasmExecJSB, err := ioutil.ReadAll(wasmExecJSR)
   518  	must(err)
   519  	wasmExecJSPath := filepath.Join(absdir, "wasm_exec.js")
   520  	must(ioutil.WriteFile(wasmExecJSPath, wasmExecJSB, 0644))
   521  
   522  	mustWriteSupportFiles(absdir, false)
   523  
   524  	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   525  
   526  	return uploadPath
   527  }