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 }