642 lines
12 KiB
Go
642 lines
12 KiB
Go
|
package eventloop
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"sync/atomic"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"apigo.cc/ai/ai/goja"
|
||
|
|
||
|
"go.uber.org/goleak"
|
||
|
)
|
||
|
|
||
|
func TestRun(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
var calledAt;
|
||
|
setTimeout(function() {
|
||
|
calledAt = now();
|
||
|
}, 1000);
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
startTime := time.Now()
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
vm.Set("now", time.Now)
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
var calledAt time.Time
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if calledAt.IsZero() {
|
||
|
t.Fatal("Not called")
|
||
|
}
|
||
|
if dur := calledAt.Sub(startTime); dur < time.Second {
|
||
|
t.Fatal(dur)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestStart(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
var calledAt;
|
||
|
setTimeout(function() {
|
||
|
calledAt = now();
|
||
|
}, 1000);
|
||
|
`
|
||
|
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
startTime := time.Now()
|
||
|
loop.Start()
|
||
|
|
||
|
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||
|
vm.Set("now", time.Now)
|
||
|
vm.RunProgram(prg)
|
||
|
})
|
||
|
|
||
|
time.Sleep(2 * time.Second)
|
||
|
if remainingJobs := loop.Stop(); remainingJobs != 0 {
|
||
|
t.Fatal(remainingJobs)
|
||
|
}
|
||
|
|
||
|
var calledAt time.Time
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if calledAt.IsZero() {
|
||
|
t.Fatal("Not called")
|
||
|
}
|
||
|
if dur := calledAt.Sub(startTime); dur < time.Second {
|
||
|
t.Fatal(dur)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestStartInForeground(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
var calledAt;
|
||
|
setTimeout(function() {
|
||
|
calledAt = now();
|
||
|
}, 1000);
|
||
|
`
|
||
|
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
startTime := time.Now()
|
||
|
go loop.StartInForeground()
|
||
|
|
||
|
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||
|
vm.Set("now", time.Now)
|
||
|
vm.RunProgram(prg)
|
||
|
})
|
||
|
|
||
|
time.Sleep(2 * time.Second)
|
||
|
if remainingJobs := loop.Stop(); remainingJobs != 0 {
|
||
|
t.Fatal(remainingJobs)
|
||
|
}
|
||
|
|
||
|
var calledAt time.Time
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if calledAt.IsZero() {
|
||
|
t.Fatal("Not called")
|
||
|
}
|
||
|
if dur := calledAt.Sub(startTime); dur < time.Second {
|
||
|
t.Fatal(dur)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestInterval(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
var count = 0;
|
||
|
var t = setInterval(function(times) {
|
||
|
console.log("tick");
|
||
|
if (++count > times) {
|
||
|
clearInterval(t);
|
||
|
}
|
||
|
}, 1000, 2);
|
||
|
console.log("Started");
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
var count int64
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
count = vm.Get("count").ToInteger()
|
||
|
})
|
||
|
if count != 3 {
|
||
|
t.Fatal(count)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestImmediate(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
let log = [];
|
||
|
function cb(arg) {
|
||
|
log.push(arg);
|
||
|
}
|
||
|
var i;
|
||
|
var t = setImmediate(function() {
|
||
|
cb("tick");
|
||
|
setImmediate(cb, "tick 2");
|
||
|
i = setImmediate(cb, "should not run")
|
||
|
});
|
||
|
setImmediate(function() {
|
||
|
clearImmediate(i);
|
||
|
});
|
||
|
cb("Started");
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunString(`
|
||
|
if (log.length != 3) {
|
||
|
throw new Error("Invalid log length: " + log);
|
||
|
}
|
||
|
if (log[0] !== "Started" || log[1] !== "tick" || log[2] !== "tick 2") {
|
||
|
throw new Error("Invalid log: " + log);
|
||
|
}
|
||
|
`)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRunNoSchedule(t *testing.T) {
|
||
|
loop := NewEventLoop()
|
||
|
fired := false
|
||
|
loop.Run(func(vm *goja.Runtime) { // should not hang
|
||
|
fired = true
|
||
|
// do not schedule anything
|
||
|
})
|
||
|
|
||
|
if !fired {
|
||
|
t.Fatal("Not fired")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRunWithConsole(t *testing.T) {
|
||
|
const SCRIPT = `
|
||
|
console.log("Started");
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal("Call to console.log generated an error", err)
|
||
|
}
|
||
|
|
||
|
loop = NewEventLoop(EnableConsole(true))
|
||
|
prg, err = goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal("Call to console.log generated an error", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRunNoConsole(t *testing.T) {
|
||
|
const SCRIPT = `
|
||
|
console.log("Started");
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop(EnableConsole(false))
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err == nil {
|
||
|
t.Fatal("Call to console.log did not generate an error", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestClearIntervalRace(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
console.log("calling setInterval");
|
||
|
var t = setInterval(function() {
|
||
|
console.log("tick");
|
||
|
}, 500);
|
||
|
console.log("calling sleep");
|
||
|
sleep(2000);
|
||
|
console.log("calling clearInterval");
|
||
|
clearInterval(t);
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
// Should not hang
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
vm.Set("sleep", func(ms int) {
|
||
|
<-time.After(time.Duration(ms) * time.Millisecond)
|
||
|
})
|
||
|
vm.RunProgram(prg)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func TestNativeTimeout(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
fired := false
|
||
|
loop := NewEventLoop()
|
||
|
loop.SetTimeout(func(*goja.Runtime) {
|
||
|
fired = true
|
||
|
}, 1*time.Second)
|
||
|
loop.Run(func(*goja.Runtime) {
|
||
|
// do not schedule anything
|
||
|
})
|
||
|
if !fired {
|
||
|
t.Fatal("Not fired")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestNativeClearTimeout(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
fired := false
|
||
|
loop := NewEventLoop()
|
||
|
timer := loop.SetTimeout(func(*goja.Runtime) {
|
||
|
fired = true
|
||
|
}, 2*time.Second)
|
||
|
loop.SetTimeout(func(*goja.Runtime) {
|
||
|
loop.ClearTimeout(timer)
|
||
|
}, 1*time.Second)
|
||
|
loop.Run(func(*goja.Runtime) {
|
||
|
// do not schedule anything
|
||
|
})
|
||
|
if fired {
|
||
|
t.Fatal("Cancelled timer fired!")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestNativeInterval(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
count := 0
|
||
|
loop := NewEventLoop()
|
||
|
var i *Interval
|
||
|
i = loop.SetInterval(func(*goja.Runtime) {
|
||
|
t.Log("tick")
|
||
|
count++
|
||
|
if count > 2 {
|
||
|
loop.ClearInterval(i)
|
||
|
}
|
||
|
}, 1*time.Second)
|
||
|
loop.Run(func(*goja.Runtime) {
|
||
|
// do not schedule anything
|
||
|
})
|
||
|
if count != 3 {
|
||
|
t.Fatal("Expected interval to fire 3 times, got", count)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestNativeClearInterval(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
count := 0
|
||
|
loop := NewEventLoop()
|
||
|
loop.Run(func(*goja.Runtime) {
|
||
|
i := loop.SetInterval(func(*goja.Runtime) {
|
||
|
t.Log("tick")
|
||
|
count++
|
||
|
}, 500*time.Millisecond)
|
||
|
<-time.After(2 * time.Second)
|
||
|
loop.ClearInterval(i)
|
||
|
})
|
||
|
if count != 0 {
|
||
|
t.Fatal("Expected interval to fire 0 times, got", count)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestSetAndClearOnStoppedLoop(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
timeout := loop.SetTimeout(func(runtime *goja.Runtime) {
|
||
|
panic("must not run")
|
||
|
}, 1*time.Millisecond)
|
||
|
loop.ClearTimeout(timeout)
|
||
|
loop.Start()
|
||
|
time.Sleep(10 * time.Millisecond)
|
||
|
loop.Terminate()
|
||
|
}
|
||
|
|
||
|
func TestSetTimeoutConcurrent(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
loop.Start()
|
||
|
ch := make(chan struct{}, 1)
|
||
|
loop.SetTimeout(func(*goja.Runtime) {
|
||
|
ch <- struct{}{}
|
||
|
}, 100*time.Millisecond)
|
||
|
<-ch
|
||
|
loop.Stop()
|
||
|
}
|
||
|
|
||
|
func TestClearTimeoutConcurrent(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
loop.Start()
|
||
|
timer := loop.SetTimeout(func(*goja.Runtime) {
|
||
|
}, 100*time.Millisecond)
|
||
|
loop.ClearTimeout(timer)
|
||
|
loop.Stop()
|
||
|
if c := loop.jobCount; c != 0 {
|
||
|
t.Fatalf("jobCount: %d", c)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestClearIntervalConcurrent(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
loop.Start()
|
||
|
ch := make(chan struct{}, 1)
|
||
|
i := loop.SetInterval(func(*goja.Runtime) {
|
||
|
ch <- struct{}{}
|
||
|
}, 500*time.Millisecond)
|
||
|
|
||
|
<-ch
|
||
|
loop.ClearInterval(i)
|
||
|
loop.Stop()
|
||
|
if c := loop.jobCount; c != 0 {
|
||
|
t.Fatalf("jobCount: %d", c)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRunOnStoppedLoop(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
var failed int32
|
||
|
done := make(chan struct{})
|
||
|
go func() {
|
||
|
for atomic.LoadInt32(&failed) == 0 {
|
||
|
loop.Start()
|
||
|
time.Sleep(10 * time.Millisecond)
|
||
|
loop.Stop()
|
||
|
}
|
||
|
}()
|
||
|
go func() {
|
||
|
for atomic.LoadInt32(&failed) == 0 {
|
||
|
loop.RunOnLoop(func(*goja.Runtime) {
|
||
|
if !loop.running {
|
||
|
atomic.StoreInt32(&failed, 1)
|
||
|
close(done)
|
||
|
return
|
||
|
}
|
||
|
})
|
||
|
time.Sleep(10 * time.Millisecond)
|
||
|
}
|
||
|
}()
|
||
|
select {
|
||
|
case <-done:
|
||
|
case <-time.After(5 * time.Second):
|
||
|
}
|
||
|
if atomic.LoadInt32(&failed) != 0 {
|
||
|
t.Fatal("running job on stopped loop")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPromise(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
let result;
|
||
|
const p = new Promise((resolve, reject) => {
|
||
|
setTimeout(() => {resolve("passed")}, 500);
|
||
|
});
|
||
|
p.then(value => {
|
||
|
result = value;
|
||
|
});
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
result := vm.Get("result")
|
||
|
if !result.SameAs(vm.ToValue("passed")) {
|
||
|
err = fmt.Errorf("unexpected result: %v", result)
|
||
|
}
|
||
|
})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPromiseNative(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
let result;
|
||
|
p.then(value => {
|
||
|
result = value;
|
||
|
done();
|
||
|
});
|
||
|
`
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
ch := make(chan error)
|
||
|
loop.Start()
|
||
|
defer loop.Stop()
|
||
|
|
||
|
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||
|
vm.Set("done", func() {
|
||
|
ch <- nil
|
||
|
})
|
||
|
p, resolve, _ := vm.NewPromise()
|
||
|
vm.Set("p", p)
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
if err != nil {
|
||
|
ch <- err
|
||
|
return
|
||
|
}
|
||
|
go func() {
|
||
|
time.Sleep(500 * time.Millisecond)
|
||
|
loop.RunOnLoop(func(*goja.Runtime) {
|
||
|
resolve("passed")
|
||
|
})
|
||
|
}()
|
||
|
})
|
||
|
err = <-ch
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
loop.RunOnLoop(func(vm *goja.Runtime) {
|
||
|
result := vm.Get("result")
|
||
|
if !result.SameAs(vm.ToValue("passed")) {
|
||
|
ch <- fmt.Errorf("unexpected result: %v", result)
|
||
|
} else {
|
||
|
ch <- nil
|
||
|
}
|
||
|
})
|
||
|
err = <-ch
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestEventLoop_StopNoWait(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
loop := NewEventLoop()
|
||
|
var ran int32
|
||
|
loop.Run(func(runtime *goja.Runtime) {
|
||
|
loop.SetTimeout(func(*goja.Runtime) {
|
||
|
atomic.StoreInt32(&ran, 1)
|
||
|
}, 5*time.Second)
|
||
|
|
||
|
loop.SetTimeout(func(*goja.Runtime) {
|
||
|
loop.StopNoWait()
|
||
|
}, 500*time.Millisecond)
|
||
|
})
|
||
|
|
||
|
if atomic.LoadInt32(&ran) != 0 {
|
||
|
t.Fatal("ran != 0")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestEventLoop_ClearRunningTimeout(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
const SCRIPT = `
|
||
|
var called = 0;
|
||
|
let aTimer;
|
||
|
function a() {
|
||
|
if (++called > 5) {
|
||
|
return;
|
||
|
}
|
||
|
if (aTimer) {
|
||
|
clearTimeout(aTimer);
|
||
|
}
|
||
|
console.log("ok");
|
||
|
aTimer = setTimeout(a, 500);
|
||
|
}
|
||
|
a();`
|
||
|
|
||
|
prg, err := goja.Compile("main.js", SCRIPT, false)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
_, err = vm.RunProgram(prg)
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
var called int64
|
||
|
loop.Run(func(vm *goja.Runtime) {
|
||
|
called = vm.Get("called").ToInteger()
|
||
|
})
|
||
|
if called != 6 {
|
||
|
t.Fatal(called)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestEventLoop_Terminate(t *testing.T) {
|
||
|
defer goleak.VerifyNone(t)
|
||
|
|
||
|
loop := NewEventLoop()
|
||
|
loop.Start()
|
||
|
interval := loop.SetInterval(func(vm *goja.Runtime) {}, 10*time.Millisecond)
|
||
|
time.Sleep(500 * time.Millisecond)
|
||
|
loop.ClearInterval(interval)
|
||
|
loop.Terminate()
|
||
|
|
||
|
if loop.SetTimeout(func(*goja.Runtime) {}, time.Millisecond) != nil {
|
||
|
t.Fatal("was able to SetTimeout()")
|
||
|
}
|
||
|
if loop.SetInterval(func(*goja.Runtime) {}, time.Millisecond) != nil {
|
||
|
t.Fatal("was able to SetInterval()")
|
||
|
}
|
||
|
if loop.RunOnLoop(func(*goja.Runtime) {}) {
|
||
|
t.Fatal("was able to RunOnLoop()")
|
||
|
}
|
||
|
|
||
|
ch := make(chan struct{})
|
||
|
loop.Start()
|
||
|
if !loop.RunOnLoop(func(runtime *goja.Runtime) {
|
||
|
close(ch)
|
||
|
}) {
|
||
|
t.Fatal("RunOnLoop() has failed after restart")
|
||
|
}
|
||
|
<-ch
|
||
|
loop.Terminate()
|
||
|
}
|