Merge branch 'v5-feat' into 'v5'

feat-android-screenrecord

See merge request iesqa/httprunner!68
This commit is contained in:
李隆
2025-03-07 03:04:07 +00:00
15 changed files with 341 additions and 51 deletions
+1 -1
View File
@@ -1 +1 @@
v5.0.0-beta-2503052233 v5.0.0-beta-2503062216
+59 -4
View File
@@ -2,6 +2,7 @@ package gadb
import ( import (
"bytes" "bytes"
"context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
@@ -147,15 +148,28 @@ func (d *Device) Usb() (string, error) {
return "", errors.New("does not have attribute: usb") return "", errors.New("does not have attribute: usb")
} }
func (d *Device) SystemVersion() (string, error) {
if d.HasAttribute("systemVersion") {
return d.attrs["systemVersion"], nil
}
systemVersion, err := d.RunShellCommand("getprop", "ro.build.version.release")
systemVersion = strings.TrimSpace(systemVersion)
if err != nil {
return "", errors.New("get android system version failed")
}
d.attrs["systemVersion"] = systemVersion
return systemVersion, nil
}
func (d *Device) SdkVersion() (string, error) { func (d *Device) SdkVersion() (string, error) {
if d.HasAttribute("sdkVersion") { if d.HasAttribute("sdkVersion") {
return d.attrs["sdkVersion"], nil return d.attrs["sdkVersion"], nil
} }
sdkVersion, err := d.RunShellCommand("getprop", "ro.build.version.sdk") sdkVersion, err := d.RunShellCommand("getprop", "ro.build.version.sdk")
sdkVersion = strings.TrimSpace(sdkVersion)
if err != nil { if err != nil {
return "", errors.New("does not have attribute: sdkVersion") return "", errors.New("get android sdk version failed")
} }
sdkVersion = strings.TrimSpace(sdkVersion)
d.attrs["sdkVersion"] = sdkVersion d.attrs["sdkVersion"] = sdkVersion
return sdkVersion, nil return sdkVersion, nil
} }
@@ -732,7 +746,7 @@ func (d *Device) ScreenCap() ([]byte, error) {
time.Now().Unix()) time.Now().Unix())
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath) _, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "screencap failed")
} }
// remove temp file // remove temp file
@@ -742,5 +756,46 @@ func (d *Device) ScreenCap() ([]byte, error) {
buffer := bytes.NewBuffer(nil) buffer := bytes.NewBuffer(nil)
err = d.Pull(tempPath, buffer) err = d.Pull(tempPath, buffer)
return buffer.Bytes(), err if err != nil {
return nil, errors.Wrap(err, "pull video failed")
}
return buffer.Bytes(), nil
}
func (d *Device) ScreenRecord(ctx context.Context) ([]byte, error) {
videoPath := fmt.Sprintf("/sdcard/screenrecord_%d.mp4", time.Now().Unix())
done := make(chan error, 1)
go func() {
_, err := d.RunShellCommandWithBytes("screenrecord", videoPath)
done <- err
}()
select {
case <-ctx.Done():
// timeout or cancelled
pid, err := d.RunShellCommand("pidof", "screenrecord")
if err == nil && pid != "" {
// 发送 SIGINT 信号终止录屏
_, _ = d.RunShellCommand("kill", "-2", strings.TrimSpace(pid))
}
<-done // 等待进程完全退出
case err := <-done:
// adb screenrecord will exit on reached 180s
if err != nil {
return nil, errors.Wrap(err, "screenrecord failed")
}
}
// remove temp file
defer func() {
go d.RunShellCommand("rm", videoPath)
}()
buffer := bytes.NewBuffer(nil)
err := d.Pull(videoPath, buffer)
if err != nil {
return nil, errors.Wrap(err, "pull video failed")
}
return buffer.Bytes(), nil
} }
+53 -3
View File
@@ -4,11 +4,15 @@ package gadb
import ( import (
"bytes" "bytes"
"context"
"os" "os"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
var devices []*Device var devices []*Device
@@ -17,9 +21,7 @@ func setupDevices(t *testing.T) {
var err error var err error
setupClient(t) setupClient(t)
devices, err = adbClient.DeviceList() devices, err = adbClient.DeviceList()
if err != nil { require.Nil(t, err)
t.Fatal(err)
}
} }
func TestDevice_State(t *testing.T) { func TestDevice_State(t *testing.T) {
@@ -119,6 +121,24 @@ func TestDevice_DeviceInfo(t *testing.T) {
} }
} }
func TestDevice_SdkVersion(t *testing.T) {
setupDevices(t)
for _, device := range devices {
sdkVersion, err := device.SdkVersion()
assert.Nil(t, err)
t.Log(device.Serial(), sdkVersion)
}
}
func TestDevice_SystemVersion(t *testing.T) {
setupDevices(t)
for _, device := range devices {
systemVersion, err := device.SystemVersion()
assert.Nil(t, err)
t.Log(device.Serial(), systemVersion)
}
}
func TestDevice_Forward(t *testing.T) { func TestDevice_Forward(t *testing.T) {
setupDevices(t) setupDevices(t)
@@ -276,6 +296,36 @@ func TestDevice_Pull(t *testing.T) {
} }
} }
func TestDevice_ScreenRecord(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
// screen record with time limit 5 seconds
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
if _, err := dev.ScreenRecord(ctx); err != nil {
assert.Nil(t, err)
}
cancel()
}
for _, dev := range devices {
// screen record with cancel signal
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error)
go func() {
_, err := dev.ScreenRecord(ctx)
done <- err
}()
// record for 3 seconds
time.Sleep(time.Second * 3)
cancel()
err := <-done
assert.Nil(t, err)
}
}
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) { func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
type fields struct { type fields struct {
adbClient Client adbClient Client
+1 -1
View File
@@ -37,7 +37,7 @@ func (r *Router) screenResultHandler(c *gin.Context) {
var actionOptions []option.ActionOption var actionOptions []option.ActionOption
if screenReq.Options != nil { if screenReq.Options != nil {
actionOptions = screenReq.Options.Options() actionOptions = screenReq.Options.GetScreenShotOptions()
} }
screenResult, err := driver.GetScreenResult(actionOptions...) screenResult, err := driver.GetScreenResult(actionOptions...)
+98 -24
View File
@@ -3,6 +3,7 @@ package uixt
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
@@ -779,54 +780,127 @@ func (ad *ADBDriver) GetIme() (ime string, err error) {
return currentIme, nil return currentIme, nil
} }
func (ad *ADBDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) options := option.NewActionOptions(opts...)
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
file, err := os.Create(fileName) var filePath string
if err != nil { if options.ScreenRecordPath != "" {
log.Error().Err(err) filePath = options.ScreenRecordPath
return "", err } else {
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
} }
var ctx context.Context
if options.Context != nil {
ctx = options.Context
} else {
ctx = context.Background()
}
var cancel context.CancelFunc
duration := options.ScreenRecordDuration
if duration == 0 {
duration = options.Duration
}
if duration != 0 {
ctx, cancel = context.WithTimeout(ctx,
time.Duration(duration*float64(time.Second)))
} else {
ctx, cancel = context.WithCancel(ctx)
}
defer cancel()
// get android system version
var sysVersion int
if systemVersion, err := ad.Device.SystemVersion(); err == nil {
if version, err := strconv.Atoi(systemVersion); err == nil {
sysVersion = version
}
}
if sysVersion == 0 {
log.Warn().Err(err).Msg("get android system version failed")
}
var useAdbScreenRecord bool
audioOn := options.ScreenRecordWithAudio
if options.ScreenRecordWithScrcpy {
useAdbScreenRecord = false
} else if !audioOn {
log.Info().Bool("audioOn", audioOn).Msg("screen record with adb screenrecord by default")
useAdbScreenRecord = true
} else if sysVersion != 0 && sysVersion < 11 {
// scrcpy audio forwarding is supported for devices with Android 11 or higher
// https://github.com/Genymobile/scrcpy/blob/master/doc/audio.md
log.Warn().Bool("audioOn", audioOn).Int("version", sysVersion).
Msg("Audio disabled, it is only supported for Android >= 11, use adb screenrecord")
useAdbScreenRecord = true
}
defer func() { defer func() {
_ = file.Close() if err == nil {
filePath, err = filepath.Abs(filePath)
if err != nil {
err = errors.Wrap(err, "get absolute path failed")
} else {
log.Info().Str("path", filePath).Msg("screen record success")
}
}
}() }()
// scrcpy -s 7d21bb91 --record=file.mp4 -N if useAdbScreenRecord {
// screen record with adb screenrecord
// adb screenrecord duration is limited in range [1,180] seconds
res, err := ad.Device.ScreenRecord(ctx)
if err != nil {
return "", errors.Wrap(err, "screen record failed")
}
if err := os.WriteFile(filePath, res, 0o644); err != nil {
return "", errors.Wrap(err, "write screen record file failed")
}
return filePath, nil
}
// screen record with scrcpy
log.Info().Float64("duration(s)", duration).Msg("screen record with scrcpy")
// start scrcpy
cmd := exec.Command( cmd := exec.Command(
"scrcpy", "scrcpy",
"-s", ad.Device.Serial(), "-s", ad.Device.Serial(),
fmt.Sprintf("--record=%s", fileName), fmt.Sprintf("--record=%s", filePath),
"-N", "--record-format=mp4",
"--max-fps=30",
"--no-playback", // Disable video and audio playback on the computer
) )
cmd.Stdout = io.Discard cmd.Stdout = io.Discard
cmd.Stderr = io.Discard cmd.Stderr = io.Discard
// 启动命令
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Error().Err(err) return "", errors.Wrap(err, "start screen record failed")
return "", err
} }
timer := time.After(duration)
done := make(chan error) done := make(chan error, 1)
go func() { go func() {
// 等待 ffmpeg 命令执行完毕
done <- cmd.Wait() done <- cmd.Wait()
}() }()
select { select {
case <-timer: case <-ctx.Done():
// 超时,停止 scrcpy 进程 // timeout or cancelled
log.Info().Msg("screen recording stopped")
if err := cmd.Process.Signal(syscall.SIGINT); err != nil { if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
log.Error().Err(err) log.Error().Err(err).Msg("failed to stop scrcpy process")
_ = cmd.Process.Kill() // 强制结束进程
} }
<-done // 等待进程完全退出
case err := <-done: case err := <-done:
// ffmpeg 正常结束 log.Info().Msg("scrcpy exited")
if err != nil { if err != nil {
log.Error().Err(err) return "", errors.Wrap(err, "screen record with scrcpy failed")
return "", err
} }
} }
return filepath.Abs(fileName)
return filePath, nil
} }
func (ad *ADBDriver) Setup() error { func (ad *ADBDriver) Setup() error {
+46 -3
View File
@@ -3,6 +3,7 @@
package uixt package uixt
import ( import (
"context"
"os" "os"
"testing" "testing"
"time" "time"
@@ -195,10 +196,52 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) {
func TestDriver_ADB_ScreenRecord(t *testing.T) { func TestDriver_ADB_ScreenRecord(t *testing.T) {
driver := setupADBDriverExt(t) driver := setupADBDriverExt(t)
path, err := driver.ScreenRecord(5 * time.Second)
// adb screenrecord --time-limit 5
path1, err := driver.ScreenRecord(
option.WithScreenRecordDuation(5))
assert.Nil(t, err)
defer os.Remove(path1)
t.Log(path1)
// scrcpy with time limit
path2, err := driver.ScreenRecord(
option.WithScreenRecordDuation(5),
option.WithScreenRecordAudio(true),
)
assert.Nil(t, err)
defer os.Remove(path2)
t.Log(path2)
// scrcpy with time limit
path3, err := driver.ScreenRecord(
option.WithScreenRecordDuation(5),
option.WithScreenRecordScrcpy(true),
)
assert.Nil(t, err)
defer os.Remove(path3)
t.Log(path3)
// scrcpy with cancel signal
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error)
go func() {
path4, err := driver.ScreenRecord(
option.WithContext(ctx),
option.WithScreenRecordScrcpy(true),
)
assert.Nil(t, err)
defer os.Remove(path4)
t.Log(path4)
done <- err
}()
// record for 3 seconds
time.Sleep(time.Second * 3)
cancel()
err = <-done
assert.Nil(t, err) assert.Nil(t, err)
defer os.Remove(path)
t.Log(path)
} }
func TestDriver_ADB_Backspace(t *testing.T) { func TestDriver_ADB_Backspace(t *testing.T) {
+1 -1
View File
@@ -614,7 +614,7 @@ func (wd *BrowserDriver) GetSession() *DriverSession {
return nil return nil
} }
func (wd *BrowserDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (wd *BrowserDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
return return
} }
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
_ "image/gif" _ "image/gif"
_ "image/png" _ "image/png"
"time"
"github.com/httprunner/httprunner/v5/uixt/ai" "github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
@@ -37,7 +36,7 @@ type IDriver interface {
ForegroundInfo() (app types.AppInfo, err error) ForegroundInfo() (app types.AppInfo, err error)
WindowSize() (types.Size, error) WindowSize() (types.Size, error)
ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error) ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error)
ScreenRecord(duration time.Duration) (videoPath string, err error) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error)
Source(srcOpt ...option.SourceOption) (string, error) Source(srcOpt ...option.SourceOption) (string, error)
Orientation() (orientation types.Orientation, err error) Orientation() (orientation types.Orientation, err error)
Rotation() (rotation types.Rotation, err error) Rotation() (rotation types.Rotation, err error)
+1 -1
View File
@@ -273,7 +273,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
case ACTION_ScreenShot: case ACTION_ScreenShot:
// take screenshot // take screenshot
log.Info().Msg("take screenshot for current screen") log.Info().Msg("take screenshot for current screen")
_, err := dExt.GetScreenResult(action.GetScreenOptions()...) _, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
return err return err
case ACTION_ClosePopups: case ACTION_ClosePopups:
return dExt.ClosePopupsHandler() return dExt.ClosePopupsHandler()
+2 -2
View File
@@ -364,11 +364,11 @@ func (s *StubIOSDriver) WindowSize() (types.Size, error) {
return s.WDADriver.WindowSize() return s.WDADriver.WindowSize()
} }
func (s *StubIOSDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (s *StubIOSDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
if err := s.SetupWda(); err != nil { if err := s.SetupWda(); err != nil {
return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error()) return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error())
} }
return s.WDADriver.ScreenRecord(duration) return s.WDADriver.ScreenRecord(opts...)
} }
func (s *StubIOSDriver) Orientation() (types.Orientation, error) { func (s *StubIOSDriver) Orientation() (types.Orientation, error) {
+1 -1
View File
@@ -253,7 +253,7 @@ func (hd *HDCDriver) StopCaptureLog() (result interface{}, err error) {
return hd.points, nil return hd.points, nil
} }
func (hd *HDCDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (hd *HDCDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
return "", nil return "", nil
} }
+4 -1
View File
@@ -875,10 +875,13 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
return wd.Session.POST(data, "/gtf/automation/log") return wd.Session.POST(data, "/gtf/automation/log")
} }
func (wd *WDADriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (wd *WDADriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
options := option.NewActionOptions(opts...)
duration := time.Duration(options.Duration * float64(time.Second))
file, err := os.Create(fileName) file, err := os.Create(fileName)
if err != nil { if err != nil {
fmt.Println("Error creating file:", err) fmt.Println("Error creating file:", err)
+1 -1
View File
@@ -302,7 +302,7 @@ func TestDriver_WDA_AccessibleSource(t *testing.T) {
func TestDriver_WDA_ScreenRecord(t *testing.T) { func TestDriver_WDA_ScreenRecord(t *testing.T) {
driver := setupWDADriverExt(t) driver := setupWDADriverExt(t)
path, err := driver.ScreenRecord(5 * time.Second) path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
assert.Nil(t, err) assert.Nil(t, err)
t.Log(path) t.Log(path)
} }
+14 -4
View File
@@ -1,12 +1,14 @@
package option package option
import ( import (
"context"
"math/rand/v2" "math/rand/v2"
"github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/builtin"
) )
type ActionOptions struct { type ActionOptions struct {
Context context.Context
// log // log
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
@@ -33,6 +35,9 @@ func (o *ActionOptions) Options() []ActionOption {
return options return options
} }
if o.Context != nil {
options = append(options, WithContext(o.Context))
}
if o.Identifier != "" { if o.Identifier != "" {
options = append(options, WithIdentifier(o.Identifier)) options = append(options, WithIdentifier(o.Identifier))
} }
@@ -120,11 +125,10 @@ func (o *ActionOptions) Options() []ActionOption {
} }
} }
return options options = append(options, o.GetScreenShotOptions()...)
} options = append(options, o.GetScreenRecordOptions()...)
func (o *ActionOptions) GetScreenOptions() []ActionOption { return options
return o.ScreenOptions.Options()
} }
func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) { func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) {
@@ -207,6 +211,12 @@ func NewActionOptions(opts ...ActionOption) *ActionOptions {
type ActionOption func(o *ActionOptions) type ActionOption func(o *ActionOptions)
func WithContext(ctx context.Context) ActionOption {
return func(o *ActionOptions) {
o.Context = ctx
}
}
func WithCustomOption(key string, value interface{}) ActionOption { func WithCustomOption(key string, value interface{}) ActionOption {
return func(o *ActionOptions) { return func(o *ActionOptions) {
if o.Custom == nil { if o.Custom == nil {
+58 -2
View File
@@ -1,9 +1,12 @@
package option package option
import "github.com/httprunner/httprunner/v5/uixt/types" import (
"github.com/httprunner/httprunner/v5/uixt/types"
)
type ScreenOptions struct { type ScreenOptions struct {
ScreenShotOptions ScreenShotOptions
ScreenRecordOptions
ScreenFilterOptions ScreenFilterOptions
} }
@@ -18,7 +21,7 @@ type ScreenShotOptions struct {
ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"` ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"`
} }
func (o *ScreenShotOptions) Options() []ActionOption { func (o *ScreenShotOptions) GetScreenShotOptions() []ActionOption {
options := make([]ActionOption, 0) options := make([]ActionOption, 0)
if o == nil { if o == nil {
return options return options
@@ -125,6 +128,59 @@ func WithScreenShotFileName(fileName string) ActionOption {
} }
} }
type ScreenRecordOptions struct {
ScreenRecordDuration float64 `json:"screenrecord_duration,omitempty" yaml:"screenrecord_duration,omitempty"`
ScreenRecordWithAudio bool `json:"screenrecord_with_audio,omitempty" yaml:"screenrecord_with_audio,omitempty"`
ScreenRecordWithScrcpy bool `json:"screenrecord_with_scrcpy,omitempty" yaml:"screenrecord_with_scrcpy,omitempty"`
ScreenRecordPath string `json:"screenrecord_path,omitempty" yaml:"screenrecord_path,omitempty"`
}
func (o *ScreenRecordOptions) GetScreenRecordOptions() []ActionOption {
options := make([]ActionOption, 0)
if o == nil {
return options
}
// screen record options
if o.ScreenRecordDuration > 0 {
options = append(options, WithDuration(o.ScreenRecordDuration))
}
if o.ScreenRecordWithAudio {
options = append(options, WithScreenRecordAudio(true))
}
if o.ScreenRecordWithScrcpy {
options = append(options, WithScreenRecordScrcpy(true))
}
if o.ScreenRecordPath != "" {
options = append(options, WithScreenRecordPath(o.ScreenRecordPath))
}
return options
}
func WithScreenRecordDuation(duration float64) ActionOption {
return func(o *ActionOptions) {
o.ScreenRecordDuration = duration
}
}
func WithScreenRecordAudio(audioOn bool) ActionOption {
return func(o *ActionOptions) {
o.ScreenRecordWithAudio = audioOn
}
}
func WithScreenRecordScrcpy(scrcpyOn bool) ActionOption {
return func(o *ActionOptions) {
o.ScreenRecordWithScrcpy = scrcpyOn
}
}
func WithScreenRecordPath(path string) ActionOption {
return func(o *ActionOptions) {
o.ScreenRecordPath = path
}
}
// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner // (x1, y1) is the top left corner, (x2, y2) is the bottom right corner
// [x1, y1, x2, y2] in percentage of the screen // [x1, y1, x2, y2] in percentage of the screen
type Scope []float64 type Scope []float64