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 }