github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/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  	"log"
    14  	"mime/multipart"
    15  	"net/http"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"regexp"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/chromedp/cdproto/cdp"
    24  	"github.com/chromedp/cdproto/runtime"
    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, id runtime.ExecutionContextID, 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, id runtime.ExecutionContextID, 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, id runtime.ExecutionContextID, 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  //nolint:golint,unused
   204  func mustCleanDir(dir string) {
   205  	must(os.Chdir(dir))
   206  	b, err := os.ReadFile(".gitignore")
   207  	if err != nil {
   208  		panic(err)
   209  	}
   210  	ss := strings.Split(string(b), "\n")
   211  	for _, s := range ss {
   212  		s = strings.TrimSpace(s)
   213  		if s == "" || s == "." || s == ".." || strings.HasPrefix(s, "#") {
   214  			continue
   215  		}
   216  		// log.Printf("removing: %s", s)
   217  		os.Remove(s)
   218  	}
   219  
   220  }
   221  
   222  // mustWriteSupportFiles will write index.html and wasm_exec.js to a directory
   223  func mustWriteSupportFiles(dir string, doWasmExec bool) {
   224  	if doWasmExec {
   225  		distutil.MustCopyFile(distutil.MustWasmExecJsPath(), filepath.Join(dir, "wasm_exec.js"))
   226  	}
   227  	// distutil.MustCopyFile(distutil.(), filepath.Join(dir, "wasm_exec.js"))
   228  	// log.Println(simplehttp.DefaultPageTemplateSource)
   229  
   230  	// "/wasm_exec.js"
   231  
   232  	var buf bytes.Buffer
   233  
   234  	req, _ := http.NewRequest("GET", "/index.html", nil)
   235  	outf, err := os.OpenFile(filepath.Join(dir, "index.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   236  	distutil.Must(err)
   237  	defer outf.Close()
   238  	err = template.Must(template.New("_page_").Parse(simplehttp.DefaultPageTemplateSource)).Execute(&buf, map[string]interface{}{"Request": req})
   239  	distutil.Must(err)
   240  	// HACK: fix wasm_exec.js path, unti we can come up with a better way to do this
   241  	_, err = outf.Write(
   242  		bytes.Replace(
   243  			bytes.Replace(buf.Bytes(), []byte(`"/wasm_exec.js"`), []byte(`"wasm_exec.js"`), 1),
   244  			[]byte("/main.wasm"), []byte("main.wasm"), 1,
   245  		),
   246  	)
   247  	distutil.Must(err)
   248  }
   249  
   250  // mustUploadDir tar+gz's the given directory and posts that file to the specified endpoint,
   251  // returning the path of where to access the files
   252  func mustUploadDir(dir, endpoint string) string {
   253  
   254  	var buf bytes.Buffer
   255  	gw := gzip.NewWriter(&buf)
   256  	tw := tar.NewWriter(gw)
   257  
   258  	absDir, err := filepath.Abs(dir)
   259  	if err != nil {
   260  		panic(err)
   261  	}
   262  
   263  	// var hdr tar.Header
   264  	err = filepath.Walk(dir, filepath.WalkFunc(func(fpath string, fi os.FileInfo, err error) error {
   265  		if err != nil {
   266  			return err
   267  		}
   268  
   269  		absPath, err := filepath.Abs(fpath)
   270  		if err != nil {
   271  			panic(err)
   272  		}
   273  
   274  		relPath := path.Clean("/" + strings.TrimPrefix(absPath, absDir))
   275  
   276  		// log.Printf("path = %q, fi.Name = %q", path, fi.Name())
   277  		hdr, err := tar.FileInfoHeader(fi, "")
   278  		if err != nil {
   279  			return err
   280  		}
   281  		hdr.Name = relPath
   282  		// hdr = tar.Header{
   283  		// 	Name: fi.Name(),
   284  		// 	Mode: 0644,
   285  		// 	Size: fi.Size(),
   286  		// }
   287  		err = tw.WriteHeader(hdr)
   288  		if err != nil {
   289  			return err
   290  		}
   291  
   292  		// no body to write for directories
   293  		if fi.IsDir() {
   294  			return nil
   295  		}
   296  
   297  		inf, err := os.Open(fpath)
   298  		if err != nil {
   299  			return err
   300  		}
   301  		defer inf.Close()
   302  		_, err = io.Copy(tw, inf)
   303  
   304  		if err != nil {
   305  			return err
   306  		}
   307  
   308  		return nil
   309  	}))
   310  	if err != nil {
   311  		panic(err)
   312  	}
   313  
   314  	tw.Close()
   315  	gw.Close()
   316  
   317  	var body bytes.Buffer
   318  	writer := multipart.NewWriter(&body)
   319  	part, err := writer.CreateFormFile("archive", filepath.Base(dir)+".tar.gz")
   320  	if err != nil {
   321  		panic(err)
   322  	}
   323  	_, err = io.Copy(part, &buf)
   324  	if err != nil {
   325  		panic(err)
   326  	}
   327  	err = writer.Close()
   328  	if err != nil {
   329  		panic(err)
   330  	}
   331  
   332  	// for key, val := range params {
   333  	// 	_ = writer.WriteField(key, val)
   334  	// }
   335  	// err = writer.Close()
   336  	// if err != nil {
   337  	// 	return nil, err
   338  	// }
   339  
   340  	req, err := http.NewRequest("POST", endpoint, &body)
   341  	if err != nil {
   342  		panic(err)
   343  	}
   344  	req.Header.Set("Content-Type", writer.FormDataContentType())
   345  
   346  	res, err := http.DefaultClient.Do(req)
   347  	if err != nil {
   348  		panic(err)
   349  	}
   350  	defer res.Body.Close()
   351  
   352  	var retData struct {
   353  		Path string `json:"path"`
   354  	}
   355  	err = json.NewDecoder(res.Body).Decode(&retData)
   356  	if err != nil {
   357  		panic(err)
   358  	}
   359  	return retData.Path
   360  }
   361  
   362  // mustTGTempGopathSetup makes a temp dir and recursively copies from testPjtDir into
   363  // filepath.Join(tmpDir, outRelPath) and returns the temp dir, which can be used
   364  // as the GOPATH for a tinygo build
   365  //
   366  //nolint:golint,unused
   367  func mustTGTempGopathSetup(testPjtDir, outRelPath string) string {
   368  	// buildGopath := mustTGTempGopathSetup(dir, "src/main")
   369  
   370  	tmpParent, err := filepath.Abs(filepath.Join(testPjtDir, "../tmp"))
   371  	if err != nil {
   372  		panic(err)
   373  	}
   374  
   375  	name, err := os.MkdirTemp(tmpParent, "tggopath")
   376  	if err != nil {
   377  		panic(err)
   378  	}
   379  
   380  	log.Printf("testPjtdir=%s, name=%s", testPjtDir, name)
   381  
   382  	// copy vugu package files
   383  
   384  	// HACK: for now we use specific files names in order to avoid recursive stuff getting out of control - we should figure out something better
   385  	srcDir := filepath.Join(testPjtDir, "../..")
   386  	dstDir := filepath.Join(name, "src/github.com/vugu/vugu")
   387  	must(os.MkdirAll(dstDir, 0755))
   388  	fis, err := os.ReadDir(srcDir)
   389  	must(err)
   390  	for _, fi := range fis {
   391  		if fi.IsDir() {
   392  			continue
   393  		}
   394  		distutil.MustCopyFile(filepath.Join(srcDir, fi.Name()), filepath.Join(dstDir, fi.Name()))
   395  	}
   396  
   397  	// for _, n := range []string{
   398  	// 	"build-env.go",
   399  	// 	"change-counter.go",
   400  	// 	"change-counter_test.go",
   401  	// 	"comp-key.go",
   402  	// 	"comp-key_test.go",
   403  	// 	"component.go",
   404  	// 	"doc.go",
   405  	// 	"events-component.go",
   406  	// 	"events-dom.go",
   407  	// 	"mod-check-common.go",
   408  	// 	"mod-check-default.go",
   409  	// 	"mod-check-tinygo.go",
   410  	// 	"mod-check_test.go",
   411  	// 	"vgnode.go",
   412  	// 	"vgnode_test.go",
   413  	// } {
   414  	// 	distutil.MustCopyFile(filepath.Join(srcDir, n), filepath.Join(dstDir, n))
   415  	// }
   416  
   417  	allPattern := regexp.MustCompile(`.*`)
   418  	for _, n := range []string{"domrender", "internal", "js"} {
   419  		distutil.MustCopyDirFiltered(
   420  			filepath.Join(srcDir, n),
   421  			filepath.Join(name, "src/github.com/vugu/vugu", n),
   422  			allPattern)
   423  	}
   424  
   425  	// now finally copy the actual test program
   426  	distutil.MustCopyDirFiltered(
   427  		testPjtDir,
   428  		filepath.Join(name, "src/tgtestpgm"),
   429  		allPattern)
   430  
   431  	// distutil.MustCopyDirFiltered(srcDir, filepath.Join(name, "src/github.com/vugu/vugu"),
   432  	// 	regexp.MustCompile(`^.*\.go$`),
   433  	// 	// regexp.MustCompile(`^((.*\.go)|internal|domrender|js)$`),
   434  	// )
   435  
   436  	// log.Printf("TODO: copy vugu source into place")
   437  
   438  	return name
   439  }
   440  
   441  // mustTGGoGet runs `go get` on the packages you give it with GO111MODULE=off and GOPATH set to the path you give.
   442  // This can be used to
   443  //
   444  //nolint:golint,unused
   445  func mustTGGoGet(buildGopath string, pkgNames ...string) {
   446  	// mustTGGoGet(buildGopath, "github.com/vugu/xxhash", "github.com/vugu/vjson")
   447  
   448  	// oldDir, err := os.Getwd()
   449  	// if err != nil {
   450  	// 	panic(err)
   451  	// }
   452  	// defer os.Chdir(oldDir)
   453  
   454  	// os.Chdir(buildGopath)
   455  
   456  	var args []string
   457  	args = append(args, "get")
   458  	args = append(args, pkgNames...)
   459  	fmt.Print(distutil.MustEnvExec([]string{"GO111MODULE=off", "GOPATH=" + buildGopath}, "go", args...))
   460  }
   461  
   462  // mustTGBuildAndLoad does a build and load - absdir is the original program path (the "test-NNN-desc" folder),
   463  // and buildGopath is the temp dir where everything was copied in order to make non-module version that tinygo can compile
   464  //
   465  //nolint:golint,unused
   466  func mustTGBuildAndLoad(absdir, buildGopath string) string {
   467  	// pathSuffix := mustTGBuildAndLoad(dir, buildGopath)
   468  
   469  	// fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(absdir, "main.wasm"), "."))
   470  
   471  	// mustWriteSupportFiles(absdir)
   472  
   473  	// FROM tinygo/tinygo-dev:latest
   474  
   475  	args := []string{
   476  		"run",
   477  		"--rm", // remove after run
   478  		// "-it",                            // connect console
   479  		"-v", buildGopath + "/src:/go/src", // map src from buildGopath
   480  		"-v", absdir + ":/out", // map original dir as /out so it can just write the .wasm file
   481  		"-e", "GOPATH=/go", // set GOPATH so it picks up buildGopath/src
   482  		"vugu/tinygo-dev:latest",                                                  // use latest dev (for now)
   483  		"tinygo", "build", "-o", "/out/main.wasm", "-target", "wasm", "tgtestpgm", // tinygo command line
   484  	}
   485  
   486  	log.Printf("Executing: docker %v", args)
   487  
   488  	fmt.Print(distutil.MustExec("docker", args...))
   489  
   490  	fmt.Println("TODO: tinygo support files")
   491  
   492  	// docker run --rm -it -v `pwd`/tinygo-dev:/go/src/testpgm -e "GOPATH=/go" tinygotest \
   493  	// tinygo build -o /go/src/testpgm/testpgm.wasm -target wasm testpgm
   494  
   495  	// # copy wasm_exec.js out
   496  	// if ! [ -f tinygo-dev/wasm_exec.js ]; then
   497  	// echo "Copying wasm_exec.js"
   498  	// 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/"
   499  	// fi
   500  
   501  	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   502  	// log.Printf("uploadPath = %q", uploadPath)
   503  
   504  	return uploadPath
   505  }
   506  
   507  func mustTGGenBuildAndLoad(absdir string, useDocker bool) string {
   508  
   509  	mustTGGen(absdir)
   510  
   511  	wc := devutil.MustNewTinygoCompiler().SetDir(absdir)
   512  	defer wc.Close()
   513  
   514  	if !useDocker {
   515  		wc = wc.NoDocker()
   516  	}
   517  
   518  	outfile, err := wc.Execute()
   519  	if err != nil {
   520  		panic(err)
   521  	}
   522  	defer os.Remove(outfile)
   523  
   524  	must(distutil.CopyFile(outfile, filepath.Join(absdir, "main.wasm")))
   525  
   526  	wasmExecJSR, err := wc.WasmExecJS()
   527  	must(err)
   528  	wasmExecJSB, err := io.ReadAll(wasmExecJSR)
   529  	must(err)
   530  	wasmExecJSPath := filepath.Join(absdir, "wasm_exec.js")
   531  	must(os.WriteFile(wasmExecJSPath, wasmExecJSB, 0644))
   532  
   533  	mustWriteSupportFiles(absdir, false)
   534  
   535  	uploadPath := mustUploadDir(absdir, "http://localhost:8846/upload")
   536  
   537  	return uploadPath
   538  }