44 "bytes"
55 "context"
66 "errors"
7+ "sync"
78 "testing"
89
910 "github.com/google/go-cmp/cmp"
@@ -263,18 +264,7 @@ func TestCPython_Eval(t *testing.T) {
263264}
264265
265266func TestRealWasm_OneshotAcquire (t * testing.T ) {
266- if testing .Short () {
267- t .Skip ("skip real wasm test in -short mode" )
268- }
269- stdlib , err := cpython .WithStdlib ()
270- if err != nil {
271- t .Fatal (err )
272- }
273- rt , err := sango .New (t .Context (), cpython .Wasm (), cpython .CPython (), sango .WithWASI (), stdlib )
274- if err != nil {
275- t .Fatal (err )
276- }
277- defer rt .Close (t .Context ())
267+ rt := newExtRuntime (t )
278268
279269 inst , err := rt .Acquire (t .Context ())
280270 if err != nil {
@@ -295,19 +285,7 @@ func TestRealWasm_OneshotAcquire(t *testing.T) {
295285}
296286
297287func TestRealWasm_Fork (t * testing.T ) {
298- if testing .Short () {
299- t .Skip ("skip real wasm test in -short mode" )
300- }
301-
302- stdlib , err := cpython .WithStdlib ()
303- if err != nil {
304- t .Fatal (err )
305- }
306- rt , err := sango .New (t .Context (), cpython .Wasm (), cpython .CPython (), sango .WithWASI (), stdlib )
307- if err != nil {
308- t .Fatal (err )
309- }
310- defer rt .Close (t .Context ())
288+ rt := newExtRuntime (t )
311289
312290 ctx := t .Context ()
313291
@@ -439,15 +417,21 @@ func evalOK(t *testing.T, inst *sango.Instance, code string) string {
439417 return string (res .Value )
440418}
441419
420+ var sharedExtRuntime = sync .OnceValues (func () (* sango.Runtime , error ) {
421+ opt , err := cpython .WithStdlib ()
422+ if err != nil {
423+ return nil , err
424+ }
425+ return sango .New (context .Background (), cpython .Wasm (), cpython .CPython (),
426+ sango .WithWASI (), opt )
427+ })
428+
442429func newExtRuntime (t * testing.T ) * sango.Runtime {
443430 t .Helper ()
444- stdlib , _ := cpython .WithStdlib ()
445- rt , err := sango .New (t .Context (), cpython .Wasm (), cpython .CPython (),
446- sango .WithWASI (), stdlib )
431+ rt , err := sharedExtRuntime ()
447432 if err != nil {
448433 t .Fatal (err )
449434 }
450- t .Cleanup (func () { rt .Close (t .Context ()) })
451435 return rt
452436}
453437
@@ -511,11 +495,153 @@ func TestNumpy_Works(t *testing.T) {
511495 t .Log (evalOK (t , fork , `str(a.sum())` ))
512496}
513497
514- func TestNumpy_FFT (t * testing.T ) {
515- if testing .Short () {
516- t .Skip ("skip real wasm test in -short mode" )
498+ func TestPandas_Works (t * testing.T ) {
499+ rt := newExtRuntime (t )
500+ inst , err := rt .Acquire (t .Context ())
501+ if err != nil {
502+ t .Fatal (err )
517503 }
504+ defer inst .Release ()
505+
506+ t .Log (evalOK (t , inst , `
507+ import traceback
508+ try:
509+ pd.Timestamp("2024-01-01").tz_localize("Asia/Tokyo")
510+ _r = "ok"
511+ except Exception:
512+ _r = traceback.format_exc()
513+ _r
514+ ` ))
515+
516+ t .Log (evalOK (t , inst , `
517+ import os
518+ _r = ""
519+ for p in ("/lib/python3.13/pytz/zoneinfo/Asia/Tokyo",
520+ "/lib/python3.13/tzdata/zoneinfo/Asia/Tokyo"):
521+ _r += p + " exists=" + str(os.path.exists(p))
522+ try:
523+ f = open(p, "rb")
524+ _r += " seekable=" + str(f.seekable())
525+ f.close()
526+ except Exception as e:
527+ _r += " err=" + repr(e)
528+ _r += "\n"
529+ _r
530+ ` ))
531+
532+ t .Log (evalOK (t , inst , `import pandas as pd; pd.__version__` ))
533+
534+ t .Run ("low visibility ext modules" , func (t * testing.T ) {
535+ evalOK (t , inst , `import pandas._libs.pandas_datetime` )
536+ evalOK (t , inst , `import pandas._libs.pandas_parser` )
537+ evalOK (t , inst , `import pandas._libs.window.aggregations` )
538+ evalOK (t , inst , `import pandas._libs.tslibs.np_datetime` )
539+ })
540+
541+ t .Run ("dataframe basics" , func (t * testing.T ) {
542+ got := evalOK (t , inst , `str(pd.DataFrame({"a": [1, 2, 3]})["a"].sum())` )
543+ if got != "'6'" {
544+ t .Fatalf ("got %s, want '6'" , got )
545+ }
546+ })
547+
548+ t .Run ("groupby" , func (t * testing.T ) {
549+ got := evalOK (t , inst , `str(pd.DataFrame(` +
550+ `{"k": ["x", "x", "y"], "v": [1, 2, 3]}` +
551+ `).groupby("k")["v"].sum()["x"])` )
552+ if got != "'3'" {
553+ t .Fatalf ("got %s, want '3'" , got )
554+ }
555+ })
556+
557+ t .Run ("read_csv" , func (t * testing.T ) {
558+ got := evalOK (t , inst ,
559+ `import io; str(pd.read_csv(io.StringIO("a,b\n1,2\n3,4"))["b"].sum())` )
560+ if got != "'6'" {
561+ t .Fatalf ("got %s, want '6'" , got )
562+ }
563+ })
518564
565+ t .Run ("rolling window" , func (t * testing.T ) {
566+ got := evalOK (t , inst , `str(pd.Series([1, 2, 3]).rolling(2).sum()[2])` )
567+ if got != "'5.0'" {
568+ t .Fatalf ("got %s, want '5.0'" , got )
569+ }
570+ })
571+
572+ t .Run ("to_json" , func (t * testing.T ) {
573+ got := evalOK (t , inst , `pd.DataFrame({"a": [1]}).to_json()` )
574+ if got != `'{"a":{"0":1}}'` {
575+ t .Fatalf ("got %s" , got )
576+ }
577+ })
578+
579+ t .Run ("datetime" , func (t * testing.T ) {
580+ got := evalOK (t , inst , `str(pd.to_datetime("2024-03-15").day)` )
581+ if got != "'15'" {
582+ t .Fatalf ("got %s, want '15'" , got )
583+ }
584+ })
585+
586+ t .Run ("timezone" , func (t * testing.T ) {
587+ got := evalOK (t , inst ,
588+ `str(pd.Timestamp("2024-01-01").tz_localize("Asia/Tokyo").tz)` )
589+ if got != "'Asia/Tokyo'" {
590+ t .Fatalf ("got %s, want 'Asia/Tokyo' (tzdata is probably missing)" , got )
591+ }
592+ })
593+
594+ t .Run ("guest error is a value, not a crash" , func (t * testing.T ) {
595+ res , err := inst .Eval (t .Context (), []byte (`pd.DataFrame({"a": [1]})["missing"]` ))
596+ if err != nil {
597+ t .Fatalf ("infra error (instance died?): %v" , err )
598+ }
599+ if res .OK () {
600+ t .Fatalf ("expected KeyError, got %q" , res .Value )
601+ }
602+ if len (res .Err .Message ) == 0 {
603+ t .Fatal ("empty error message" )
604+ }
605+ t .Logf ("KeyError (expected): %s" , res .Err )
606+ })
607+
608+ t .Run ("instance survives a thrown exception" , func (t * testing.T ) {
609+ got := evalOK (t , inst , `str(pd.DataFrame({"a": [1, 2]})["a"].sum())` )
610+ if got != "'3'" {
611+ t .Fatalf ("instance broken after exception: got %s" , got )
612+ }
613+ })
614+
615+ t .Run ("snapshot and restore" , func (t * testing.T ) {
616+ ctx := t .Context ()
617+
618+ if _ , err := inst .Eval (ctx , []byte (
619+ `df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})` )); err != nil {
620+ t .Fatal (err )
621+ }
622+
623+ snap , err := rt .Snapshot (inst )
624+ if err != nil {
625+ t .Fatal (err )
626+ }
627+ fork , err := rt .Restore (ctx , snap )
628+ if err != nil {
629+ t .Fatal (err )
630+ }
631+ defer fork .Release ()
632+
633+ got := evalOK (t , fork , `str(df["b"].sum())` )
634+ if got != "'15'" {
635+ t .Fatalf ("got %s, want '15'" , got )
636+ }
637+
638+ if got := evalOK (t , fork , `str("pandas" in __import__("sys").modules)` ); got != "'True'" {
639+ t .Fatalf ("pandas not in restored sys.modules: %s" , got )
640+ }
641+ })
642+ }
643+
644+ func TestNumpy_FFT (t * testing.T ) {
519645 rt := newExtRuntime (t )
520646 inst , err := rt .Acquire (t .Context ())
521647 if err != nil {
0 commit comments