diff --git a/build.go b/build.go index f04808d4..0ce77a56 100644 --- a/build.go +++ b/build.go @@ -10,11 +10,11 @@ import ( "regexp" "strings" - "github.com/httprunner/funplugin/fungo" - "github.com/httprunner/funplugin/myexec" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/funplugin/fungo" + "github.com/httprunner/funplugin/myexec" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" @@ -107,7 +107,7 @@ func (pt *pluginTemplate) generate(tmpl, output string) error { func (pt *pluginTemplate) generatePy(output string) error { // specify output file path if output == "" { - output = filepath.Join(config.RootDir, PluginPySourceGenFile) + output = filepath.Join(config.GetConfig().RootDir, PluginPySourceGenFile) } else if builtin.IsFolderPathExists(output) { output = filepath.Join(output, PluginPySourceGenFile) } @@ -155,7 +155,7 @@ func (pt *pluginTemplate) generateGo(output string) error { // specify output file path if output == "" { - output = filepath.Join(config.RootDir, PluginHashicorpGoBuiltFile) + output = filepath.Join(config.GetConfig().RootDir, PluginHashicorpGoBuiltFile) } else if builtin.IsFolderPathExists(output) { output = filepath.Join(output, PluginHashicorpGoBuiltFile) } diff --git a/build_test.go b/build_test.go deleted file mode 100644 index 4b22b9ea..00000000 --- a/build_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package hrp - -import ( - "path/filepath" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRun(t *testing.T) { - err := BuildPlugin(tmpl("plugin/debugtalk.go"), "./debugtalk.bin") - if !assert.Nil(t, err) { - t.Fatal() - } - - genDebugTalkPyPath := filepath.Join(tmpl("plugin/"), PluginPySourceGenFile) - err = BuildPlugin(tmpl("plugin/debugtalk.py"), genDebugTalkPyPath) - if !assert.Nil(t, err) { - t.Fatal() - } - - contentBytes, err := readFile(genDebugTalkPyPath) - if !assert.Nil(t, err) { - t.Fatal() - } - - content := string(contentBytes) - if !assert.Contains(t, content, "import funppy") { - t.Fatal() - } - - if !assert.Contains(t, content, "funppy.register") { - t.Fatal() - } - - reg, _ := regexp.Compile(`funppy\.register`) - matchedSlice := reg.FindAllStringSubmatch(content, -1) - if !assert.Len(t, matchedSlice, 10) { - t.Fatal() - } -} - -func TestFindAllPythonFunctionNames(t *testing.T) { - content := ` -def test_1(): # exported function - pass - -def _test_2(): # exported function - pass - -def __test_3(): # private function - pass - -# def test_4(): # commented out function -# pass - -def Test5(): # exported function - pass -` - names, err := regexPyFunctionName.findAllFunctionNames(content) - if !assert.Nil(t, err) { - t.FailNow() - } - if !assert.Contains(t, names, "test_1") { - t.FailNow() - } - if !assert.Contains(t, names, "Test5") { - t.FailNow() - } - if !assert.Contains(t, names, "_test_2") { - t.FailNow() - } - if !assert.NotContains(t, names, "__test_3") { - t.FailNow() - } - // commented out function - if !assert.NotContains(t, names, "test_4") { - t.FailNow() - } -} - -func TestFindAllGoFunctionNames(t *testing.T) { - content := ` -func Test1() { // exported function - return -} - -func testFunc2() { // exported function - return -} - -func init() { // private function - return -} - -func _Test3() { // exported function - return -} - -// func Test4() { // commented out function -// return -// } -` - names, err := regexGoFunctionName.findAllFunctionNames(content) - if !assert.Nil(t, err) { - t.FailNow() - } - if !assert.Contains(t, names, "Test1") { - t.FailNow() - } - if !assert.Contains(t, names, "testFunc2") { - t.FailNow() - } - if !assert.NotContains(t, names, "init") { - t.FailNow() - } - if !assert.Contains(t, names, "_Test3") { - t.FailNow() - } - // commented out function - if !assert.NotContains(t, names, "Test4") { - t.FailNow() - } -} - -func TestFindAllGoFunctionNamesAbnormal(t *testing.T) { - content := ` -func init() { // private function - return -} - -func main() { // should not define main() function - return -} -` - _, err := regexGoFunctionName.findAllFunctionNames(content) - if !assert.NotNil(t, err) { - t.FailNow() - } -} diff --git a/cmd/adb/init.go b/cmd/adb/init.go index 508e2274..d7f495e5 100644 --- a/cmd/adb/init.go +++ b/cmd/adb/init.go @@ -3,8 +3,8 @@ package adb import ( "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var serial string diff --git a/cmd/adb/install.go b/cmd/adb/install.go index a5f1e885..16677076 100644 --- a/cmd/adb/install.go +++ b/cmd/adb/install.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var ( diff --git a/cmd/convert.go b/cmd/convert.go index df6d868d..8695d6b1 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -4,14 +4,14 @@ import ( "os" "path/filepath" - "github.com/httprunner/funplugin/myexec" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/httprunner/funplugin/myexec" "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/convert" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/convert" ) var convertCmd = &cobra.Command{ diff --git a/cmd/ios/apps.go b/cmd/ios/apps.go index 5c5285b0..6c49c32b 100644 --- a/cmd/ios/apps.go +++ b/cmd/ios/apps.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" ) type Application struct { diff --git a/cmd/ios/devices.go b/cmd/ios/devices.go index f9718f32..e9d7bcfa 100644 --- a/cmd/ios/devices.go +++ b/cmd/ios/devices.go @@ -12,7 +12,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" ) type Device struct { diff --git a/cmd/ios/init.go b/cmd/ios/init.go index b91ea164..f146f3c6 100644 --- a/cmd/ios/init.go +++ b/cmd/ios/init.go @@ -3,8 +3,8 @@ package ios import ( "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var iosRootCmd = &cobra.Command{ diff --git a/cmd/ios/install.go b/cmd/ios/install.go index f398db80..5401d093 100644 --- a/cmd/ios/install.go +++ b/cmd/ios/install.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var installCmd = &cobra.Command{ diff --git a/cmd/ios/tunnel.go b/cmd/ios/tunnel.go new file mode 100644 index 00000000..fd4720e8 --- /dev/null +++ b/cmd/ios/tunnel.go @@ -0,0 +1,40 @@ +package ios + +import ( + "context" + "os" + "strings" + "time" + + "github.com/danielpaulus/go-ios/ios" + "github.com/httprunner/httprunner/v5/internal/sdk" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var tunnelCmd = &cobra.Command{ + Use: "tunnel", + Short: "tunnel start", + RunE: func(cmd *cobra.Command, args []string) (err error) { + startTime := time.Now() + defer func() { + sdk.SendGA4Event("hrp_ios_tunnel", map[string]interface{}{ + "args": strings.Join(args, "-"), + "success": err == nil, + "engagement_time_msec": time.Since(startTime).Milliseconds(), + }) + }() + ctx := context.TODO() + err = uixt.StartTunnel(ctx, os.TempDir(), ios.HttpApiPort(), true) + if err != nil { + log.Error().Err(err).Msg("failed to start tunnel") + } + <-ctx.Done() + return err + }, +} + +func init() { + iosRootCmd.AddCommand(tunnelCmd) +} diff --git a/cmd/ios/uninstall.go b/cmd/ios/uninstall.go index e2f12780..14ace0ac 100644 --- a/cmd/ios/uninstall.go +++ b/cmd/ios/uninstall.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var uninstallCmd = &cobra.Command{ diff --git a/cmd/server.go b/cmd/server.go index 053fd3c8..052e3739 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -14,6 +14,7 @@ var serverCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return server_ext.NewExtRouter().Run(port) + // return server.NewRouter().Run(port) }, } diff --git a/compat.go b/compat.go index 0975e966..22976841 100644 --- a/compat.go +++ b/compat.go @@ -8,8 +8,8 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) // ConvertCaseCompatibility converts TestCase compatible with Golang engine style diff --git a/config.go b/config.go index c32ee5f1..d5dd7139 100644 --- a/config.go +++ b/config.go @@ -4,8 +4,8 @@ import ( "reflect" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) type IConfig interface { @@ -80,7 +80,7 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig { } // SetThinkTime sets think time config for current testcase. -func (c *TConfig) SetThinkTime(strategy thinkTimeStrategy, cfg interface{}, limit float64) *TConfig { +func (c *TConfig) SetThinkTime(strategy ThinkTimeStrategy, cfg interface{}, limit float64) *TConfig { c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit} return c } @@ -194,7 +194,7 @@ func (c *TConfig) DisableAutoPopupHandler() *TConfig { } type ThinkTimeConfig struct { - Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore + Strategy ThinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5 Limit float64 `json:"limit,omitempty" yaml:"limit,omitempty"` // limit think time no more than specific time, ignore if value <= 0 } @@ -205,10 +205,10 @@ func (ttc *ThinkTimeConfig) checkThinkTime() { } // unset strategy, set default strategy if ttc.Strategy == "" { - ttc.Strategy = thinkTimeDefault + ttc.Strategy = ThinkTimeDefault } // check think time - if ttc.Strategy == thinkTimeRandomPercentage { + if ttc.Strategy == ThinkTimeRandomPercentage { if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map { ttc.Setting = thinkTimeDefaultRandom return @@ -237,7 +237,7 @@ func (ttc *ThinkTimeConfig) checkThinkTime() { return } ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right} - } else if ttc.Strategy == thinkTimeMultiply { + } else if ttc.Strategy == ThinkTimeMultiply { if ttc.Setting == nil { ttc.Setting = float64(0) // default return @@ -248,19 +248,19 @@ func (ttc *ThinkTimeConfig) checkThinkTime() { return } ttc.Setting = value - } else if ttc.Strategy != thinkTimeIgnore { + } else if ttc.Strategy != ThinkTimeIgnore { // unrecognized strategy, set default strategy - ttc.Strategy = thinkTimeDefault + ttc.Strategy = ThinkTimeDefault } } -type thinkTimeStrategy string +type ThinkTimeStrategy string const ( - thinkTimeDefault thinkTimeStrategy = "default" // as recorded - thinkTimeRandomPercentage thinkTimeStrategy = "random_percentage" // use random percentage of recorded think time - thinkTimeMultiply thinkTimeStrategy = "multiply" // multiply recorded think time - thinkTimeIgnore thinkTimeStrategy = "ignore" // ignore recorded think time + ThinkTimeDefault ThinkTimeStrategy = "default" // as recorded + ThinkTimeRandomPercentage ThinkTimeStrategy = "random_percentage" // use random percentage of recorded think time + ThinkTimeMultiply ThinkTimeStrategy = "multiply" // multiply recorded think time + ThinkTimeIgnore ThinkTimeStrategy = "ignore" // ignore recorded think time ) const ( diff --git a/pkg/convert/README.md b/convert/README.md similarity index 100% rename from pkg/convert/README.md rename to convert/README.md diff --git a/pkg/convert/asset/flowgram.png b/convert/asset/flowgram.png similarity index 100% rename from pkg/convert/asset/flowgram.png rename to convert/asset/flowgram.png diff --git a/pkg/convert/from_ab.go b/convert/from_ab.go similarity index 100% rename from pkg/convert/from_ab.go rename to convert/from_ab.go diff --git a/pkg/convert/from_curl.go b/convert/from_curl.go similarity index 100% rename from pkg/convert/from_curl.go rename to convert/from_curl.go diff --git a/pkg/convert/from_curl_test.go b/convert/from_curl_test.go similarity index 100% rename from pkg/convert/from_curl_test.go rename to convert/from_curl_test.go diff --git a/pkg/convert/from_gotest.go b/convert/from_gotest.go similarity index 100% rename from pkg/convert/from_gotest.go rename to convert/from_gotest.go diff --git a/pkg/convert/from_har.go b/convert/from_har.go similarity index 100% rename from pkg/convert/from_har.go rename to convert/from_har.go diff --git a/pkg/convert/from_har_test.go b/convert/from_har_test.go similarity index 100% rename from pkg/convert/from_har_test.go rename to convert/from_har_test.go diff --git a/pkg/convert/from_jmeter.go b/convert/from_jmeter.go similarity index 100% rename from pkg/convert/from_jmeter.go rename to convert/from_jmeter.go diff --git a/pkg/convert/from_json.go b/convert/from_json.go similarity index 100% rename from pkg/convert/from_json.go rename to convert/from_json.go diff --git a/pkg/convert/from_postman.go b/convert/from_postman.go similarity index 100% rename from pkg/convert/from_postman.go rename to convert/from_postman.go diff --git a/pkg/convert/from_postman_test.go b/convert/from_postman_test.go similarity index 100% rename from pkg/convert/from_postman_test.go rename to convert/from_postman_test.go diff --git a/pkg/convert/from_pytest.go b/convert/from_pytest.go similarity index 100% rename from pkg/convert/from_pytest.go rename to convert/from_pytest.go diff --git a/pkg/convert/from_swagger.go b/convert/from_swagger.go similarity index 100% rename from pkg/convert/from_swagger.go rename to convert/from_swagger.go diff --git a/pkg/convert/from_yaml.go b/convert/from_yaml.go similarity index 100% rename from pkg/convert/from_yaml.go rename to convert/from_yaml.go diff --git a/pkg/convert/main.go b/convert/main.go similarity index 100% rename from pkg/convert/main.go rename to convert/main.go diff --git a/pkg/convert/main_test.go b/convert/main_test.go similarity index 100% rename from pkg/convert/main_test.go rename to convert/main_test.go diff --git a/pkg/convert/testcase.tmpl b/convert/testcase.tmpl similarity index 100% rename from pkg/convert/testcase.tmpl rename to convert/testcase.tmpl diff --git a/pkg/convert/to_gotest.go b/convert/to_gotest.go similarity index 100% rename from pkg/convert/to_gotest.go rename to convert/to_gotest.go diff --git a/pkg/convert/to_json.go b/convert/to_json.go similarity index 100% rename from pkg/convert/to_json.go rename to convert/to_json.go diff --git a/pkg/convert/to_pytest.go b/convert/to_pytest.go similarity index 100% rename from pkg/convert/to_pytest.go rename to convert/to_pytest.go diff --git a/pkg/convert/to_yaml.go b/convert/to_yaml.go similarity index 100% rename from pkg/convert/to_yaml.go rename to convert/to_yaml.go diff --git a/examples/uitest/android_e2e_delay_test.go b/examples/uitest/android_e2e_delay_test.go index f45e8812..6b113c4c 100644 --- a/examples/uitest/android_e2e_delay_test.go +++ b/examples/uitest/android_e2e_delay_test.go @@ -4,7 +4,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestAndroidDouyinE2E(t *testing.T) { diff --git a/examples/uitest/bili/android/cli.go b/examples/uitest/bili/android/cli.go index 3e946f90..d0ceed39 100644 --- a/examples/uitest/bili/android/cli.go +++ b/examples/uitest/bili/android/cli.go @@ -6,8 +6,8 @@ import ( "strconv" "time" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) var ( diff --git a/examples/uitest/bili/ios/cli.go b/examples/uitest/bili/ios/cli.go index 94e54a36..2daa6399 100644 --- a/examples/uitest/bili/ios/cli.go +++ b/examples/uitest/bili/ios/cli.go @@ -6,9 +6,9 @@ import ( "strconv" "time" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) var ( diff --git a/examples/uitest/demo_android_feed_swipe_test.go b/examples/uitest/demo_android_feed_swipe_test.go index f14a0cb5..a4080c40 100644 --- a/examples/uitest/demo_android_feed_swipe_test.go +++ b/examples/uitest/demo_android_feed_swipe_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestAndroidDouyinFeedTest(t *testing.T) { diff --git a/examples/uitest/demo_android_live_swipe_test.go b/examples/uitest/demo_android_live_swipe_test.go index 1dec1bee..b5b0d691 100644 --- a/examples/uitest/demo_android_live_swipe_test.go +++ b/examples/uitest/demo_android_live_swipe_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestAndroidLiveSwipeTest(t *testing.T) { diff --git a/examples/uitest/demo_douyin_follow_live_test.go b/examples/uitest/demo_douyin_follow_live_test.go index c9fcb911..8b714f19 100644 --- a/examples/uitest/demo_douyin_follow_live_test.go +++ b/examples/uitest/demo_douyin_follow_live_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestIOSDouyinFollowLive(t *testing.T) { diff --git a/examples/uitest/demo_harmony_test.go b/examples/uitest/demo_harmony_test.go index 08f5eef2..27536a4b 100644 --- a/examples/uitest/demo_harmony_test.go +++ b/examples/uitest/demo_harmony_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestHamonyDouyinFeedTest(t *testing.T) { diff --git a/examples/uitest/demo_ios_live_swipe_test.go b/examples/uitest/demo_ios_live_swipe_test.go index 85acce4e..76e970d9 100644 --- a/examples/uitest/demo_ios_live_swipe_test.go +++ b/examples/uitest/demo_ios_live_swipe_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestIOSDouyinLive(t *testing.T) { diff --git a/examples/uitest/demo_ios_wda_log_test.go b/examples/uitest/demo_ios_wda_log_test.go index d3a53e35..30407c0d 100644 --- a/examples/uitest/demo_ios_wda_log_test.go +++ b/examples/uitest/demo_ios_wda_log_test.go @@ -6,7 +6,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestWDALog(t *testing.T) { diff --git a/examples/uitest/expert_test.go b/examples/uitest/expert_test.go index e3b9848e..061bc2af 100644 --- a/examples/uitest/expert_test.go +++ b/examples/uitest/expert_test.go @@ -4,7 +4,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestAndroidExpertTest(t *testing.T) { diff --git a/examples/uitest/harmony_e2e_delay_test.go b/examples/uitest/harmony_e2e_delay_test.go index e427702f..31312a7a 100644 --- a/examples/uitest/harmony_e2e_delay_test.go +++ b/examples/uitest/harmony_e2e_delay_test.go @@ -4,7 +4,7 @@ import ( "testing" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestHarmonyDouyinE2E(t *testing.T) { diff --git a/examples/worldcup/cli.go b/examples/worldcup/cli.go index 16e68ae7..31c95d71 100644 --- a/examples/worldcup/cli.go +++ b/examples/worldcup/cli.go @@ -7,9 +7,9 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) var rootCmd = &cobra.Command{ diff --git a/examples/worldcup/main.go b/examples/worldcup/main.go index 16e28be4..4afbf986 100644 --- a/examples/worldcup/main.go +++ b/examples/worldcup/main.go @@ -15,8 +15,8 @@ import ( "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) func convertTimeToSeconds(timeStr string) (int, error) { diff --git a/examples/worldcup/main_test.go b/examples/worldcup/main_test.go index f37e8878..077279a7 100644 --- a/examples/worldcup/main_test.go +++ b/examples/worldcup/main_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" hrp "github.com/httprunner/httprunner/v5" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestConvertTimeToSeconds(t *testing.T) { diff --git a/go.mod b/go.mod index 9661ac61..0ba4a624 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.0 require ( code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27 - github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 github.com/Masterminds/semver v1.5.0 github.com/andybalholm/brotli v1.0.4 github.com/danielpaulus/go-ios v1.0.161 @@ -54,7 +53,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/grandcat/zeroconf v1.0.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-plugin v1.4.10 // indirect diff --git a/go.sum b/go.sum index 692c28a6..9c7d42f3 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27 h1:+wNJiEXXIUP6luKJRA4tfwDqfnWUON6LIopKD9tvUns= code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27/go.mod h1:C2kq6TTE+JAOnqDorSwae1MQzRuex03RshuSUC2U/FY= -github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= -github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -86,8 +84,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= diff --git a/internal/builtin/function.go b/internal/builtin/function.go index baf94a19..3da9ea55 100644 --- a/internal/builtin/function.go +++ b/internal/builtin/function.go @@ -47,10 +47,6 @@ func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } -func init() { - rand.Seed(time.Now().UnixNano()) -} - func random_range(a, b float64) float64 { return a + rand.Float64()*(b-a) } diff --git a/internal/builtin/utils.go b/internal/builtin/utils.go index e4accadc..3e49f202 100644 --- a/internal/builtin/utils.go +++ b/internal/builtin/utils.go @@ -5,16 +5,13 @@ import ( "bytes" "context" "crypto/hmac" - "crypto/md5" "crypto/sha256" "encoding/csv" builtinJSON "encoding/json" "fmt" - "io" "math" "math/rand" "net" - "net/http" "os" "os/exec" "path/filepath" @@ -23,7 +20,6 @@ import ( "strings" "time" - "github.com/BurntSushi/locker" "github.com/pkg/errors" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" @@ -229,6 +225,22 @@ func InterfaceType(raw interface{}) string { return reflect.TypeOf(raw).String() } +func LoadFile(path string) ([]byte, error) { + var err error + path, err = filepath.Abs(path) + if err != nil { + log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") + return nil, errors.Wrap(code.LoadFileError, err.Error()) + } + + file, err := os.ReadFile(path) + if err != nil { + log.Error().Err(err).Msg("read file failed") + return nil, errors.Wrap(code.LoadFileError, err.Error()) + } + return file, nil +} + func loadFromCSV(path string) []map[string]interface{} { log.Info().Str("path", path).Msg("load csv file") file, err := os.ReadFile(path) @@ -358,21 +370,18 @@ func ConvertToStringSlice(val interface{}) ([]string, error) { } func GetFreePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return 0, errors.Wrap(err, "resolve tcp addr failed") - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, errors.Wrap(err, "listen tcp addr failed") - } - defer func() { - if err = l.Close(); err != nil { - log.Error().Err(err).Msg(fmt.Sprintf("close addr %s error", l.Addr().String())) + minPort := 20000 + maxPort := 50000 + for i := 0; i < 10; i++ { + port := rand.Intn(maxPort-minPort+1) + minPort + addr := fmt.Sprintf("0.0.0.0:%d", port) + l, err := net.Listen("tcp", addr) + if err == nil { + defer l.Close() // 端口成功绑定后立即释放,返回该端口号 + return port, nil } - }() - return l.Addr().(*net.TCPAddr).Port, nil + } + return 0, errors.New("failed to get available port") } func GetCurrentDay() string { @@ -382,7 +391,7 @@ func GetCurrentDay() string { return formattedDate } -func fileExists(filepath string) bool { +func FileExists(filepath string) bool { _, err := os.Stat(filepath) if os.IsNotExist(err) { return false // 文件不存在 @@ -390,61 +399,6 @@ func fileExists(filepath string) bool { return err == nil // 文件存在,且没有其他错误 } -func DownloadFileByUrl(fileUrl string) (filePath string, err error) { - // 使用 UUID 生成唯一文件名 - cwd, err := os.Getwd() - if err != nil { - return "", err - } - hash := md5.Sum([]byte(fileUrl)) - fileName := fmt.Sprintf("%x", hash) - filePath = filepath.Join(cwd, fileName) - locker.Lock(filePath) - defer locker.Unlock(filePath) - if fileExists(filePath) { - return filePath, nil - } - - fmt.Printf("Downloading file to %s from URL %s\n", filePath, fileUrl) - - // Create an HTTP client with default settings. - client := &http.Client{} - - // Build the HTTP GET request. - req, err := http.NewRequest("GET", fileUrl, nil) - if err != nil { - return "", err - } - - // Perform the request. - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // Check the HTTP status code. - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to download file: %s", resp.Status) - } - - // Create the output file. - outFile, err := os.Create(fileName) - if err != nil { - return "", err - } - defer outFile.Close() - - // Copy the response body to the file. - _, err = io.Copy(outFile, resp.Body) - if err != nil { - return "", err - } - - fmt.Printf("File downloaded successfully: %s\n", fileName) - return filePath, nil -} - func RunCommand(cmdName string, args ...string) error { cmd := exec.Command(cmdName, args...) log.Info().Str("command", cmd.String()).Msg("exec command") diff --git a/internal/config/config.go b/internal/config/config.go index 8c99e7af..ee6abe65 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "sync" "time" "github.com/rs/zerolog/log" @@ -12,39 +13,60 @@ import ( const ( ResultsDirName = "results" + DownloadsDirName = "downloads" ScreenshotsDirName = "screenshots" ActionLogDirName = "action_log" ) -var ( +type Config struct { RootDir string ResultsDir string ResultsPath string + DownloadsPath string ScreenShotsPath string - StartTime = time.Now() - StartTimeStr = StartTime.Format("20060102150405") + StartTime time.Time ActionLogFilePath string DeviceActionLogFilePath string +} + +var ( + globalConfig *Config + once sync.Once ) -func init() { - var err error - RootDir, err = os.Getwd() - if err != nil { - panic(err) - } +func GetConfig() *Config { + once.Do(func() { + cfg := &Config{ + StartTime: time.Now(), + } - ResultsDir = filepath.Join(ResultsDirName, StartTimeStr) - ResultsPath = filepath.Join(RootDir, ResultsDir) - ScreenShotsPath = filepath.Join(ResultsPath, ScreenshotsDirName) - ActionLogFilePath = filepath.Join(ResultsDir, ActionLogDirName) - DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor" + var err error + cfg.RootDir, err = os.Getwd() + if err != nil { + panic(err) + } - // create results directory - if err := builtin.EnsureFolderExists(ResultsPath); err != nil { - log.Fatal().Err(err).Msg("create results directory failed") - } - if err := builtin.EnsureFolderExists(ScreenShotsPath); err != nil { - log.Fatal().Err(err).Msg("create screenshots directory failed") - } + startTimeStr := cfg.StartTime.Format("20060102150405") + cfg.ResultsDir = filepath.Join(ResultsDirName, startTimeStr) + cfg.ResultsPath = filepath.Join(cfg.RootDir, cfg.ResultsDir) + cfg.DownloadsPath = filepath.Join(cfg.RootDir, filepath.Join(DownloadsDirName, startTimeStr)) + cfg.ScreenShotsPath = filepath.Join(cfg.ResultsPath, ScreenshotsDirName) + cfg.ActionLogFilePath = filepath.Join(cfg.ResultsDir, ActionLogDirName) + cfg.DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor" + + // create results directory + if err := builtin.EnsureFolderExists(cfg.ResultsPath); err != nil { + log.Fatal().Err(err).Msg("create results directory failed") + } + if err := builtin.EnsureFolderExists(cfg.DownloadsPath); err != nil { + log.Fatal().Err(err).Msg("create downloads directory failed") + } + if err := builtin.EnsureFolderExists(cfg.ScreenShotsPath); err != nil { + log.Fatal().Err(err).Msg("create screenshots directory failed") + } + + globalConfig = cfg + }) + + return globalConfig } diff --git a/pkg/httpstat/main.go b/internal/httpstat/main.go similarity index 100% rename from pkg/httpstat/main.go rename to internal/httpstat/main.go diff --git a/pkg/httpstat/demo/main_test.go b/internal/httpstat/main_test.go similarity index 76% rename from pkg/httpstat/demo/main_test.go rename to internal/httpstat/main_test.go index 61054691..4a8f9cd8 100644 --- a/pkg/httpstat/demo/main_test.go +++ b/internal/httpstat/main_test.go @@ -1,19 +1,17 @@ -package demo +package httpstat import ( "fmt" "net/http" "testing" "time" - - "github.com/httprunner/httprunner/v5/pkg/httpstat" ) func TestMain(t *testing.T) { - var httpStat httpstat.Stat + var httpStat Stat req, _ := http.NewRequest("GET", "https://httprunner.com", nil) - ctx := httpstat.WithHTTPStat(req, &httpStat) + ctx := WithHTTPStat(req, &httpStat) client := &http.Client{ Timeout: time.Second * 10, diff --git a/internal/version/VERSION b/internal/version/VERSION index 81d96d83..9e536156 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2502192152 +v5.0.0-beta-2503051939 diff --git a/loader.go b/loader.go index e592c68d..2e25212e 100644 --- a/loader.go +++ b/loader.go @@ -12,6 +12,7 @@ import ( "gopkg.in/yaml.v2" "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/json" ) @@ -85,7 +86,7 @@ func LoadTestCases(tests ...ITestCase) ([]*TestCase, error) { // LoadFileObject loads file content with file extension and assigns to structObj func LoadFileObject(path string, structObj interface{}) (err error) { log.Info().Str("path", path).Msg("load file") - file, err := readFile(path) + file, err := builtin.LoadFile(path) if err != nil { return errors.Wrap(err, "read file failed") } @@ -145,19 +146,3 @@ func parseEnvContent(file []byte, obj interface{}) error { } return nil } - -func readFile(path string) ([]byte, error) { - var err error - path, err = filepath.Abs(path) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") - return nil, errors.Wrap(code.LoadFileError, err.Error()) - } - - file, err := os.ReadFile(path) - if err != nil { - log.Error().Err(err).Msg("read file failed") - return nil, errors.Wrap(code.LoadFileError, err.Error()) - } - return file, nil -} diff --git a/loader_test.go b/loader_test.go deleted file mode 100644 index a870217a..00000000 --- a/loader_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package hrp - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadTestCases(t *testing.T) { - // load test cases from folder path - tc := TestCasePath("../examples/demo-with-py-plugin/testcases/") - testCases, err := LoadTestCases(&tc) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, 4, len(testCases)) { - t.Fatal() - } - - // load test cases from folder path, including sub folders - tc = TestCasePath("../examples/demo-with-py-plugin/") - testCases, err = LoadTestCases(&tc) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, 4, len(testCases)) { - t.Fatal() - } - - // load test cases from single file path - tc = TestCasePath(demoTestCaseWithPluginJSONPath) - testCases, err = LoadTestCases(&tc) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, 1, len(testCases)) { - t.Fatal() - } - - // load test cases from TestCase instance - testcase := &TestCase{ - Config: NewConfig("TestCase").SetWeight(3), - } - testCases, err = LoadTestCases(testcase) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, len(testCases), 1) { - t.Fatal() - } - - // load test cases from TestCaseJSON - testcaseJSON := TestCaseJSON(` - { - "config":{"name":"TestCaseJSON"}, - "teststeps":[ - {"name": "step1", "request":{"url": "https://httpbin.org/get"}}, - {"name": "step2", "shell":{"string": "ls -l"}} - ] - }`) - testCases, err = LoadTestCases(&testcaseJSON) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, len(testCases), 1) { - t.Fatal() - } -} - -func TestLoadCase(t *testing.T) { - tcJSON := &TestCaseDef{} - tcYAML := &TestCaseDef{} - err := LoadFileObject(demoTestCaseWithPluginJSONPath, tcJSON) - if !assert.NoError(t, err) { - t.Fatal() - } - err = LoadFileObject(demoTestCaseWithPluginYAMLPath, tcYAML) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name) { - t.Fatal() - } - if !assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL) { - t.Fatal() - } - if !assert.Equal(t, tcJSON.Steps[1].StepName, tcYAML.Steps[1].StepName) { - t.Fatal() - } - if !assert.Equal(t, tcJSON.Steps[1].Request, tcJSON.Steps[1].Request) { - t.Fatal() - } -} diff --git a/parameters.go b/parameters.go index f57c338e..1de3d21c 100644 --- a/parameters.go +++ b/parameters.go @@ -13,7 +13,7 @@ import ( type TParamsConfig struct { PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` // overall pick-order strategy - Strategies map[string]iteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters + Strategies map[string]IteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` } @@ -35,13 +35,13 @@ const ( */ type Parameters []map[string]interface{} -type iteratorStrategy struct { +type IteratorStrategy struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` } -func (p *Parser) initParametersIterator(cfg *TConfig) (*ParametersIterator, error) { - parameters, err := p.loadParameters(cfg.Parameters, cfg.Variables) +func (p *Parser) InitParametersIterator(cfg *TConfig) (*ParametersIterator, error) { + parameters, err := p.LoadParameters(cfg.Parameters, cfg.Variables) if err != nil { return nil, err } @@ -57,13 +57,13 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf hasNext: true, sequentialParameters: nil, randomParameterNames: nil, - limit: config.Limit, - index: 0, + Limit: config.Limit, + Index: 0, } if len(parameters) == 0 { iterator.data = map[string]Parameters{} - iterator.limit = 1 + iterator.Limit = 1 return iterator } @@ -85,24 +85,24 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf } // generate cartesian product for sequential parameters - iterator.sequentialParameters = genCartesianProduct(parametersList) + iterator.sequentialParameters = GenCartesianProduct(parametersList) - if iterator.limit < 0 { + if iterator.Limit < 0 { log.Warn().Msg("parameters unlimited mode is only supported for load testing") - iterator.limit = 0 + iterator.Limit = 0 } - if iterator.limit == 0 { + if iterator.Limit == 0 { // limit not set if len(iterator.sequentialParameters) > 0 { // use cartesian product of sequential parameters size as limit - iterator.limit = len(iterator.sequentialParameters) + iterator.Limit = len(iterator.sequentialParameters) } else { // all parameters are selected by random // only run once - iterator.limit = 1 + iterator.Limit = 1 } } else { // limit > 0 - log.Info().Int("limit", iterator.limit).Msg("set limit for parameters") + log.Info().Int("limit", iterator.Limit).Msg("set limit for parameters") } return iterator @@ -114,14 +114,14 @@ type ParametersIterator struct { hasNext bool // cache query result sequentialParameters Parameters // cartesian product for sequential parameters randomParameterNames []string // value is parameter names - limit int // limit count for iteration - index int // current iteration index + Limit int // limit count for iteration + Index int // current iteration index } // SetUnlimitedMode is used for load testing func (iter *ParametersIterator) SetUnlimitedMode() { log.Info().Msg("set parameters unlimited mode") - iter.limit = -1 + iter.Limit = -1 } func (iter *ParametersIterator) HasNext() bool { @@ -130,12 +130,12 @@ func (iter *ParametersIterator) HasNext() bool { } // unlimited mode - if iter.limit == -1 { + if iter.Limit == -1 { return true } // reached limit - if iter.index >= iter.limit { + if iter.Index >= iter.Limit { // cache query result iter.hasNext = false return false @@ -155,11 +155,11 @@ func (iter *ParametersIterator) Next() map[string]interface{} { var selectedParameters map[string]interface{} if len(iter.sequentialParameters) == 0 { selectedParameters = make(map[string]interface{}) - } else if iter.index < len(iter.sequentialParameters) { - selectedParameters = iter.sequentialParameters[iter.index] + } else if iter.Index < len(iter.sequentialParameters) { + selectedParameters = iter.sequentialParameters[iter.Index] } else { // loop back to the first sequential parameter - index := iter.index % len(iter.sequentialParameters) + index := iter.Index % len(iter.sequentialParameters) selectedParameters = iter.sequentialParameters[index] } @@ -172,8 +172,8 @@ func (iter *ParametersIterator) Next() map[string]interface{} { } } - iter.index++ - if iter.limit > 0 && iter.index >= iter.limit { + iter.Index++ + if iter.Limit > 0 && iter.Index >= iter.Limit { iter.hasNext = false } @@ -188,7 +188,7 @@ func (iter *ParametersIterator) Data() map[string]interface{} { return res } -func genCartesianProduct(multiParameters []Parameters) Parameters { +func GenCartesianProduct(multiParameters []Parameters) Parameters { if len(multiParameters) == 0 { return nil } @@ -208,7 +208,7 @@ func genCartesianProduct(multiParameters []Parameters) Parameters { } /* - loadParameters loads parameters from multiple sources. + LoadParameters loads parameters from multiple sources. parameter value may be in three types: @@ -240,7 +240,7 @@ parameter value may be in three types: ] } */ -func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) ( +func (p *Parser) LoadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) ( map[string]Parameters, error) { if len(configParameters) == 0 { @@ -291,7 +291,7 @@ func (p *Parser) loadParameters(configParameters map[string]interface{}, variabl return nil, errors.New("config parameters raw value format error") } - parameterSlice, err := convertParameters(k, parametersRawList) + parameterSlice, err := ConvertParameters(k, parametersRawList) if err != nil { return nil, err } @@ -320,7 +320,7 @@ case 3: key = "username-password" parametersRawList = [["test1", "111111"], ["test2", "222222"]] */ -func convertParameters(key string, parametersRawList interface{}) (parameterSlice []map[string]interface{}, err error) { +func ConvertParameters(key string, parametersRawList interface{}) (parameterSlice []map[string]interface{}, err error) { parametersRawSlice := reflect.ValueOf(parametersRawList) if parametersRawSlice.Kind() != reflect.Slice { return nil, errors.New("parameters raw value is not list") diff --git a/parser.go b/parser.go index 317f6352..8a5cee32 100644 --- a/parser.go +++ b/parser.go @@ -10,17 +10,17 @@ import ( "strconv" "strings" - "github.com/httprunner/funplugin" - "github.com/httprunner/funplugin/fungo" "github.com/maja42/goval" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/funplugin" + "github.com/httprunner/funplugin/fungo" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" ) -func newParser() *Parser { +func NewParser() *Parser { return &Parser{} } diff --git a/parser_test.go b/parser_test.go index b007c5e6..b7cc49f7 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,8 +1,11 @@ package hrp import ( + "io" + "net/http" "net/url" "sort" + "strings" "testing" "time" @@ -208,7 +211,7 @@ func TestParseDataStringWithVariables(t *testing.T) { {"abc$var_5", "abctrue"}, // "abcTrue" } - parser := newParser() + parser := NewParser() for _, data := range testData { parsedData, err := parser.Parse(data.expr, variablesMapping) if !assert.NoError(t, err) { @@ -233,7 +236,7 @@ func TestParseDataStringWithUndefinedVariables(t *testing.T) { {"/api/$SECRET_KEY", "/api/$SECRET_KEY"}, // raise error } - parser := newParser() + parser := NewParser() for _, data := range testData { parsedData, err := parser.Parse(data.expr, variablesMapping) if !assert.Error(t, err) { @@ -278,7 +281,7 @@ func TestParseDataStringWithVariablesAbnormal(t *testing.T) { {"ABC$var_1{}a", "ABCabc{}a"}, // {} } - parser := newParser() + parser := NewParser() for _, data := range testData { parsedData, err := parser.Parse(data.expr, variablesMapping) if !assert.NoError(t, err) { @@ -309,7 +312,7 @@ func TestParseDataMapWithVariables(t *testing.T) { {map[string]interface{}{"$var2": "$val1"}, map[string]interface{}{"123": 200}}, } - parser := newParser() + parser := NewParser() for _, data := range testData { parsedData, err := parser.Parse(data.expr, variablesMapping) if !assert.NoError(t, err) { @@ -343,7 +346,7 @@ func TestParseHeaders(t *testing.T) { {map[string]string{"$var2": "$val2"}, map[string]string{"123": ""}}, } - parser := newParser() + parser := NewParser() for _, data := range testData { parsedHeaders, err := parser.ParseHeaders(data.rawHeaders, variablesMapping) if !assert.NoError(t, err) { @@ -488,7 +491,7 @@ func TestMergeValidators(t *testing.T) { } func TestCallBuiltinFunction(t *testing.T) { - parser := newParser() + parser := NewParser() // call function without arguments _, err := parser.callFunc("get_timestamp") @@ -601,7 +604,7 @@ func TestParseDataStringWithFunctions(t *testing.T) { {"123${gen_random_string($n)}abc", 11}, } - parser := newParser() + parser := NewParser() for _, data := range testData1 { value, err := parser.Parse(data.expr, variablesMapping) if !assert.NoError(t, err) { @@ -670,7 +673,7 @@ func TestParseVariables(t *testing.T) { }, } - parser := newParser() + parser := NewParser() for _, data := range testData { value, err := parser.ParseVariables(data.rawVars) if !assert.NoError(t, err) { @@ -701,7 +704,7 @@ func TestParseVariablesAbnormal(t *testing.T) { }, } - parser := newParser() + parser := NewParser() for _, data := range testData { value, err := parser.ParseVariables(data.rawVars) if !assert.Error(t, err) { @@ -784,3 +787,193 @@ func TestFindallVariables(t *testing.T) { } } } + +func TestSearchJmespath(t *testing.T) { + testText := `{"a": {"b": "foo"}, "c": "bar", "d": {"e": [{"f": "foo"}, {"f": "bar"}]}}` + testData := []struct { + raw string + expected string + }{ + {"body.a.b", "foo"}, + {"body.c", "bar"}, + {"body.d.e[0].f", "foo"}, + {"body.d.e[1].f", "bar"}, + } + resp := http.Response{} + resp.Body = io.NopCloser(strings.NewReader(testText)) + respObj, err := newHttpResponseObject(t, NewParser(), &resp) + if err != nil { + t.Fatal() + } + for _, data := range testData { + if !assert.Equal(t, data.expected, respObj.searchJmespath(data.raw)) { + t.Fatal() + } + } +} + +func TestSearchRegexp(t *testing.T) { + testText := ` + +` + testData := []struct { + raw string + expected string + }{ + {"/user/signOut\">(.*)", "Sign Out"}, + {"
  • ", "Leo"}, + } + // new response object + resp := http.Response{} + resp.Body = io.NopCloser(strings.NewReader(testText)) + respObj, err := newHttpResponseObject(t, NewParser(), &resp) + if err != nil { + t.Fatal() + } + for _, data := range testData { + if !assert.Equal(t, data.expected, respObj.searchRegexp(data.raw)) { + t.Fatal() + } + } +} + +func TestConvertCheckExpr(t *testing.T) { + exprs := []struct { + before string + after string + }{ + // normal check expression + {"a.b.c", "a.b.c"}, + {"a.\"b-c\".d", "a.\"b-c\".d"}, + {"a.b-c.d", "a.b-c.d"}, + {"body.args.a[-1]", "body.args.a[-1]"}, + // check expression using regex + {"covering (.*) testing,", "covering (.*) testing,"}, + {" (.*) a-b-c", " (.*) a-b-c"}, + // abnormal check expression + {"headers.Content-Type", "headers.\"Content-Type\""}, + {"headers.\"Content-Type", "headers.\"Content-Type\""}, + {"headers.Content-Type\"", "headers.\"Content-Type\""}, + {"headers.User-Agent", "headers.\"User-Agent\""}, + } + for _, expr := range exprs { + assert.Equal(t, expr.after, convertJmespathExpr(expr.before)) + } +} + +func TestFindAllPythonFunctionNames(t *testing.T) { + content := ` +def test_1(): # exported function + pass + +def _test_2(): # exported function + pass + +def __test_3(): # private function + pass + +# def test_4(): # commented out function +# pass + +def Test5(): # exported function + pass +` + names, err := regexPyFunctionName.findAllFunctionNames(content) + if !assert.Nil(t, err) { + t.FailNow() + } + if !assert.Contains(t, names, "test_1") { + t.FailNow() + } + if !assert.Contains(t, names, "Test5") { + t.FailNow() + } + if !assert.Contains(t, names, "_test_2") { + t.FailNow() + } + if !assert.NotContains(t, names, "__test_3") { + t.FailNow() + } + // commented out function + if !assert.NotContains(t, names, "test_4") { + t.FailNow() + } +} + +func TestFindAllGoFunctionNames(t *testing.T) { + content := ` +func Test1() { // exported function + return +} + +func testFunc2() { // exported function + return +} + +func init() { // private function + return +} + +func _Test3() { // exported function + return +} + +// func Test4() { // commented out function +// return +// } +` + names, err := regexGoFunctionName.findAllFunctionNames(content) + if !assert.Nil(t, err) { + t.FailNow() + } + if !assert.Contains(t, names, "Test1") { + t.FailNow() + } + if !assert.Contains(t, names, "testFunc2") { + t.FailNow() + } + if !assert.NotContains(t, names, "init") { + t.FailNow() + } + if !assert.Contains(t, names, "_Test3") { + t.FailNow() + } + // commented out function + if !assert.NotContains(t, names, "Test4") { + t.FailNow() + } +} + +func TestFindAllGoFunctionNamesAbnormal(t *testing.T) { + content := ` +func init() { // private function + return +} + +func main() { // should not define main() function + return +} +` + _, err := regexGoFunctionName.findAllFunctionNames(content) + if !assert.NotNil(t, err) { + t.FailNow() + } +} diff --git a/pkg/gadb/device.go b/pkg/gadb/device.go index 17e8358e..9a9abd4d 100644 --- a/pkg/gadb/device.go +++ b/pkg/gadb/device.go @@ -640,6 +640,7 @@ func (d *Device) InstallAPK(apkPath string, args ...string) (string, error) { haserr := func(ret string) bool { return strings.Contains(ret, "Failure") } + // 该方法掉线不会返回error。导致误认为安装成功 if d.HasFeature(FeatAbbExec) { raw, err := d.installViaABBExec(apkFile) if err != nil { @@ -734,6 +735,11 @@ func (d *Device) ScreenCap() ([]byte, error) { return nil, err } + // remove temp file + defer func() { + go d.RunShellCommand("rm", tempPath) + }() + buffer := bytes.NewBuffer(nil) err = d.Pull(tempPath, buffer) return buffer.Bytes(), err diff --git a/pkg/uixt/driver_ext/ext.go b/pkg/uixt/driver_ext/ext.go deleted file mode 100644 index 28cacebb..00000000 --- a/pkg/uixt/driver_ext/ext.go +++ /dev/null @@ -1,71 +0,0 @@ -package driver_ext - -import ( - "fmt" - "time" - - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" -) - -type IStubDriver interface { - uixt.IDriver - - LoginNoneUI(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error) - LogoutNoneUI(packageName string) error -} - -func NewXTDriver(driver uixt.IDriver, opts ...ai.AIServiceOption) *XTDriver { - services := ai.NewAIService(opts...) - driverExt := &XTDriver{ - XTDriver: &uixt.XTDriver{ - IDriver: driver, - CVService: services.ICVService, - LLMService: services.ILLMService, - }, - } - return driverExt -} - -type XTDriver struct { - *uixt.XTDriver -} - -func (dExt *XTDriver) InstallByUrl(url string, opts ...option.InstallOption) error { - appPath, err := builtin.DownloadFileByUrl(url) - if err != nil { - return err - } - err = dExt.Install(appPath, opts...) - if err != nil { - return err - } - return nil -} - -func (dExt *XTDriver) Install(filePath string, opts ...option.InstallOption) error { - if _, ok := dExt.GetDevice().(*uixt.AndroidDevice); ok { - stopChan := make(chan struct{}) - go func() { - ticker := time.NewTicker(8 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - _ = dExt.TapByOCR("^(.*无视风险安装|正在扫描.*|我知道了|稍后继续|稍后提醒|继续安装|知道了|确定|继续|完成|点击继续安装|继续安装旧版本|替换|.*正在安装|安装|授权本次安装|重新安装|仍要安装|更多详情|我知道了|已了解此应用未经检测.)$", option.WithRegex(true), option.WithIgnoreNotFoundError(true)) - case <-stopChan: - fmt.Println("Ticker stopped") - return - } - } - }() - defer func() { - close(stopChan) - }() - } - - return dExt.GetDevice().Install(filePath, opts...) -} diff --git a/pkg/uixt/driver_ext/ios_stub_driver.go b/pkg/uixt/driver_ext/ios_stub_driver.go deleted file mode 100644 index f3939c22..00000000 --- a/pkg/uixt/driver_ext/ios_stub_driver.go +++ /dev/null @@ -1,286 +0,0 @@ -package driver_ext - -import ( - "encoding/json" - "fmt" - "net/url" - "time" - - "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" -) - -type StubIOSDriver struct { - *uixt.WDADriver - - timeout time.Duration - douyinUrlPrefix string - douyinLiteUrlPrefix string -} - -const ( - IOSDouyinPort = 32921 - IOSDouyinLitePort = 33461 - defaultBightInsightPort = 8000 -) - -func NewStubIOSDriver(dev *uixt.IOSDevice) (*StubIOSDriver, error) { - // lazy setup WDA - dev.Options.LazySetup = true - - wdaDriver, err := uixt.NewWDADriver(dev) - if err != nil { - return nil, err - } - driver := &StubIOSDriver{ - WDADriver: wdaDriver, - timeout: 10 * time.Second, - } - - // setup driver - if err := driver.Setup(); err != nil { - return nil, err - } - - // register driver session reset handler - driver.Session.RegisterResetHandler(driver.Setup) - - return driver, nil -} - -func (s *StubIOSDriver) Setup() error { - localPort, err := s.getLocalPort() - if err != nil { - return err - } - err = s.Session.SetupPortForward(localPort) - if err != nil { - return err - } - s.Session.SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", localPort)) - - localDouyinPort, err := builtin.GetFreePort() - if err != nil { - return errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("get free port failed: %v", err)) - } - if err = s.Device.Forward(localDouyinPort, IOSDouyinPort); err != nil { - return errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("forward tcp port failed: %v", err)) - } - s.douyinUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinPort) - - localDouyinLitePort, err := builtin.GetFreePort() - if err != nil { - return errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("get free port failed: %v", err)) - } - if err = s.Device.Forward(localDouyinLitePort, IOSDouyinLitePort); err != nil { - return errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("forward tcp port failed: %v", err)) - } - s.douyinLiteUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinLitePort) - return nil -} - -func (s *StubIOSDriver) getLocalPort() (int, error) { - localStubPort, err := builtin.GetFreePort() - if err != nil { - return 0, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("get free port failed: %v", err)) - } - if err = s.Device.Forward(localStubPort, defaultBightInsightPort); err != nil { - return 0, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("forward tcp port failed: %v", err)) - } - return localStubPort, nil -} - -func (s *StubIOSDriver) Source(srcOpt ...option.SourceOption) (string, error) { - resp, err := s.Session.GET("/source?format=json&onlyWeb=false") - if err != nil { - log.Error().Err(err).Msg("get source err") - return "", nil - } - return string(resp), nil -} - -func (s *StubIOSDriver) OpenUrl(urlStr string, options ...option.ActionOption) (err error) { - targetUrl := fmt.Sprintf("/openURL?url=%s", url.QueryEscape(urlStr)) - _, err = s.Session.GET(targetUrl) - if err != nil { - log.Error().Err(err).Msg("get source err") - return nil - } - return nil -} - -func (s *StubIOSDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { - appInfo, err := s.ForegroundInfo() - if err != nil { - return info, err - } - if appInfo.BundleId == "com.ss.iphone.ugc.AwemeInhouse" || appInfo.BundleId == "com.ss.iphone.ugc.awemeinhouse.lite" { - return s.LoginDouyin(appInfo.BundleId, phoneNumber, captcha, password) - } else if appInfo.BundleId == "com.ss.iphone.InHouse.article.Video" { - return s.LoginXigua(appInfo.BundleId, phoneNumber, captcha, password) - } else { - return info, fmt.Errorf("not support app") - } -} - -func (s *StubIOSDriver) LoginXigua(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { - loginSchema := "" - if captcha != "" { - loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=1&account=%s&smscode=%s", phoneNumber, captcha) - } else if password != "" { - loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=2&account=%s&password=%s", phoneNumber, password) - } else { - return info, fmt.Errorf("password and capcha is empty") - } - info.IsLogin = true - return info, s.OpenUrl(loginSchema) -} - -func (s *StubIOSDriver) LoginDouyin(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { - params := map[string]interface{}{ - "phone": phoneNumber, - } - if captcha != "" { - params["captcha"] = captcha - } else if password != "" { - params["password"] = password - } else { - return info, fmt.Errorf("password and capcha is empty") - } - bsJSON, err := json.Marshal(params) - if err != nil { - return info, err - } - - urlPrefix, err := s.getUrlPrefix(packageName) - if err != nil { - return info, err - } - fullUrl := urlPrefix + "/host/login/account/" + urlPrefix - resp, err := s.Session.POST(bsJSON, fullUrl) - if err != nil { - return info, err - } - res, err := resp.ValueConvertToJsonObject() - if err != nil { - return info, err - } - log.Info().Msgf("%v", res) - // {'isSuccess': True, 'data': '登录成功', 'code': 0} - if res["isSuccess"] != true { - err = fmt.Errorf("falied to logout %s", res["data"]) - log.Err(err).Msgf("%v", res) - return info, err - } - time.Sleep(20 * time.Second) - info, err = s.getLoginAppInfo(packageName) - if err != nil || !info.IsLogin { - return info, fmt.Errorf("falied to login %v", info) - } - return info, nil -} - -func (s *StubIOSDriver) LogoutNoneUI(packageName string) error { - urlPrefix, err := s.getUrlPrefix(packageName) - if err != nil { - return err - } - fullUrl := urlPrefix + "/host/loginout/" - resp, err := s.Session.GET(fullUrl) - if err != nil { - return err - } - res, err := resp.ValueConvertToJsonObject() - if err != nil { - return err - } - log.Info().Msgf("%v", res) - if res["isSuccess"] != true { - err = fmt.Errorf("falied to logout %s", res["data"]) - log.Err(err).Msgf("%v", res) - return err - } - time.Sleep(10 * time.Second) - return nil -} - -func (s *StubIOSDriver) EnableDevtool(packageName string, enable bool) (err error) { - urlPrefix, err := s.getUrlPrefix(packageName) - if err != nil { - return err - } - fullUrl := urlPrefix + "/host/devtool/enable" - - params := map[string]interface{}{ - "enable": enable, - } - bsJSON, err := json.Marshal(params) - if err != nil { - return err - } - resp, err := s.Session.POST(bsJSON, fullUrl) - if err != nil { - return err - } - res, err := resp.ValueConvertToJsonObject() - if err != nil { - return err - } - log.Info().Msgf("%v", res) - if res["isSuccess"] != true { - err = fmt.Errorf("falied to enable devtool %s", res["data"]) - log.Err(err).Msgf("%v", res) - return err - } - return nil -} - -func (s *StubIOSDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) { - urlPrefix, err := s.getUrlPrefix(packageName) - if err != nil { - return info, err - } - fullUrl := urlPrefix + "/host/app/info/" - - resp, err := s.Session.GET(fullUrl) - if err != nil { - return info, err - } - res, err := resp.ValueConvertToJsonObject() - if err != nil { - return info, err - } - log.Info().Msgf("%v", res) - if res["isSuccess"] != true { - err = fmt.Errorf("falied to get is login %s", res["data"]) - log.Err(err).Msgf("%v", res) - return info, err - } - err = json.Unmarshal([]byte(res["data"].(string)), &info) - if err != nil { - return info, err - } - return info, nil -} - -func (s *StubIOSDriver) getUrlPrefix(packageName string) (urlPrefix string, err error) { - if packageName == "com.ss.iphone.ugc.AwemeInhouse" { - urlPrefix = s.douyinUrlPrefix - } else if packageName == "com.ss.iphone.ugc.awemeinhouse.lite" { - urlPrefix = s.douyinLiteUrlPrefix - } else { - return "", fmt.Errorf("not support app %s", packageName) - } - return urlPrefix, nil -} diff --git a/plugin.go b/plugin.go index 30926115..6baaac7a 100644 --- a/plugin.go +++ b/plugin.go @@ -6,11 +6,11 @@ import ( "strings" "sync" - "github.com/httprunner/funplugin" - "github.com/httprunner/funplugin/myexec" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/funplugin" + "github.com/httprunner/funplugin/myexec" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/sdk" @@ -37,12 +37,12 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er Bool("logOn", logOn).Msg("init plugin") // plugin file not found if path == "" { - return nil, nil + return nil, errors.New("testcase path not specified") } - pluginPath, err := locatePlugin(path) + pluginPath, err := LocatePlugin(path) if err != nil { log.Warn().Str("path", path).Msg("locate plugin failed") - return nil, nil + return nil, errors.Wrap(err, "locate plugin failed") } pluginMutex.Lock() @@ -100,21 +100,21 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er return } -func locatePlugin(path string) (pluginPath string, err error) { +func LocatePlugin(path string) (pluginPath string, err error) { log.Info().Str("path", path).Msg("locate plugin") // priority: hashicorp plugin (debugtalk.bin > debugtalk.py) > go plugin (debugtalk.so) - pluginPath, err = locateFile(path, PluginHashicorpGoBuiltFile) + pluginPath, err = LocateFile(path, PluginHashicorpGoBuiltFile) if err == nil { return } - pluginPath, err = locateFile(path, PluginPySourceFile) + pluginPath, err = LocateFile(path, PluginPySourceFile) if err == nil { return } - pluginPath, err = locateFile(path, PluginGoBuiltFile) + pluginPath, err = LocateFile(path, PluginGoBuiltFile) if err == nil { return } @@ -122,9 +122,9 @@ func locatePlugin(path string) (pluginPath string, err error) { return "", errors.New("plugin file not found") } -// locateFile searches destFile upward recursively until system root dir +// LocateFile searches destFile upward recursively until system root dir // if not found, then searches in hrp executable dir -func locateFile(startPath string, destFile string) (pluginPath string, err error) { +func LocateFile(startPath string, destFile string) (pluginPath string, err error) { stat, err := os.Stat(startPath) if os.IsNotExist(err) { return "", errors.Wrap(err, "start path not exists") @@ -153,7 +153,7 @@ func locateFile(startPath string, destFile string) (pluginPath string, err error return "", errors.New("searched to system root dir, plugin file not found") } - return locateFile(parentDir, destFile) + return LocateFile(parentDir, destFile) } // locateExecutable finds destFile in hrp executable dir @@ -173,13 +173,13 @@ func locateExecutable(destFile string) (string, error) { } func GetProjectRootDirPath(path string) (rootDir string, err error) { - pluginPath, err := locatePlugin(path) + pluginPath, err := LocatePlugin(path) if err == nil { rootDir = filepath.Dir(pluginPath) return } // fix: no debugtalk file in project but having proj.json created by startproject - projPath, err := locateFile(path, projectInfoFile) + projPath, err := LocateFile(path, projectInfoFile) if err == nil { rootDir = filepath.Dir(projPath) return @@ -188,5 +188,5 @@ func GetProjectRootDirPath(path string) (rootDir string, err error) { // failed to locate project root dir // maybe project plugin debugtalk.xx and proj.json are not exist // use current dir instead - return config.RootDir, nil + return config.GetConfig().RootDir, nil } diff --git a/plugin_test.go b/plugin_test.go deleted file mode 100644 index 27cbaa77..00000000 --- a/plugin_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package hrp - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLocateFile(t *testing.T) { - // specify target file path - _, err := locateFile(tmpl("plugin/debugtalk.go"), PluginGoSourceFile) - if !assert.Nil(t, err) { - t.Fatal() - } - - // specify path with the same dir - _, err = locateFile(tmpl("plugin/debugtalk.py"), PluginGoSourceFile) - if !assert.Nil(t, err) { - t.Fatal() - } - - // specify target file path dir - _, err = locateFile(tmpl("plugin/"), PluginGoSourceFile) - if !assert.Nil(t, err) { - t.Fatal() - } - - // specify wrong path - _, err = locateFile(".", PluginGoSourceFile) - if !assert.Error(t, err) { - t.Fatal() - } - _, err = locateFile("/abc", PluginGoSourceFile) - if !assert.Error(t, err) { - t.Fatal() - } -} - -func TestLocatePythonPlugin(t *testing.T) { - _, err := locatePlugin(tmpl("plugin/debugtalk.py")) - if !assert.Nil(t, err) { - t.Fatal() - } -} - -func TestLocateGoPlugin(t *testing.T) { - buildHashicorpGoPlugin() - defer removeHashicorpGoPlugin() - - _, err := locatePlugin(tmpl("debugtalk.bin")) - if !assert.Nil(t, err) { - t.Fatal() - } -} diff --git a/runner.go b/runner.go index 89db76af..f850ebae 100644 --- a/runner.go +++ b/runner.go @@ -17,17 +17,18 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/httprunner/funplugin" "github.com/jinzhu/copier" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/net/http2" + "github.com/httprunner/funplugin" "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/sdk" "github.com/httprunner/httprunner/v5/internal/version" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" ) // Run starts to run testcase with default configs. @@ -281,7 +282,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { caseRunner := &CaseRunner{ TestCase: testcase, hrpRunner: r, - parser: newParser(), + parser: NewParser(), uixtDrivers: make(map[string]*uixt.XTDriver), } config := testcase.Config.Get() @@ -296,7 +297,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { // load plugin info to testcase config pluginPath := plugin.Path() - pluginContent, err := readFile(pluginPath) + pluginContent, err := builtin.LoadFile(pluginPath) if err != nil { return nil, err } @@ -407,7 +408,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { parsedConfig.WebSocketSetting.checkWebSocket() // parse testcase config parameters - parametersIterator, err := r.parser.initParametersIterator(parsedConfig) + parametersIterator, err := r.parser.InitParametersIterator(parsedConfig) if err != nil { log.Error().Err(err). Interface("parameters", parsedConfig.Parameters). diff --git a/server/app.go b/server/app.go index edc6c564..a958b2ba 100644 --- a/server/app.go +++ b/server/app.go @@ -2,11 +2,13 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt" ) -func foregroundAppHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *Router) foregroundAppHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -18,13 +20,13 @@ func foregroundAppHandler(c *gin.Context) { RenderSuccess(c, appInfo) } -func appInfoHandler(c *gin.Context) { +func (r *Router) appInfoHandler(c *gin.Context) { var appInfoReq AppInfoRequest if err := c.ShouldBindQuery(&appInfoReq); err != nil { RenderErrorValidateRequest(c, err) return } - device, err := GetDevice(c) + device, err := r.GetDevice(c) if err != nil { return } @@ -47,18 +49,18 @@ func appInfoHandler(c *gin.Context) { } } -func clearAppHandler(c *gin.Context) { +func (r *Router) clearAppHandler(c *gin.Context) { var appClearReq AppClearRequest if err := c.ShouldBindJSON(&appClearReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } - err = driver.IDriver.(*uixt.ADBDriver).AppClear(appClearReq.PackageName) + err = driver.AppClear(appClearReq.PackageName) if err != nil { RenderError(c, err) return @@ -66,13 +68,13 @@ func clearAppHandler(c *gin.Context) { RenderSuccess(c, true) } -func launchAppHandler(c *gin.Context) { +func (r *Router) launchAppHandler(c *gin.Context) { var appLaunchReq AppLaunchRequest if err := c.ShouldBindJSON(&appLaunchReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } @@ -84,13 +86,13 @@ func launchAppHandler(c *gin.Context) { RenderSuccess(c, true) } -func terminalAppHandler(c *gin.Context) { +func (r *Router) terminalAppHandler(c *gin.Context) { var appTerminalReq AppTerminalRequest if err := c.ShouldBindJSON(&appTerminalReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } @@ -102,20 +104,19 @@ func terminalAppHandler(c *gin.Context) { RenderSuccess(c, true) } -func uninstallAppHandler(c *gin.Context) { +func (r *Router) uninstallAppHandler(c *gin.Context) { var appUninstallReq AppUninstallRequest if err := c.ShouldBindJSON(&appUninstallReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } err = driver.GetDevice().Uninstall(appUninstallReq.PackageName) if err != nil { - RenderError(c, err) - return + log.Err(err).Msg("failed to uninstall app") } RenderSuccess(c, true) } diff --git a/server/context.go b/server/context.go index 40bee258..23d4f175 100644 --- a/server/context.go +++ b/server/context.go @@ -9,17 +9,17 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) -func GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) { - deviceObj, exists := c.Get("device") +func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) { var device uixt.IDevice var driver uixt.IDriver + deviceObj, exists := c.Get("device") if !exists { - device, err = GetDevice(c) + device, err = r.GetDevice(c) if err != nil { return nil, err } @@ -32,14 +32,14 @@ func GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) { RenderErrorInitDriver(c, err) return } - c.Set("driver", driver) driverExt = uixt.NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM)) + c.Set("driver", driverExt) return driverExt, nil } -func GetDevice(c *gin.Context) (device uixt.IDevice, err error) { +func (r *Router) GetDevice(c *gin.Context) (device uixt.IDevice, err error) { platform := c.Param("platform") serial := c.Param("serial") if serial == "" { @@ -54,7 +54,6 @@ func GetDevice(c *gin.Context) (device uixt.IDevice, err error) { RenderErrorInitDriver(c, err) return } - _ = device.Setup() case "ios": device, err = uixt.NewIOSDevice( option.WithUDID(serial), @@ -66,10 +65,20 @@ func GetDevice(c *gin.Context) (device uixt.IDevice, err error) { RenderErrorInitDriver(c, err) return } + case "browser": + device, err = uixt.NewBrowserDevice(option.WithBrowserID(serial)) + if err != nil { + RenderErrorInitDriver(c, err) + return + } default: err = fmt.Errorf("[%s]: invalid platform", c.HandlerName()) return } + err = device.Setup() + if err != nil { + log.Error().Err(err).Msg("setup device failed") + } c.Set("device", device) return device, nil } @@ -87,7 +96,7 @@ func RenderError(c *gin.Context, err error) { c.JSON(http.StatusInternalServerError, HttpResponse{ Code: code.GetErrorCode(err), - Message: err.Error(), + Message: "grey " + err.Error(), }, ) c.Abort() @@ -102,7 +111,7 @@ func RenderErrorInitDriver(c *gin.Context, err error) { c.JSON(http.StatusInternalServerError, HttpResponse{ Code: errCode, - Message: "init driver failed", + Message: "grey init driver failed", }, ) c.Abort() @@ -112,7 +121,7 @@ func RenderErrorValidateRequest(c *gin.Context, err error) { log.Error().Err(err).Msg("validate request failed") c.JSON(http.StatusBadRequest, HttpResponse{ Code: code.GetErrorCode(code.InvalidParamError), - Message: fmt.Sprintf("validate request param failed: %s", err.Error()), + Message: fmt.Sprintf("grey validate request param failed: %s", err.Error()), }) c.Abort() } diff --git a/server/device.go b/server/device.go index 33fde5c9..28263d7f 100644 --- a/server/device.go +++ b/server/device.go @@ -7,14 +7,14 @@ import ( "github.com/Masterminds/semver" "github.com/danielpaulus/go-ios/ios" "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/gadb" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/pkg/gadb" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) -func listDeviceHandler(c *gin.Context) { +func (r *Router) listDeviceHandler(c *gin.Context) { var deviceList []interface{} client, err := gadb.NewClient() if err == nil { @@ -88,17 +88,47 @@ func listDeviceHandler(c *gin.Context) { RenderSuccess(c, deviceList) } -func pushImageHandler(c *gin.Context) { +func createBrowserHandler(c *gin.Context) { + var createBrowserReq CreateBrowserRequest + if err := c.ShouldBindJSON(&createBrowserReq); err != nil { + RenderErrorValidateRequest(c, err) + return + } + + browserInfo, err := uixt.CreateBrowser(createBrowserReq.Timeout) + if err != nil { + RenderError(c, err) + return + } + RenderSuccess(c, browserInfo) + return +} + +func (r *Router) deleteBrowserHandler(c *gin.Context) { + driver, err := r.GetDriver(c) + if err != nil { + RenderError(c, err) + return + } + err = driver.DeleteSession() + if err != nil { + RenderError(c, err) + return + } + RenderSuccess(c, true) +} + +func (r *Router) pushImageHandler(c *gin.Context) { var pushMediaReq PushMediaRequest if err := c.ShouldBindJSON(&pushMediaReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } - imagePath, err := builtin.DownloadFileByUrl(pushMediaReq.ImageUrl) + imagePath, err := uixt.DownloadFileByUrl(pushMediaReq.ImageUrl) if path.Ext(imagePath) == "" { err = os.Rename(imagePath, imagePath+".png") if err != nil { @@ -122,8 +152,8 @@ func pushImageHandler(c *gin.Context) { RenderSuccess(c, true) } -func clearImageHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *Router) clearImageHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -135,6 +165,6 @@ func clearImageHandler(c *gin.Context) { RenderSuccess(c, true) } -func videoHandler(c *gin.Context) { +func (r *Router) videoHandler(c *gin.Context) { RenderSuccess(c, "") } diff --git a/server/ext/app.go b/server/ext/app.go index 32880b8c..233b7d9b 100644 --- a/server/ext/app.go +++ b/server/ext/app.go @@ -6,18 +6,17 @@ import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt" "github.com/httprunner/httprunner/v5/server" + "github.com/httprunner/httprunner/v5/uixt" ) -func installAppHandler(c *gin.Context) { +func (r *RouterExt) installAppHandler(c *gin.Context) { var appInstallReq AppInstallRequest if err := c.ShouldBindJSON(&appInstallReq); err != nil { server.RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } @@ -32,7 +31,7 @@ func installAppHandler(c *gin.Context) { server.RenderSuccess(c, true) return } - localMappingPath, err := builtin.DownloadFileByUrl(appInstallReq.MappingUrl) + localMappingPath, err := uixt.DownloadFileByUrl(appInstallReq.MappingUrl) if err != nil { server.RenderError(c, err) } @@ -45,7 +44,7 @@ func installAppHandler(c *gin.Context) { server.RenderError(c, err) return } - localResourceMappingPath, err := builtin.DownloadFileByUrl( + localResourceMappingPath, err := uixt.DownloadFileByUrl( appInstallReq.ResourceMappingUrl) if err != nil { server.RenderError(c, err) diff --git a/server/ext/context.go b/server/ext/context.go index 10e26ea4..dbf84bb7 100644 --- a/server/ext/context.go +++ b/server/ext/context.go @@ -5,37 +5,40 @@ import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext" "github.com/httprunner/httprunner/v5/server" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/driver_ext" ) -func GetDriver(c *gin.Context) (driverExt *driver_ext.XTDriver, err error) { - platform := c.Param("platform") - deviceObj, exists := c.Get("device") +func (r *RouterExt) GetDriver(c *gin.Context) (driverExt *driver_ext.StubXTDriver, err error) { var device uixt.IDevice - var driver uixt.IDriver + var driver driver_ext.IStubDriver + deviceObj, exists := c.Get("device") if !exists { - device, err = server.GetDevice(c) + device, err = r.GetDevice(c) if err != nil { return nil, err } } else { device = deviceObj.(uixt.IDevice) } + platform := c.Param("platform") switch strings.ToLower(platform) { case "android": driver, err = driver_ext.NewStubAndroidDriver(device.(*uixt.AndroidDevice)) case "ios": driver, err = driver_ext.NewStubIOSDriver(device.(*uixt.IOSDevice)) + case "browser": + driver, err = driver_ext.NewStubBrowserDriver(device.(*uixt.BrowserDevice)) } if err != nil { server.RenderErrorInitDriver(c, err) return } - c.Set("driver", driver) - driverExt = driver_ext.NewXTDriver(driver, + + driverExt = driver_ext.NewStubXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM)) + c.Set("driver", driverExt) return driverExt, nil } diff --git a/server/ext/handler.go b/server/ext/handler.go index 09b932dd..980b89ee 100644 --- a/server/ext/handler.go +++ b/server/ext/handler.go @@ -1,27 +1,28 @@ package server_ext import ( + "time" + "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext" "github.com/httprunner/httprunner/v5/server" ) -func loginHandler(c *gin.Context) { +func (r *RouterExt) loginHandler(c *gin.Context) { var loginReq LoginRequest if err := c.ShouldBindJSON(&loginReq); err != nil { server.RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } - info, err := driver.IDriver.(driver_ext.IStubDriver). - LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, - loginReq.Captcha, loginReq.Password) + info, err := driver.LoginNoneUI( + loginReq.PackageName, loginReq.PhoneNumber, + loginReq.Captcha, loginReq.Password) if err != nil { server.RenderError(c, err) return @@ -29,19 +30,18 @@ func loginHandler(c *gin.Context) { server.RenderSuccess(c, info) } -func logoutHandler(c *gin.Context) { +func (r *RouterExt) logoutHandler(c *gin.Context) { var logoutReq LogoutRequest if err := c.ShouldBindJSON(&logoutReq); err != nil { server.RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } - err = driver.IDriver.(driver_ext.IStubDriver). - LogoutNoneUI(logoutReq.PackageName) + err = driver.LogoutNoneUI(logoutReq.PackageName) if err != nil { server.RenderError(c, err) return @@ -49,8 +49,8 @@ func logoutHandler(c *gin.Context) { server.RenderSuccess(c, true) } -func sourceHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *RouterExt) sourceHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -58,5 +58,9 @@ func sourceHandler(c *gin.Context) { if err != nil { log.Warn().Err(err).Msg("get source failed") } + if source == "{}" || source == "" { + time.Sleep(1 * time.Second) + source, err = driver.Source() + } server.RenderSuccess(c, source) } diff --git a/server/ext/main.go b/server/ext/main.go index 11de3e8c..e53c4cba 100644 --- a/server/ext/main.go +++ b/server/ext/main.go @@ -4,13 +4,23 @@ import ( "github.com/httprunner/httprunner/v5/server" ) -func NewExtRouter() *server.Router { - router := server.NewRouter() - apiV1PlatformSerial := router.Group("/api/v1").Group("/:platform").Group("/:serial") +type RouterExt struct { + *server.Router +} - apiV1PlatformSerial.GET("/stub/source", sourceHandler) - apiV1PlatformSerial.POST("/stub/login", loginHandler) - apiV1PlatformSerial.POST("/stub/logout", logoutHandler) - apiV1PlatformSerial.POST("/app/install", installAppHandler) +func NewExtRouter() *RouterExt { + router := &RouterExt{ + Router: server.NewRouter(), + } + router.Init() return router } + +func (r *RouterExt) Init() { + apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial") + + apiV1PlatformSerial.GET("/stub/source", r.sourceHandler) + apiV1PlatformSerial.POST("/stub/login", r.loginHandler) + apiV1PlatformSerial.POST("/stub/logout", r.logoutHandler) + apiV1PlatformSerial.POST("/app/install", r.installAppHandler) +} diff --git a/server/ext/model.go b/server/ext/model.go index 4afa6caf..dcd8adb6 100644 --- a/server/ext/model.go +++ b/server/ext/model.go @@ -9,7 +9,7 @@ type AppInstallRequest struct { type LoginRequest struct { PackageName string `json:"packageName"` - PhoneNumber string `json:"phoneNumber" binding:"required"` + PhoneNumber string `json:"phoneNumber"` Captcha string `json:"captcha" binding:"required_without=Password"` Password string `json:"password" binding:"required_without=Captcha"` } @@ -19,7 +19,7 @@ type LogoutRequest struct { } type HttpResponse struct { - Code int `json:"code"` - Message string `json:"msg"` + Code int `json:"errorCode"` + Message string `json:"errorMsg"` Result interface{} `json:"result,omitempty"` } diff --git a/server/key.go b/server/key.go index e982c087..d129044b 100644 --- a/server/key.go +++ b/server/key.go @@ -3,11 +3,11 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" ) -func unlockHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *Router) unlockHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -19,8 +19,8 @@ func unlockHandler(c *gin.Context) { RenderSuccess(c, true) } -func homeHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *Router) homeHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -32,7 +32,7 @@ func homeHandler(c *gin.Context) { RenderSuccess(c, true) } -func backspaceHandler(c *gin.Context) { +func (r *Router) backspaceHandler(c *gin.Context) { var deleteReq DeleteRequest if err := c.ShouldBindJSON(&deleteReq); err != nil { RenderErrorValidateRequest(c, err) @@ -41,7 +41,7 @@ func backspaceHandler(c *gin.Context) { if deleteReq.Count == 0 { deleteReq.Count = 20 } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } @@ -53,13 +53,13 @@ func backspaceHandler(c *gin.Context) { RenderSuccess(c, true) } -func keycodeHandler(c *gin.Context) { +func (r *Router) keycodeHandler(c *gin.Context) { var keycodeReq KeycodeRequest if err := c.ShouldBindJSON(&keycodeReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } diff --git a/server/main.go b/server/main.go index 3a125ce9..2877a859 100644 --- a/server/main.go +++ b/server/main.go @@ -4,14 +4,16 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) func NewRouter() *Router { - router := &Router{} + router := &Router{ + Engine: gin.Default(), + } router.Init() return router } @@ -21,46 +23,51 @@ type Router struct { } func (r *Router) Init() { - r.Engine = gin.Default() - r.Engine.Use(teardown()) - r.Engine.GET("/ping", pingHandler) - r.Engine.GET("/", pingHandler) - r.Engine.POST("/", pingHandler) - r.Engine.GET("/api/v1/devices", listDeviceHandler) + r.Engine.Use(r.teardown()) + r.Engine.GET("/ping", r.pingHandler) + r.Engine.GET("/", r.pingHandler) + r.Engine.POST("/", r.pingHandler) + r.Engine.GET("/api/v1/devices", r.listDeviceHandler) + r.Engine.POST("/api/v1/browser/create_browser", createBrowserHandler) apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial") // UI operations - apiV1PlatformSerial.POST("/ui/tap", tapHandler) - apiV1PlatformSerial.POST("/ui/double_tap", doubleTapHandler) - apiV1PlatformSerial.POST("/ui/drag", dragHandler) - apiV1PlatformSerial.POST("/ui/input", inputHandler) - apiV1PlatformSerial.POST("/ui/home", homeHandler) + apiV1PlatformSerial.POST("/ui/tap", r.tapHandler) + apiV1PlatformSerial.POST("/ui/right_click", r.rightClickHandler) + apiV1PlatformSerial.POST("/ui/double_tap", r.doubleTapHandler) + apiV1PlatformSerial.POST("/ui/drag", r.dragHandler) + apiV1PlatformSerial.POST("/ui/input", r.inputHandler) + apiV1PlatformSerial.POST("/ui/home", r.homeHandler) + apiV1PlatformSerial.POST("/ui/upload", r.uploadHandler) + apiV1PlatformSerial.POST("/ui/hover", r.hoverHandler) + apiV1PlatformSerial.POST("/ui/scroll", r.scrollHandler) // Key operations - apiV1PlatformSerial.POST("/key/unlock", unlockHandler) - apiV1PlatformSerial.POST("/key/home", homeHandler) - apiV1PlatformSerial.POST("/key/backspace", backspaceHandler) - apiV1PlatformSerial.POST("/key", keycodeHandler) + apiV1PlatformSerial.POST("/key/unlock", r.unlockHandler) + apiV1PlatformSerial.POST("/key/home", r.homeHandler) + apiV1PlatformSerial.POST("/key/backspace", r.backspaceHandler) + apiV1PlatformSerial.POST("/key", r.keycodeHandler) // APP operations - apiV1PlatformSerial.GET("/app/foreground", foregroundAppHandler) - apiV1PlatformSerial.GET("/app/appInfo", appInfoHandler) - apiV1PlatformSerial.POST("/app/clear", clearAppHandler) - apiV1PlatformSerial.POST("/app/launch", launchAppHandler) - apiV1PlatformSerial.POST("/app/terminal", terminalAppHandler) - apiV1PlatformSerial.POST("/app/uninstall", uninstallAppHandler) + apiV1PlatformSerial.GET("/app/foreground", r.foregroundAppHandler) + apiV1PlatformSerial.GET("/app/appInfo", r.appInfoHandler) + apiV1PlatformSerial.POST("/app/clear", r.clearAppHandler) + apiV1PlatformSerial.POST("/app/launch", r.launchAppHandler) + apiV1PlatformSerial.POST("/app/terminal", r.terminalAppHandler) + apiV1PlatformSerial.POST("/app/uninstall", r.uninstallAppHandler) // Device operations - apiV1PlatformSerial.GET("/screenshot", screenshotHandler) - apiV1PlatformSerial.GET("/video", videoHandler) - apiV1PlatformSerial.POST("/device/push_image", pushImageHandler) - apiV1PlatformSerial.POST("/device/clear_image", clearImageHandler) - apiV1PlatformSerial.GET("/adb/source", adbSourceHandler) + apiV1PlatformSerial.GET("/screenshot", r.screenshotHandler) + apiV1PlatformSerial.DELETE("/close_browser", r.deleteBrowserHandler) + apiV1PlatformSerial.GET("/video", r.videoHandler) + apiV1PlatformSerial.POST("/device/push_image", r.pushImageHandler) + apiV1PlatformSerial.POST("/device/clear_image", r.clearImageHandler) + apiV1PlatformSerial.GET("/adb/source", r.adbSourceHandler) // uixt operations - apiV1PlatformSerial.POST("/uixt/action", uixtActionHandler) - apiV1PlatformSerial.POST("/uixt/actions", uixtActionsHandler) + apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler) + apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler) } func (r *Router) Run(port int) error { @@ -72,11 +79,11 @@ func (r *Router) Run(port int) error { return nil } -func pingHandler(c *gin.Context) { +func (r *Router) pingHandler(c *gin.Context) { RenderSuccess(c, true) } -func teardown() gin.HandlerFunc { +func (r *Router) teardown() gin.HandlerFunc { return func(c *gin.Context) { logID := c.Request.Header.Get("x-tt-logid") startTime := time.Now() @@ -99,7 +106,7 @@ func teardown() gin.HandlerFunc { deviceObj, exists := c.Get("device") if exists { - if device, ok := deviceObj.(*uixt.IOSDevice); ok { + if device, ok := deviceObj.(uixt.IDevice); ok { err := device.Teardown() if err != nil { log.Error().Err(err) diff --git a/server/model.go b/server/model.go index 41e85a27..b64981a1 100644 --- a/server/model.go +++ b/server/model.go @@ -1,7 +1,7 @@ package server import ( - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) type TapRequest struct { @@ -11,6 +11,13 @@ type TapRequest struct { Options *option.ActionOptions `json:"options,omitempty"` } +type uploadRequest struct { + X float64 `json:"x"` + Y float64 `json:"y"` + FileUrl string `json:"file_url"` + FileFormat string `json:"file_format"` +} + type DragRequest struct { FromX float64 `json:"from_x" binding:"required"` FromY float64 `json:"from_y" binding:"required"` @@ -71,8 +78,8 @@ type OperateRequest struct { } type HttpResponse struct { - Code int `json:"code"` - Message string `json:"msg"` + Code int `json:"errorCode"` + Message string `json:"errorMsg"` Result interface{} `json:"result,omitempty"` } diff --git a/server/source.go b/server/source.go index fd184f12..87cbec7a 100644 --- a/server/source.go +++ b/server/source.go @@ -6,11 +6,11 @@ import ( "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) -func screenshotHandler(c *gin.Context) { - driver, err := GetDriver(c) +func (r *Router) screenshotHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -23,8 +23,8 @@ func screenshotHandler(c *gin.Context) { RenderSuccess(c, base64.StdEncoding.EncodeToString(raw.Bytes())) } -func screenResultHandler(c *gin.Context) { - dExt, err := GetDriver(c) +func (r *Router) screenResultHandler(c *gin.Context) { + driver, err := r.GetDriver(c) if err != nil { return } @@ -40,7 +40,7 @@ func screenResultHandler(c *gin.Context) { actionOptions = screenReq.Options.Options() } - screenResult, err := dExt.GetScreenResult(actionOptions...) + screenResult, err := driver.GetScreenResult(actionOptions...) if err != nil { log.Err(err).Msg("get screen result failed") RenderError(c, err) @@ -49,8 +49,8 @@ func screenResultHandler(c *gin.Context) { RenderSuccess(c, screenResult) } -func adbSourceHandler(c *gin.Context) { - dExt, err := GetDriver(c) +func (r *Router) adbSourceHandler(c *gin.Context) { + dExt, err := r.GetDriver(c) if err != nil { return } diff --git a/server/ui.go b/server/ui.go index 7cb63855..80cf4199 100644 --- a/server/ui.go +++ b/server/ui.go @@ -2,25 +2,25 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) -func tapHandler(c *gin.Context) { +func (r *Router) tapHandler(c *gin.Context) { var tapReq TapRequest if err := c.ShouldBindJSON(&tapReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } if tapReq.Duration > 0 { err = driver.Drag(tapReq.X, tapReq.Y, tapReq.X, tapReq.Y, - option.WithDuration(tapReq.Duration), - option.WithAbsoluteCoordinate(true)) + option.WithDuration(tapReq.Duration)) } else { - err = driver.TapAbsXY(tapReq.X, tapReq.Y) + err = driver.TapXY(tapReq.X, tapReq.Y) } if err != nil { RenderError(c, err) @@ -29,25 +29,106 @@ func tapHandler(c *gin.Context) { RenderSuccess(c, true) } -func doubleTapHandler(c *gin.Context) { +func (r *Router) rightClickHandler(c *gin.Context) { + var rightClickReq TapRequest + if err := c.ShouldBindJSON(&rightClickReq); err != nil { + RenderErrorValidateRequest(c, err) + return + } + driver, err := r.GetDriver(c) + if err != nil { + return + } + err = driver.IDriver.(*uixt.BrowserDriver). + RightClick(rightClickReq.X, rightClickReq.Y) + if err != nil { + RenderError(c, err) + return + } + RenderSuccess(c, true) +} + +func (r *Router) uploadHandler(c *gin.Context) { + var uploadRequest uploadRequest + if err := c.ShouldBindJSON(&uploadRequest); err != nil { + RenderErrorValidateRequest(c, err) + return + } + + driver, err := r.GetDriver(c) + if err != nil { + RenderError(c, err) + return + } + err = driver.IDriver.(*uixt.BrowserDriver). + UploadFile(uploadRequest.X, uploadRequest.Y, + uploadRequest.FileUrl, uploadRequest.FileFormat) + if err != nil { + c.Abort() + return + } + RenderSuccess(c, true) +} + +func (r *Router) hoverHandler(c *gin.Context) { + var hoverReq HoverRequest + if err := c.ShouldBindJSON(&hoverReq); err != nil { + RenderErrorValidateRequest(c, err) + return + } + + driver, err := r.GetDriver(c) + if err != nil { + RenderError(c, err) + return + } + + err = driver.IDriver.(*uixt.BrowserDriver). + Hover(hoverReq.X, hoverReq.Y) + + if err != nil { + RenderError(c, err) + return + } + RenderSuccess(c, true) +} + +func (r *Router) scrollHandler(c *gin.Context) { + var scrollReq ScrollRequest + if err := c.ShouldBindJSON(&scrollReq); err != nil { + RenderErrorValidateRequest(c, err) + return + } + + driver, err := r.GetDriver(c) + if err != nil { + RenderError(c, err) + return + } + + err = driver.IDriver.(*uixt.BrowserDriver). + Scroll(scrollReq.Delta) + + if err != nil { + RenderError(c, err) + return + } + RenderSuccess(c, true) +} + +func (r *Router) doubleTapHandler(c *gin.Context) { var tapReq TapRequest if err := c.ShouldBindJSON(&tapReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } - if tapReq.X < 1 && tapReq.Y < 1 { - err = driver.DoubleTapXY(tapReq.X, tapReq.Y) - } else { - err = driver.DoubleTapXY(tapReq.X, tapReq.Y, - option.WithAbsoluteCoordinate(true)) - } - + err = driver.DoubleTap(tapReq.X, tapReq.Y) if err != nil { RenderError(c, err) return @@ -55,7 +136,7 @@ func doubleTapHandler(c *gin.Context) { RenderSuccess(c, true) } -func dragHandler(c *gin.Context) { +func (r *Router) dragHandler(c *gin.Context) { var dragReq DragRequest if err := c.ShouldBindJSON(&dragReq); err != nil { RenderErrorValidateRequest(c, err) @@ -64,14 +145,14 @@ func dragHandler(c *gin.Context) { if dragReq.Duration == 0 { dragReq.Duration = 1 } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } err = driver.Drag(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, - option.WithDuration(dragReq.Duration), option.WithPressDuration(dragReq.PressDuration), - option.WithAbsoluteCoordinate(true)) + option.WithDuration(dragReq.Duration), + option.WithPressDuration(dragReq.PressDuration)) if err != nil { RenderError(c, err) return @@ -79,13 +160,13 @@ func dragHandler(c *gin.Context) { RenderSuccess(c, true) } -func inputHandler(c *gin.Context) { +func (r *Router) inputHandler(c *gin.Context) { var inputReq InputRequest if err := c.ShouldBindJSON(&inputReq); err != nil { RenderErrorValidateRequest(c, err) return } - driver, err := GetDriver(c) + driver, err := r.GetDriver(c) if err != nil { return } diff --git a/server/ui_test.go b/server/ui_test.go new file mode 100644 index 00000000..aeb453e0 --- /dev/null +++ b/server/ui_test.go @@ -0,0 +1,73 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTapHandler(t *testing.T) { + router := NewRouter() + + tests := []struct { + name string + path string + tapReq TapRequest + wantStatus int + wantResp HttpResponse + }{ + { + name: "tap abs xy", + path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), + tapReq: TapRequest{ + X: 500, + Y: 800, + Duration: 0, + }, + wantStatus: http.StatusOK, + wantResp: HttpResponse{ + Code: 0, + Message: "success", + Result: true, + }, + }, + { + name: "tap relative xy", + path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), + tapReq: TapRequest{ + X: 0.5, + Y: 0.6, + Duration: 0, + }, + wantStatus: http.StatusOK, + wantResp: HttpResponse{ + Code: 0, + Message: "success", + Result: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqBody, _ := json.Marshal(tt.tapReq) + req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, tt.wantStatus, w.Code) + + var got HttpResponse + err := json.Unmarshal(w.Body.Bytes(), &got) + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + }) + } +} diff --git a/server/uixt.go b/server/uixt.go index c40d430e..52f17764 100644 --- a/server/uixt.go +++ b/server/uixt.go @@ -2,13 +2,13 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" "github.com/rs/zerolog/log" ) // exec a single uixt action -func uixtActionHandler(c *gin.Context) { - dExt, err := GetDriver(c) +func (r *Router) uixtActionHandler(c *gin.Context) { + dExt, err := r.GetDriver(c) if err != nil { return } @@ -29,8 +29,8 @@ func uixtActionHandler(c *gin.Context) { } // exec multiple uixt actions -func uixtActionsHandler(c *gin.Context) { - dExt, err := GetDriver(c) +func (r *Router) uixtActionsHandler(c *gin.Context) { + dExt, err := r.GetDriver(c) if err != nil { return } diff --git a/step.go b/step.go index da7dd6f4..b3f67a41 100644 --- a/step.go +++ b/step.go @@ -1,6 +1,6 @@ package hrp -import "github.com/httprunner/httprunner/v5/pkg/uixt" +import "github.com/httprunner/httprunner/v5/uixt" type StepType string diff --git a/step_function.go b/step_function.go index a53a86ea..5436957a 100644 --- a/step_function.go +++ b/step_function.go @@ -5,9 +5,10 @@ import ( "os" "time" - "github.com/httprunner/httprunner/v5/pkg/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt" ) // StepFunction implements IStep interface. diff --git a/step_request.go b/step_request.go index 041615fa..4a06ac6d 100644 --- a/step_request.go +++ b/step_request.go @@ -22,21 +22,21 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/internal/httpstat" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/httpstat" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) type HTTPMethod string const ( - httpGET HTTPMethod = "GET" - httpHEAD HTTPMethod = "HEAD" - httpPOST HTTPMethod = "POST" - httpPUT HTTPMethod = "PUT" - httpDELETE HTTPMethod = "DELETE" - httpOPTIONS HTTPMethod = "OPTIONS" - httpPATCH HTTPMethod = "PATCH" + HTTP_GET HTTPMethod = "GET" + HTTP_HEAD HTTPMethod = "HEAD" + HTTP_POST HTTPMethod = "POST" + HTTP_PUT HTTPMethod = "PUT" + HTTP_DELETE HTTPMethod = "DELETE" + HTTP_OPTIONS HTTPMethod = "OPTIONS" + HTTP_PATCH HTTPMethod = "PATCH" ) // Request represents HTTP request data structure. @@ -566,11 +566,11 @@ func (s *StepRequest) Loop(times int) *StepRequest { // GET makes a HTTP GET request. func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpGET + s.Request.Method = HTTP_GET s.Request.URL = url } else { s.Request = &Request{ - Method: httpGET, + Method: HTTP_GET, URL: url, } } @@ -582,11 +582,11 @@ func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs { // HEAD makes a HTTP HEAD request. func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpHEAD + s.Request.Method = HTTP_HEAD s.Request.URL = url } else { s.Request = &Request{ - Method: httpHEAD, + Method: HTTP_HEAD, URL: url, } } @@ -598,11 +598,11 @@ func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs { // POST makes a HTTP POST request. func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpPOST + s.Request.Method = HTTP_POST s.Request.URL = url } else { s.Request = &Request{ - Method: httpPOST, + Method: HTTP_POST, URL: url, } } @@ -614,11 +614,11 @@ func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs { // PUT makes a HTTP PUT request. func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpPUT + s.Request.Method = HTTP_PUT s.Request.URL = url } else { s.Request = &Request{ - Method: httpPUT, + Method: HTTP_PUT, URL: url, } } @@ -630,11 +630,11 @@ func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs { // DELETE makes a HTTP DELETE request. func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpDELETE + s.Request.Method = HTTP_DELETE s.Request.URL = url } else { s.Request = &Request{ - Method: httpDELETE, + Method: HTTP_DELETE, URL: url, } } @@ -646,11 +646,11 @@ func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs { // OPTIONS makes a HTTP OPTIONS request. func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpOPTIONS + s.Request.Method = HTTP_OPTIONS s.Request.URL = url } else { s.Request = &Request{ - Method: httpOPTIONS, + Method: HTTP_OPTIONS, URL: url, } } @@ -662,11 +662,11 @@ func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs { // PATCH makes a HTTP PATCH request. func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs { if s.Request != nil { - s.Request.Method = httpPATCH + s.Request.Method = HTTP_PATCH s.Request.URL = url } else { s.Request = &Request{ - Method: httpPATCH, + Method: HTTP_PATCH, URL: url, } } diff --git a/step_request_response_test.go b/step_request_response_test.go deleted file mode 100644 index d10a1454..00000000 --- a/step_request_response_test.go +++ /dev/null @@ -1 +0,0 @@ -package hrp diff --git a/step_thinktime.go b/step_thinktime.go index bfb986c3..596ad676 100644 --- a/step_thinktime.go +++ b/step_thinktime.go @@ -42,14 +42,14 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { cfg := r.caseRunner.Config.Get().ThinkTimeSetting if cfg == nil { - cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} + cfg = &ThinkTimeConfig{ThinkTimeDefault, nil, 0} } var tt time.Duration switch cfg.Strategy { - case thinkTimeDefault: + case ThinkTimeDefault: tt = time.Duration(thinkTime.Time*1000) * time.Millisecond - case thinkTimeRandomPercentage: + case ThinkTimeRandomPercentage: // e.g. {"min_percentage": 0.5, "max_percentage": 1.5} m, ok := cfg.Setting.(map[string]float64) if !ok { @@ -58,13 +58,13 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { } res := builtin.GetRandomNumber(int(thinkTime.Time*m["min_percentage"]*1000), int(thinkTime.Time*m["max_percentage"]*1000)) tt = time.Duration(res) * time.Millisecond - case thinkTimeMultiply: + case ThinkTimeMultiply: value, ok := cfg.Setting.(float64) // e.g. 0.5 if !ok || value <= 0 { value = thinkTimeDefaultMultiply } tt = time.Duration(thinkTime.Time*value*1000) * time.Millisecond - case thinkTimeIgnore: + case ThinkTimeIgnore: // nothing to do } diff --git a/step_ui.go b/step_ui.go index 5bcefd91..4dafc3d5 100644 --- a/step_ui.go +++ b/step_ui.go @@ -10,8 +10,8 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/sdk" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" ) type MobileUI struct { @@ -103,7 +103,9 @@ func (s *StepMobile) Home() *StepMobile { return s } -// TapXY taps the point {X,Y}, X & Y is percentage of coordinates +// TapXY taps the point {X,Y} +// if X<1 & Y<1, {X,Y} will be considered as percentage +// else, X & Y will be considered as absolute coordinates func (s *StepMobile) TapXY(x, y float64, opts ...option.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_TapXY, diff --git a/step_websocket.go b/step_websocket.go index 39b23e48..84b20b81 100644 --- a/step_websocket.go +++ b/step_websocket.go @@ -220,28 +220,130 @@ func (s *StepWebSocket) WithCloseStatus(closeStatus int64) *StepWebSocket { } // Validate switches to step validation. -func (s *StepWebSocket) Validate() *StepRequestValidation { - return &StepRequestValidation{ - StepRequestWithOptionalArgs: &StepRequestWithOptionalArgs{ - StepRequest: &StepRequest{ - StepConfig: s.StepConfig, - }, - }, +func (s *StepWebSocket) Validate() *StepWebSocketValidation { + return &StepWebSocketValidation{ + StepWebSocket: s, } } // Extract switches to step extraction. -func (s *StepWebSocket) Extract() *StepRequestExtraction { +func (s *StepWebSocket) Extract() *StepWebSocketExtraction { s.StepConfig.Extract = make(map[string]string) - return &StepRequestExtraction{ - StepRequestWithOptionalArgs: &StepRequestWithOptionalArgs{ - StepRequest: &StepRequest{ - StepConfig: s.StepConfig, - }, - }, + return &StepWebSocketExtraction{ + StepWebSocket: s, } } +// StepWebSocketExtraction implements IStep interface. +type StepWebSocketExtraction struct { + *StepWebSocket +} + +// WithJmesPath sets the JMESPath expression to extract from the response. +func (s *StepWebSocketExtraction) WithJmesPath(jmesPath string, varName string) *StepWebSocketExtraction { + s.StepConfig.Extract[varName] = jmesPath + return s +} + +// Validate switches to step validation. +func (s *StepWebSocketExtraction) Validate() *StepWebSocketValidation { + return &StepWebSocketValidation{ + StepWebSocket: s.StepWebSocket, + } +} + +func (s *StepWebSocketExtraction) Name() string { + return s.StepName +} + +func (s *StepWebSocketExtraction) Type() StepType { + stepType := StepType(fmt.Sprintf("websocket-%v", s.WebSocket.Type)) + return stepType + stepTypeSuffixExtraction +} + +func (s *StepWebSocketExtraction) Struct() *StepConfig { + return &s.StepConfig +} + +func (s *StepWebSocketExtraction) Run(r *SessionRunner) (*StepResult, error) { + if s.WebSocket != nil { + return runStepWebSocket(r, s.StepWebSocket) + } + return nil, errors.New("unexpected protocol type") +} + +// StepWebSocketValidation implements IStep interface. +type StepWebSocketValidation struct { + *StepWebSocket +} + +func (s *StepWebSocketValidation) Name() string { + if s.StepName != "" { + return s.StepName + } + return fmt.Sprintf("%s %s", s.WebSocket.Type, s.WebSocket.URL) +} + +func (s *StepWebSocketValidation) Type() StepType { + stepType := StepType(fmt.Sprintf("websocket-%v", s.WebSocket.Type)) + return stepType + stepTypeSuffixValidation +} + +func (s *StepWebSocketValidation) Config() *StepConfig { + return &s.StepConfig +} + +func (s *StepWebSocketValidation) Run(r *SessionRunner) (*StepResult, error) { + if s.WebSocket != nil { + return runStepWebSocket(r, s.StepWebSocket) + } + return nil, errors.New("unexpected protocol type") +} + +func (s *StepWebSocketValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation { + v := Validator{ + Check: jmesPath, + Assert: "equals", + Expect: expected, + Message: msg, + } + s.Validators = append(s.Validators, v) + return s +} + +func (s *StepWebSocketValidation) AssertEqualFold(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation { + v := Validator{ + Check: jmesPath, + Assert: "equal_fold", + Expect: expected, + Message: msg, + } + s.Validators = append(s.Validators, v) + return s +} + +func (s *StepWebSocketValidation) AssertLengthEqual(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation { + v := Validator{ + Check: jmesPath, + Assert: "length_equals", + Expect: expected, + Message: msg, + } + s.Validators = append(s.Validators, v) + return s +} + +func (s *StepWebSocketValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepWebSocketValidation { + v := Validator{ + Check: jmesPath, + Assert: "contains", + Expect: expected, + Message: msg, + } + s.Validators = append(s.Validators, v) + return s +} + type WebSocketAction struct { Type WSActionType `json:"type" yaml:"type"` URL string `json:"url" yaml:"url"` diff --git a/summary.go b/summary.go index cb6fd3a1..0cac3117 100644 --- a/summary.go +++ b/summary.go @@ -15,7 +15,7 @@ import ( "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/version" - "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/uixt" ) func NewSummary() *Summary { @@ -32,7 +32,7 @@ func NewSummary() *Summary { }, }, Time: &TestCaseTime{ - StartAt: config.StartTime, + StartAt: config.GetConfig().StartTime, }, Platform: platForm, } @@ -67,7 +67,7 @@ func (s *Summary) AddCaseSummary(caseSummary *TestCaseSummary) { s.rootDir = caseSummary.RootDir } else if s.rootDir != caseSummary.RootDir { // if multiple testcases have different root path, use current working dir - s.rootDir = config.RootDir + s.rootDir = config.GetConfig().RootDir } // merge action stats @@ -80,7 +80,7 @@ func (s *Summary) AddCaseSummary(caseSummary *TestCaseSummary) { } func (s *Summary) SetupDirPath() (path string, err error) { - dirPath := filepath.Join(s.rootDir, config.ResultsDir) + dirPath := filepath.Join(s.rootDir, config.GetConfig().ResultsDir) err = builtin.EnsureFolderExists(dirPath) if err != nil { return "", err diff --git a/tests/build_test.go b/tests/build_test.go new file mode 100644 index 00000000..5063da22 --- /dev/null +++ b/tests/build_test.go @@ -0,0 +1,32 @@ +package tests + +import ( + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + + hrp "github.com/httprunner/httprunner/v5" + "github.com/httprunner/httprunner/v5/internal/builtin" +) + +func TestRun(t *testing.T) { + err := hrp.BuildPlugin(tmpl("plugin/debugtalk.go"), "./debugtalk.bin") + assert.Nil(t, err) + + genDebugTalkPyPath := filepath.Join(tmpl("plugin/"), hrp.PluginPySourceGenFile) + err = hrp.BuildPlugin(tmpl("plugin/debugtalk.py"), genDebugTalkPyPath) + assert.Nil(t, err) + + contentBytes, err := builtin.LoadFile(genDebugTalkPyPath) + assert.Nil(t, err) + + content := string(contentBytes) + assert.Contains(t, content, "import funppy") + assert.Contains(t, content, "funppy.register") + + reg, _ := regexp.Compile(`funppy\.register`) + matchedSlice := reg.FindAllStringSubmatch(content, -1) + assert.Len(t, matchedSlice, 10) +} diff --git a/tests/loader_test.go b/tests/loader_test.go new file mode 100644 index 00000000..851c9fc8 --- /dev/null +++ b/tests/loader_test.go @@ -0,0 +1,65 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + hrp "github.com/httprunner/httprunner/v5" +) + +func TestLoadTestCases(t *testing.T) { + // load test cases from folder path + tc := hrp.TestCasePath("../examples/demo-with-py-plugin/testcases/") + testCases, err := hrp.LoadTestCases(&tc) + assert.Nil(t, err) + assert.Equal(t, 3, len(testCases)) + + // load test cases from folder path, including sub folders + tc = hrp.TestCasePath("../examples/demo-with-py-plugin/") + testCases, err = hrp.LoadTestCases(&tc) + assert.Nil(t, err) + assert.Equal(t, 3, len(testCases)) + + // load test cases from single file path + tc = hrp.TestCasePath(demoTestCaseWithPluginJSONPath) + testCases, err = hrp.LoadTestCases(&tc) + assert.Nil(t, err) + assert.Equal(t, 1, len(testCases)) + + // load test cases from TestCase instance + testcase := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase").SetWeight(3), + } + testCases, err = hrp.LoadTestCases(testcase) + assert.Nil(t, err) + assert.Equal(t, len(testCases), 1) + + // load test cases from TestCaseJSON + testcaseJSON := hrp.TestCaseJSON(` + { + "config":{"name":"TestCaseJSON"}, + "teststeps":[ + {"name": "step1", "request":{"url": "https://httpbin.org/get"}}, + {"name": "step2", "shell":{"string": "ls -l"}} + ] + }`) + testCases, err = hrp.LoadTestCases(&testcaseJSON) + assert.Nil(t, err) + assert.Equal(t, len(testCases), 1) +} + +func TestLoadCase(t *testing.T) { + tcJSON := &hrp.TestCaseDef{} + tcYAML := &hrp.TestCaseDef{} + err := hrp.LoadFileObject(demoTestCaseWithPluginJSONPath, tcJSON) + assert.Nil(t, err) + + err = hrp.LoadFileObject(demoTestCaseWithPluginYAMLPath, tcYAML) + assert.Nil(t, err) + + assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name) + assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL) + assert.Equal(t, tcJSON.Steps[1].StepName, tcYAML.Steps[1].StepName) + assert.Equal(t, tcJSON.Steps[1].Request, tcJSON.Steps[1].Request) +} diff --git a/parameters_test.go b/tests/parameters_test.go similarity index 77% rename from parameters_test.go rename to tests/parameters_test.go index 4d2781f2..2df83a68 100644 --- a/parameters_test.go +++ b/tests/parameters_test.go @@ -1,22 +1,24 @@ -package hrp +package tests import ( "fmt" "testing" "github.com/stretchr/testify/assert" + + hrp "github.com/httprunner/httprunner/v5" ) func TestLoadParameters(t *testing.T) { testData := []struct { configParameters map[string]interface{} - loadedParameters map[string]Parameters + loadedParameters map[string]hrp.Parameters }{ { map[string]interface{}{ "username-password": fmt.Sprintf("${parameterize(%s/$file)}", hrpExamplesDir), }, - map[string]Parameters{ + map[string]hrp.Parameters{ "username-password": { {"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}, @@ -33,7 +35,7 @@ func TestLoadParameters(t *testing.T) { "user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}, "app_version": []interface{}{4.0}, }, - map[string]Parameters{ + map[string]hrp.Parameters{ "username-password": { {"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}, @@ -54,7 +56,7 @@ func TestLoadParameters(t *testing.T) { []interface{}{"test2", "222222"}, }, }, - map[string]Parameters{ + map[string]hrp.Parameters{ "username-password": { {"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}, @@ -74,15 +76,11 @@ func TestLoadParameters(t *testing.T) { variablesMapping := map[string]interface{}{ "file": "account.csv", } - parser := newParser() + parser := hrp.NewParser() for _, data := range testData { - value, err := parser.loadParameters(data.configParameters, variablesMapping) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, data.loadedParameters, value) { - t.Fatal() - } + value, err := parser.LoadParameters(data.configParameters, variablesMapping) + assert.Nil(t, err) + assert.Equal(t, data.loadedParameters, value) } } @@ -109,12 +107,10 @@ func TestLoadParametersError(t *testing.T) { }, }, } - parser := newParser() + parser := hrp.NewParser() for _, data := range testData { - _, err := parser.loadParameters(data.configParameters, map[string]interface{}{}) - if !assert.Error(t, err) { - t.Fatal() - } + _, err := parser.LoadParameters(data.configParameters, map[string]interface{}{}) + assert.Error(t, err) } } @@ -125,28 +121,28 @@ func TestInitParametersIteratorCount(t *testing.T) { "app_version": []interface{}{4.0}, // 1 } testData := []struct { - cfg *TConfig + cfg *hrp.TConfig expectLimit int }{ // default, no parameters setting { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{}, + ParametersSetting: &hrp.TParamsConfig{}, }, 6, // 3 * 2 * 1 }, { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, }, 6, // 3 * 2 * 1 }, // default equals to set overall parameters pick-order to "sequential" { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ + ParametersSetting: &hrp.TParamsConfig{ PickOrder: "sequential", }, }, @@ -154,10 +150,10 @@ func TestInitParametersIteratorCount(t *testing.T) { }, // default equals to set each individual parameters pick-order to "sequential" { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ - Strategies: map[string]iteratorStrategy{ + ParametersSetting: &hrp.TParamsConfig{ + Strategies: map[string]hrp.IteratorStrategy{ "username-password": {Name: "user-info", PickOrder: "sequential"}, "user_agent": {Name: "user-identity", PickOrder: "sequential"}, "app_version": {Name: "app-version", PickOrder: "sequential"}, @@ -167,10 +163,10 @@ func TestInitParametersIteratorCount(t *testing.T) { 6, // 3 * 2 * 1 }, { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ - Strategies: map[string]iteratorStrategy{ + ParametersSetting: &hrp.TParamsConfig{ + Strategies: map[string]hrp.IteratorStrategy{ "user_agent": {Name: "user-identity", PickOrder: "sequential"}, "app_version": {Name: "app-version", PickOrder: "sequential"}, }, @@ -182,9 +178,9 @@ func TestInitParametersIteratorCount(t *testing.T) { // set overall parameters overall pick-order to "random" // each random parameters only select one item { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ + ParametersSetting: &hrp.TParamsConfig{ PickOrder: "random", }, }, @@ -193,10 +189,10 @@ func TestInitParametersIteratorCount(t *testing.T) { // set some individual parameters pick-order to "random" // this will override overall strategy { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ - Strategies: map[string]iteratorStrategy{ + ParametersSetting: &hrp.TParamsConfig{ + Strategies: map[string]hrp.IteratorStrategy{ "user_agent": {Name: "user-identity", PickOrder: "random"}, }, }, @@ -204,10 +200,10 @@ func TestInitParametersIteratorCount(t *testing.T) { 3, // 3 * 1 * 1 }, { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{ - Strategies: map[string]iteratorStrategy{ + ParametersSetting: &hrp.TParamsConfig{ + Strategies: map[string]hrp.IteratorStrategy{ "username-password": {Name: "user-info", PickOrder: "random"}, }, }, @@ -217,18 +213,18 @@ func TestInitParametersIteratorCount(t *testing.T) { // set limit for parameters { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, // total: 6 = 3 * 2 * 1 - ParametersSetting: &TParamsConfig{ + ParametersSetting: &hrp.TParamsConfig{ Limit: 4, // limit could be less than total }, }, 4, }, { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, // total: 6 = 3 * 2 * 1 - ParametersSetting: &TParamsConfig{ + ParametersSetting: &hrp.TParamsConfig{ Limit: 9, // limit could also be greater than total }, }, @@ -238,33 +234,25 @@ func TestInitParametersIteratorCount(t *testing.T) { // no parameters // also will generate one empty item { - &TConfig{ + &hrp.TConfig{ Parameters: nil, ParametersSetting: nil, }, 1, }, } - parser := newParser() + parser := hrp.NewParser() for _, data := range testData { - iterator, err := parser.initParametersIterator(data.cfg) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, data.expectLimit, iterator.limit) { - t.Fatal() - } + iterator, err := parser.InitParametersIterator(data.cfg) + assert.Nil(t, err) + assert.Equal(t, data.expectLimit, iterator.Limit) for i := 0; i < data.expectLimit; i++ { - if !assert.True(t, iterator.HasNext()) { - t.Fatal() - } + assert.True(t, iterator.HasNext()) iterator.Next() // consume next parameters } // should not have next - if !assert.False(t, iterator.HasNext()) { - t.Fatal() - } + assert.False(t, iterator.HasNext()) } } @@ -275,50 +263,40 @@ func TestInitParametersIteratorUnlimitedCount(t *testing.T) { "app_version": []interface{}{4.0}, // 1 } testData := []struct { - cfg *TConfig + cfg *hrp.TConfig }{ // default, no parameters setting { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, - ParametersSetting: &TParamsConfig{}, + ParametersSetting: &hrp.TParamsConfig{}, }, }, // no parameters // also will generate one empty item { - &TConfig{ + &hrp.TConfig{ Parameters: nil, ParametersSetting: nil, }, }, } - parser := newParser() + parser := hrp.NewParser() for _, data := range testData { - iterator, err := parser.initParametersIterator(data.cfg) - if !assert.Nil(t, err) { - t.Fatal() - } + iterator, err := parser.InitParametersIterator(data.cfg) + assert.Nil(t, err) // set unlimited mode iterator.SetUnlimitedMode() - if !assert.Equal(t, -1, iterator.limit) { - t.Fatal() - } + assert.Equal(t, -1, iterator.Limit) for i := 0; i < 100; i++ { - if !assert.True(t, iterator.HasNext()) { - t.Fatal() - } + assert.True(t, iterator.HasNext()) iterator.Next() // consume next parameters } - if !assert.Equal(t, 100, iterator.index) { - t.Fatal() - } + assert.Equal(t, 100, iterator.Index) // should also have next - if !assert.True(t, iterator.HasNext()) { - t.Fatal() - } + assert.True(t, iterator.HasNext()) } } @@ -329,13 +307,13 @@ func TestInitParametersIteratorContent(t *testing.T) { "app_version": []interface{}{4.0}, // 1 } testData := []struct { - cfg *TConfig + cfg *hrp.TConfig checkIndex int expectParameters map[string]interface{} }{ // default, no parameters setting { - &TConfig{ + &hrp.TConfig{ Parameters: configParameters, }, 0, // check first item @@ -346,16 +324,16 @@ func TestInitParametersIteratorContent(t *testing.T) { // set limit for parameters { - &TConfig{ + &hrp.TConfig{ Parameters: map[string]interface{}{ "username-password": []map[string]interface{}{ // 1 {"username": "test1", "password": 111111, "other": "111"}, }, "user_agent": []string{"iOS/10.1", "iOS/10.2"}, // 2 }, - ParametersSetting: &TParamsConfig{ + ParametersSetting: &hrp.TParamsConfig{ Limit: 5, // limit could also be greater than total - Strategies: map[string]iteratorStrategy{ + Strategies: map[string]hrp.IteratorStrategy{ "username-password": {Name: "user-info", PickOrder: "random"}, }, }, @@ -369,7 +347,7 @@ func TestInitParametersIteratorContent(t *testing.T) { // no parameters // also will generate one empty item { - &TConfig{ + &hrp.TConfig{ Parameters: nil, ParametersSetting: nil, }, @@ -377,35 +355,29 @@ func TestInitParametersIteratorContent(t *testing.T) { map[string]interface{}{}, }, } - parser := newParser() + parser := hrp.NewParser() for _, data := range testData { - iterator, err := parser.initParametersIterator(data.cfg) - if !assert.Nil(t, err) { - t.Fatal() - } + iterator, err := parser.InitParametersIterator(data.cfg) + assert.Nil(t, err) // get expected parameters item for i := 0; i < data.checkIndex; i++ { - if !assert.True(t, iterator.HasNext()) { - t.Fatal() - } + assert.True(t, iterator.HasNext()) iterator.Next() // consume next parameters } parametersItem := iterator.Next() - if !assert.Equal(t, data.expectParameters, parametersItem) { - t.Fatal() - } + assert.Equal(t, data.expectParameters, parametersItem) } } func TestGenCartesianProduct(t *testing.T) { testData := []struct { - multiParameters []Parameters - expect Parameters + multiParameters []hrp.Parameters + expect hrp.Parameters }{ { - []Parameters{ + []hrp.Parameters{ { {"app_version": 4.0}, }, @@ -418,7 +390,7 @@ func TestGenCartesianProduct(t *testing.T) { {"user_agent": "iOS/10.2"}, }, }, - Parameters{ + hrp.Parameters{ {"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.1", "username": "test1"}, {"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.2", "username": "test1"}, {"app_version": 4.0, "password": "222222", "user_agent": "iOS/10.1", "username": "test2"}, @@ -430,16 +402,14 @@ func TestGenCartesianProduct(t *testing.T) { nil, }, { - []Parameters{}, + []hrp.Parameters{}, nil, }, } for _, data := range testData { - parameters := genCartesianProduct(data.multiParameters) - if !assert.Equal(t, data.expect, parameters) { - t.Fatal() - } + parameters := hrp.GenCartesianProduct(data.multiParameters) + assert.Equal(t, data.expect, parameters) } } @@ -490,13 +460,9 @@ func TestConvertParameters(t *testing.T) { } for _, data := range testData { - value, err := convertParameters(data.key, data.parametersRawList) - if !assert.Nil(t, err) { - t.Fatal() - } - if !assert.Equal(t, data.expect, value) { - t.Fatal() - } + value, err := hrp.ConvertParameters(data.key, data.parametersRawList) + assert.Nil(t, err) + assert.Equal(t, data.expect, value) } } @@ -530,9 +496,7 @@ func TestConvertParametersError(t *testing.T) { } for _, data := range testData { - _, err := convertParameters(data.key, data.parametersRawList) - if !assert.Error(t, err) { - t.Fatal() - } + _, err := hrp.ConvertParameters(data.key, data.parametersRawList) + assert.Error(t, err) } } diff --git a/tests/plugin_test.go b/tests/plugin_test.go new file mode 100644 index 00000000..4764616a --- /dev/null +++ b/tests/plugin_test.go @@ -0,0 +1,42 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + hrp "github.com/httprunner/httprunner/v5" +) + +func TestLocateFile(t *testing.T) { + // specify target file path + _, err := hrp.LocateFile(tmpl("plugin/debugtalk.go"), hrp.PluginGoSourceFile) + assert.Nil(t, err) + + // specify path with the same dir + _, err = hrp.LocateFile(tmpl("plugin/debugtalk.py"), hrp.PluginGoSourceFile) + assert.Nil(t, err) + + // specify target file path dir + _, err = hrp.LocateFile(tmpl("plugin/"), hrp.PluginGoSourceFile) + assert.Nil(t, err) + + // specify wrong path + _, err = hrp.LocateFile(".", hrp.PluginGoSourceFile) + assert.Error(t, err) + _, err = hrp.LocateFile("/abc", hrp.PluginGoSourceFile) + assert.Error(t, err) +} + +func TestLocatePythonPlugin(t *testing.T) { + _, err := hrp.LocatePlugin(tmpl("plugin/debugtalk.py")) + assert.Nil(t, err) +} + +func TestLocateGoPlugin(t *testing.T) { + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() + + _, err := hrp.LocatePlugin(tmpl("debugtalk.bin")) + assert.Nil(t, err) +} diff --git a/runner_test.go b/tests/runner_test.go similarity index 64% rename from runner_test.go rename to tests/runner_test.go index 4cdf4edd..e4745a5b 100644 --- a/runner_test.go +++ b/tests/runner_test.go @@ -1,22 +1,22 @@ -package hrp +package tests import ( "errors" "fmt" "os" - "path/filepath" "testing" "time" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + hrp "github.com/httprunner/httprunner/v5" "github.com/httprunner/httprunner/v5/code" ) func buildHashicorpGoPlugin() { log.Info().Msg("[init] build hashicorp go plugin") - err := BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin")) + err := hrp.BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin")) if err != nil { log.Error().Err(err).Msg("build hashicorp go plugin failed") os.Exit(code.GetErrorCode(err)) @@ -26,8 +26,6 @@ func buildHashicorpGoPlugin() { func removeHashicorpGoPlugin() { log.Info().Msg("[teardown] remove hashicorp go plugin") os.Remove(tmpl("debugtalk.bin")) - pluginPath, _ := filepath.Abs(tmpl("debugtalk.bin")) - pluginMap.Delete(pluginPath) } func buildHashicorpPyPlugin() { @@ -43,8 +41,8 @@ func buildHashicorpPyPlugin() { func removeHashicorpPyPlugin() { log.Info().Msg("[teardown] remove hashicorp python plugin") // on v4.1^, running case will generate .debugtalk_gen.py used by python plugin - os.Remove(tmpl(PluginPySourceFile)) - os.Remove(tmpl(PluginPySourceGenFile)) + os.Remove(tmpl(hrp.PluginPySourceFile)) + os.Remove(tmpl(hrp.PluginPySourceGenFile)) } func TestRunCaseWithGoPlugin(t *testing.T) { @@ -62,21 +60,22 @@ func TestRunCaseWithPythonPlugin(t *testing.T) { } func assertRunTestCases(t *testing.T) { - refCase := TestCasePath(demoTestCaseWithPluginJSONPath) - testcase1 := &TestCase{ - Config: NewConfig("TestCase1"). - SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("testcase1-step1"). + refCase := hrp.TestCasePath(demoTestCaseWithPluginJSONPath) + testcase1 := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase1"). + SetBaseURL("https://postman-echo.com"). + EnablePlugin(), // TODO: FIXME + TestSteps: []hrp.IStep{ + hrp.NewStep("testcase1-step1"). GET("/headers"). Validate(). AssertEqual("status_code", 200, "check status code"). AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check http response Content-Type"), - NewStep("testcase1-step2").CallRefCase( - &TestCase{ - Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("ip"). + hrp.NewStep("testcase1-step2").CallRefCase( + &hrp.TestCase{ + Config: hrp.NewConfig("testcase1-step3-ref-case").SetBaseURL("https://postman-echo.com"), + TestSteps: []hrp.IStep{ + hrp.NewStep("ip"). GET("/ip"). Validate(). AssertEqual("status_code", 200, "check status code"). @@ -84,14 +83,14 @@ func assertRunTestCases(t *testing.T) { }, }, ), - NewStep("testcase1-step3").CallRefCase(&refCase), + hrp.NewStep("testcase1-step3").CallRefCase(&refCase), }, } - testcase2 := &TestCase{ - Config: NewConfig("TestCase2").SetWeight(3), + testcase2 := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase2").SetWeight(3), } - r := NewRunner(t) + r := hrp.NewRunner(t) r.SetPluginLogOn() err := r.Run(testcase1, testcase2) if err != nil { @@ -103,46 +102,46 @@ func TestRunCaseWithThinkTime(t *testing.T) { buildHashicorpGoPlugin() defer removeHashicorpGoPlugin() - testcases := []*TestCase{ + testcases := []*hrp.TestCase{ { - Config: NewConfig("TestCase1"), - TestSteps: []IStep{ - NewStep("thinkTime").SetThinkTime(2), + Config: hrp.NewConfig("TestCase1"), + TestSteps: []hrp.IStep{ + hrp.NewStep("thinkTime").SetThinkTime(2), }, }, { - Config: NewConfig("TestCase2"). - SetThinkTime(thinkTimeIgnore, nil, 0), - TestSteps: []IStep{ - NewStep("thinkTime").SetThinkTime(0.5), + Config: hrp.NewConfig("TestCase2"). + SetThinkTime(hrp.ThinkTimeIgnore, nil, 0), + TestSteps: []hrp.IStep{ + hrp.NewStep("thinkTime").SetThinkTime(0.5), }, }, { - Config: NewConfig("TestCase3"). - SetThinkTime(thinkTimeRandomPercentage, nil, 0), - TestSteps: []IStep{ - NewStep("thinkTime").SetThinkTime(1), + Config: hrp.NewConfig("TestCase3"). + SetThinkTime(hrp.ThinkTimeRandomPercentage, nil, 0), + TestSteps: []hrp.IStep{ + hrp.NewStep("thinkTime").SetThinkTime(1), }, }, { - Config: NewConfig("TestCase4"). - SetThinkTime(thinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5), - TestSteps: []IStep{ - NewStep("thinkTime").SetThinkTime(1), + Config: hrp.NewConfig("TestCase4"). + SetThinkTime(hrp.ThinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5), + TestSteps: []hrp.IStep{ + hrp.NewStep("thinkTime").SetThinkTime(1), }, }, { - Config: NewConfig("TestCase5"), - TestSteps: []IStep{ + Config: hrp.NewConfig("TestCase5"), + TestSteps: []hrp.IStep{ // think time: 3s, random pct: {"min_percentage":1, "max_percentage":1.5}, limit: 4s - NewStep("thinkTime").CallRefCase(&demoTestCaseWithThinkTimePath), + hrp.NewStep("thinkTime").CallRefCase(&demoTestCaseWithThinkTimePath), }, }, } expectedMinValue := []float64{2, 0, 0.5, 2, 3} expectedMaxValue := []float64{2.5, 0.5, 2, 3, 10} for idx, testcase := range testcases { - r := NewRunner(t) + r := hrp.NewRunner(t) startTime := time.Now() err := r.Run(testcase) if err != nil { @@ -158,20 +157,20 @@ func TestRunCaseWithThinkTime(t *testing.T) { } func TestRunCaseWithShell(t *testing.T) { - testcase1 := &TestCase{ - Config: NewConfig("complex shell with env variables"). + testcase1 := &hrp.TestCase{ + Config: hrp.NewConfig("complex shell with env variables"). WithVariables(map[string]interface{}{ "SS": "12345", "ABC": "$SS", }), - TestSteps: []IStep{ - NewStep("shell21").Shell("echo hello world"), + TestSteps: []hrp.IStep{ + hrp.NewStep("shell21").Shell("echo hello world"), // NewStep("shell21").Shell("echo $ABC"), // NewStep("shell21").Shell("which hrp"), }, } - r := NewRunner(t) + r := hrp.NewRunner(t) err := r.Run(testcase1) if err != nil { t.Fatal() @@ -193,16 +192,16 @@ func TestRunCaseWithFunction(t *testing.T) { err3 = errors.New("func3 error") fmt.Println("call function3 with return value and error") } - testcase1 := &TestCase{ - Config: NewConfig("call function"), - TestSteps: []IStep{ - NewStep("fn1").Function(fn1), - NewStep("fn2").Function(fn2), - NewStep("fn3").Function(fn3), + testcase1 := &hrp.TestCase{ + Config: hrp.NewConfig("call function"), + TestSteps: []hrp.IStep{ + hrp.NewStep("fn1").Function(fn1), + hrp.NewStep("fn2").Function(fn2), + hrp.NewStep("fn3").Function(fn3), }, } - r := NewRunner(t) + r := hrp.NewRunner(t) err := r.Run(testcase1) if err != nil { t.Fatal() @@ -220,8 +219,9 @@ func TestRunCaseWithPluginJSON(t *testing.T) { buildHashicorpGoPlugin() defer removeHashicorpGoPlugin() - testCase := TestCasePath(demoTestCaseWithPluginJSONPath) - err := NewRunner(nil).Run(&testCase) // hrp.Run(testCase) + testCase := hrp.TestCasePath(demoTestCaseWithPluginJSONPath) + // TODO: FIXME, enable plugin + err := hrp.NewRunner(nil).Run(&testCase) // hrp.Run(testCase) if err != nil { t.Fatal() } @@ -243,22 +243,22 @@ func TestRunCaseWithRefAPI(t *testing.T) { buildHashicorpGoPlugin() defer removeHashicorpGoPlugin() - testCase := TestCasePath(demoTestCaseWithRefAPIPath) - err := NewRunner(nil).Run(&testCase) + testCase := hrp.TestCasePath(demoTestCaseWithRefAPIPath) + err := hrp.NewRunner(nil).Run(&testCase) if err != nil { t.Fatal() } - refAPI := APIPath(demoAPIGETPath) - testcase := &TestCase{ - Config: NewConfig("TestCase"). + refAPI := hrp.APIPath(demoAPIGETPath) + testcase := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase"). SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("run referenced api").CallRefAPI(&refAPI), + TestSteps: []hrp.IStep{ + hrp.NewStep("run referenced api").CallRefAPI(&refAPI), }, } - r := NewRunner(t) + r := hrp.NewRunner(t) err = r.Run(testcase) if err != nil { t.Fatal() @@ -266,15 +266,15 @@ func TestRunCaseWithRefAPI(t *testing.T) { } func TestSessionRunner(t *testing.T) { - testcase := TestCase{ - Config: NewConfig("TestCase"). + testcase := hrp.TestCase{ + Config: hrp.NewConfig("TestCase"). WithVariables(map[string]interface{}{ "a": 12.3, "b": 3.45, "varFoo": "${max($a, $b)}", }), - TestSteps: []IStep{ - NewStep("check variables"). + TestSteps: []hrp.IStep{ + hrp.NewStep("check variables"). WithVariables(map[string]interface{}{ "a": 12.3, "b": 34.5, @@ -287,7 +287,7 @@ func TestSessionRunner(t *testing.T) { }, } - caseRunner, _ := NewRunner(t).NewCaseRunner(testcase) + caseRunner, _ := hrp.NewRunner(t).NewCaseRunner(testcase) sessionRunner := caseRunner.NewSession() step := testcase.TestSteps[0] if !assert.Equal(t, step.Config().Variables["varFoo"], "${max($a, $b)}") { diff --git a/step_rendezvous_test.go b/tests/step_rendezvous_test.go similarity index 77% rename from step_rendezvous_test.go rename to tests/step_rendezvous_test.go index 220719ef..75e1d881 100644 --- a/step_rendezvous_test.go +++ b/tests/step_rendezvous_test.go @@ -1,40 +1,42 @@ -package hrp +package tests import ( "math" "testing" + + hrp "github.com/httprunner/httprunner/v5" ) func TestRunCaseWithRendezvous(t *testing.T) { - rendezvousBoundaryTestcase := &TestCase{ - Config: NewConfig("run request with functions"). + rendezvousBoundaryTestcase := &hrp.TestCase{ + Config: hrp.NewConfig("run request with functions"). SetBaseURL("https://postman-echo.com"). WithVariables(map[string]interface{}{ "n": 5, "a": 12.3, "b": 3.45, }), - TestSteps: []IStep{ - NewStep("test negative number"). + TestSteps: []hrp.IStep{ + hrp.NewStep("test negative number"). SetRendezvous("test negative number"). WithUserNumber(-1), - NewStep("test overflow number"). + hrp.NewStep("test overflow number"). SetRendezvous("test overflow number"). WithUserNumber(1000000), - NewStep("test negative percent"). + hrp.NewStep("test negative percent"). SetRendezvous("test very low percent"). WithUserPercent(-0.5), - NewStep("test very low percent"). + hrp.NewStep("test very low percent"). SetRendezvous("test very low percent"). WithUserPercent(0.00001), - NewStep("test overflow percent"). + hrp.NewStep("test overflow percent"). SetRendezvous("test overflow percent"). WithUserPercent(1.5), - NewStep("test conflict params"). + hrp.NewStep("test conflict params"). SetRendezvous("test conflict params"). WithUserNumber(1). WithUserPercent(0.123), - NewStep("test negative timeout"). + hrp.NewStep("test negative timeout"). SetRendezvous("test negative timeout"). WithTimeout(-1000), }, @@ -55,7 +57,7 @@ func TestRunCaseWithRendezvous(t *testing.T) { {number: 100, percent: 1, timeout: 5000}, } - rendezvousList := InitRendezvous(rendezvousBoundaryTestcase, 100) + rendezvousList := hrp.InitRendezvous(rendezvousBoundaryTestcase, 100) for i, r := range rendezvousList { if r.Number != expectedRendezvousParams[i].number { diff --git a/step_request_test.go b/tests/step_request_test.go similarity index 64% rename from step_request_test.go rename to tests/step_request_test.go index 4648d0a8..daeba0ad 100644 --- a/step_request_test.go +++ b/tests/step_request_test.go @@ -1,17 +1,15 @@ -package hrp +package tests import ( - "io" - "net/http" - "strings" "testing" "time" + hrp "github.com/httprunner/httprunner/v5" "github.com/stretchr/testify/assert" ) var ( - stepGET = NewStep("get with params"). + stepGET = hrp.NewStep("get with params"). GET("/get"). WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}). WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). @@ -21,7 +19,7 @@ var ( AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type"). AssertEqual("body.args.foo1", "bar1", "check param foo1"). AssertEqual("body.args.foo2", "bar2", "check param foo2") - stepPOSTData = NewStep("post form data"). + stepPOSTData = hrp.NewStep("post form data"). POST("/post"). WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}). WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}). @@ -33,7 +31,7 @@ var ( func TestRunRequestGetToStruct(t *testing.T) { tStep := stepGET - if tStep.Request.Method != httpGET { + if tStep.Request.Method != hrp.HTTP_GET { t.Fatalf("tStep.Request.Method != GET") } if tStep.Request.URL != "/get" { @@ -48,7 +46,7 @@ func TestRunRequestGetToStruct(t *testing.T) { if tStep.Request.Cookies["user"] != "debugtalk" { t.Fatalf("tStep.Request.Cookies mismatch") } - validator, ok := tStep.Validators[0].(Validator) + validator, ok := tStep.Validators[0].(hrp.Validator) if !ok || validator.Check != "status_code" || validator.Expect != 200 { t.Fatalf("tStep.Validators mismatch") } @@ -56,7 +54,7 @@ func TestRunRequestGetToStruct(t *testing.T) { func TestRunRequestPostDataToStruct(t *testing.T) { tStep := stepPOSTData - if tStep.Request.Method != httpPOST { + if tStep.Request.Method != hrp.HTTP_POST { t.Fatalf("tStep.Request.Method != POST") } if tStep.Request.URL != "/post" { @@ -74,18 +72,18 @@ func TestRunRequestPostDataToStruct(t *testing.T) { if tStep.Request.Body != "a=1&b=2" { t.Fatalf("tStep.Request.Data mismatch") } - validator, ok := tStep.Validators[0].(Validator) + validator, ok := tStep.Validators[0].(hrp.Validator) if !ok || validator.Check != "status_code" || validator.Expect != 200 { t.Fatalf("tStep.Validators mismatch") } } func TestRunRequestStatOn(t *testing.T) { - testcase := TestCase{ - Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{stepGET, stepPOSTData}, + testcase := hrp.TestCase{ + Config: hrp.NewConfig("test").SetBaseURL("https://postman-echo.com"), + TestSteps: []hrp.IStep{stepGET, stepPOSTData}, } - caseRunner, _ := NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase) + caseRunner, _ := hrp.NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase) sessionRunner := caseRunner.NewSession() summary, err := sessionRunner.Start(nil) if err != nil { @@ -162,15 +160,15 @@ func TestRunRequestStatOn(t *testing.T) { } func TestRunCaseWithTimeout(t *testing.T) { - r := NewRunner(t) + r := hrp.NewRunner(t) // global timeout - testcase1 := &TestCase{ - Config: NewConfig("TestCase1"). + testcase1 := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase1"). SetRequestTimeout(10). // set global timeout to 10s SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("step1"). + TestSteps: []hrp.IStep{ + hrp.NewStep("step1"). GET("/delay/1"). Validate(). AssertEqual("status_code", 200, "check status code"), @@ -181,12 +179,12 @@ func TestRunCaseWithTimeout(t *testing.T) { t.FailNow() } - testcase2 := &TestCase{ - Config: NewConfig("TestCase2"). + testcase2 := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase2"). SetRequestTimeout(5). // set global timeout to 10s SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("step1"). + TestSteps: []hrp.IStep{ + hrp.NewStep("step1"). GET("/delay/10"). Validate(). AssertEqual("status_code", 200, "check status code"), @@ -198,12 +196,12 @@ func TestRunCaseWithTimeout(t *testing.T) { } // step timeout - testcase3 := &TestCase{ - Config: NewConfig("TestCase3"). + testcase3 := &hrp.TestCase{ + Config: hrp.NewConfig("TestCase3"). SetRequestTimeout(10). SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{ - NewStep("step2"). + TestSteps: []hrp.IStep{ + hrp.NewStep("step2"). GET("/delay/11"). SetTimeout(15*time.Second). // set step timeout to 4s Validate(). @@ -215,70 +213,3 @@ func TestRunCaseWithTimeout(t *testing.T) { t.FailNow() } } - -func TestSearchJmespath(t *testing.T) { - testText := `{"a": {"b": "foo"}, "c": "bar", "d": {"e": [{"f": "foo"}, {"f": "bar"}]}}` - testData := []struct { - raw string - expected string - }{ - {"body.a.b", "foo"}, - {"body.c", "bar"}, - {"body.d.e[0].f", "foo"}, - {"body.d.e[1].f", "bar"}, - } - resp := http.Response{} - resp.Body = io.NopCloser(strings.NewReader(testText)) - respObj, err := newHttpResponseObject(t, newParser(), &resp) - if err != nil { - t.Fatal() - } - for _, data := range testData { - if !assert.Equal(t, data.expected, respObj.searchJmespath(data.raw)) { - t.Fatal() - } - } -} - -func TestSearchRegexp(t *testing.T) { - testText := ` - -` - testData := []struct { - raw string - expected string - }{ - {"/user/signOut\">(.*)
  • ", "Sign Out"}, - {"
  • ", "Leo"}, - } - // new response object - resp := http.Response{} - resp.Body = io.NopCloser(strings.NewReader(testText)) - respObj, err := newHttpResponseObject(t, newParser(), &resp) - if err != nil { - t.Fatal() - } - for _, data := range testData { - if !assert.Equal(t, data.expected, respObj.searchRegexp(data.raw)) { - t.Fatal() - } - } -} diff --git a/step_ui_test.go b/tests/step_ui_test.go similarity index 55% rename from step_ui_test.go rename to tests/step_ui_test.go index d47e4fb6..2d8514bb 100644 --- a/step_ui_test.go +++ b/tests/step_ui_test.go @@ -1,87 +1,88 @@ //go:build localtest -package hrp +package tests import ( "testing" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + hrp "github.com/httprunner/httprunner/v5" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestIOSSettingsAction(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("ios ui action on Settings"). + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("ios ui action on Settings"). SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)), - TestSteps: []IStep{ - NewStep("launch Settings"). + TestSteps: []hrp.IStep{ + hrp.NewStep("launch Settings"). IOS().Home().TapByOCR("设置"). Validate(). AssertNameExists("飞行模式"). AssertLabelExists("蓝牙"). AssertOCRExists("个人热点"), - NewStep("swipe up and down"). + hrp.NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), }, } - err := NewRunner(t).Run(testCase) + err := hrp.NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) } } func TestIOSSearchApp(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("ios ui action on Search App 资源库"), - TestSteps: []IStep{ - NewStep("进入 App 资源库 搜索框"). + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("ios ui action on Search App 资源库"), + TestSteps: []hrp.IStep{ + hrp.NewStep("进入 App 资源库 搜索框"). IOS().Home().SwipeLeft().SwipeLeft().TapByCV("dewey-search-field"). Validate(). AssertLabelExists("取消"), - NewStep("搜索抖音"). + hrp.NewStep("搜索抖音"). IOS().Input("抖音\n"), }, } - err := NewRunner(t).Run(testCase) + err := hrp.NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) } } func TestIOSAppLaunch(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("启动 & 关闭 App"). + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("启动 & 关闭 App"). SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)), - TestSteps: []IStep{ - NewStep("终止今日头条"). + TestSteps: []hrp.IStep{ + hrp.NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), - NewStep("启动今日头条"). + hrp.NewStep("启动今日头条"). IOS().AppLaunch("com.ss.iphone.article.News"), - NewStep("终止今日头条"). + hrp.NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), - NewStep("启动今日头条"). + hrp.NewStep("启动今日头条"). IOS().AppLaunch("com.ss.iphone.article.News"), }, } - err := NewRunner(t).Run(testCase) + err := hrp.NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) } } func TestAndroidAction(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("android ui action"), - TestSteps: []IStep{ - NewStep("launch douyin"). + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("android ui action"), + TestSteps: []hrp.IStep{ + hrp.NewStep("launch douyin"). Android().Serial("xxx").TapByOCR("抖音"). Validate(). AssertNameExists("首页", "首页 tab 不存在"). AssertNameExists("消息", "消息 tab 不存在"), - NewStep("swipe up and down"). + hrp.NewStep("swipe up and down"). Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), }, } - err := NewRunner(t).Run(testCase) + err := hrp.NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) } diff --git a/summary_test.go b/tests/summary_test.go similarity index 65% rename from summary_test.go rename to tests/summary_test.go index e055daf9..951fbc33 100644 --- a/summary_test.go +++ b/tests/summary_test.go @@ -1,23 +1,25 @@ -package hrp +package tests import ( "testing" "github.com/stretchr/testify/assert" + + hrp "github.com/httprunner/httprunner/v5" ) func TestGenHTMLReport(t *testing.T) { - summary := NewSummary() + summary := hrp.NewSummary() - caseSummary1 := NewCaseSummary() - stepResult1 := &StepResult{} + caseSummary1 := hrp.NewCaseSummary() + stepResult1 := &hrp.StepResult{} caseSummary1.AddStepResult(stepResult1) summary.AddCaseSummary(caseSummary1) - caseSummary2 := NewCaseSummary() - stepResult2 := &StepResult{ + caseSummary2 := hrp.NewCaseSummary() + stepResult2 := &hrp.StepResult{ Name: "Test", - StepType: StepTypeRequest, + StepType: hrp.StepTypeRequest, Success: false, ContentSize: 0, Attachments: "err", @@ -37,22 +39,22 @@ func TestGenHTMLReport(t *testing.T) { } func TestTestCaseSummary_AddStepResult(t *testing.T) { - caseSummary := NewCaseSummary() - stepResult1 := &StepResult{ + caseSummary := hrp.NewCaseSummary() + stepResult1 := &hrp.StepResult{ Name: "Test1", - StepType: StepTypeRequest, + StepType: hrp.StepTypeRequest, Success: true, ContentSize: 0, Attachments: "err", } caseSummary.AddStepResult(stepResult1) - stepResult2 := &StepResult{ + stepResult2 := &hrp.StepResult{ Name: "Test2", - StepType: StepTypeTestCase, + StepType: hrp.StepTypeTestCase, Success: false, ContentSize: 0, Attachments: "err", - Data: []*StepResult{stepResult1}, + Data: []*hrp.StepResult{stepResult1}, } caseSummary.AddStepResult(stepResult2) diff --git a/testcase_test.go b/tests/testcase_test.go similarity index 79% rename from testcase_test.go rename to tests/testcase_test.go index b0c15dbb..fd9d8924 100644 --- a/testcase_test.go +++ b/tests/testcase_test.go @@ -1,10 +1,10 @@ -package hrp +package tests import ( "path/filepath" "testing" - "github.com/stretchr/testify/assert" + hrp "github.com/httprunner/httprunner/v5" ) const ( @@ -13,7 +13,7 @@ const ( // tmpl returns template file path func tmpl(relativePath string) string { - return filepath.Join("internal/scaffold/templates/", relativePath) + return filepath.Join("../internal/scaffold/templates/", relativePath) } var ( @@ -25,10 +25,10 @@ var ( demoAPIGETPath = tmpl("/api/get.yml") ) -var demoTestCaseWithThinkTimePath TestCasePath = hrpExamplesDir + "/think_time_test.json" +var demoTestCaseWithThinkTimePath hrp.TestCasePath = hrpExamplesDir + "/think_time_test.json" -var demoTestCaseWithPlugin = &TestCase{ - Config: NewConfig("demo with complex mechanisms"). +var demoTestCaseWithPlugin = &hrp.TestCase{ + Config: hrp.NewConfig("demo with complex mechanisms"). SetBaseURL("https://postman-echo.com"). WithVariables(map[string]interface{}{ // global level variables "n": "${sum_ints(1, 2, 2)}", @@ -37,9 +37,9 @@ var demoTestCaseWithPlugin = &TestCase{ "varFoo1": "${gen_random_string($n)}", "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function }), - TestSteps: []IStep{ - NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction - NewStep("get with params"). + TestSteps: []hrp.IStep{ + hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction + hrp.NewStep("get with params"). WithVariables(map[string]interface{}{ // step level variables "n": 3, // inherit config level variables if not set in step level, a/varFoo1 "b": 34.5, // override config level variable if existed, n/b/varFoo2 @@ -59,8 +59,8 @@ var demoTestCaseWithPlugin = &TestCase{ AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string - NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction - NewStep("post json data"). + hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction + hrp.NewStep("post json data"). POST("/post"). WithBody(map[string]interface{}{ "foo1": "$varFoo1", // reference former extracted variable @@ -70,7 +70,7 @@ var demoTestCaseWithPlugin = &TestCase{ AssertEqual("status_code", 200, "check status code"). AssertLengthEqual("body.json.foo1", 5, "check args foo1"). AssertEqual("body.json.foo2", 12.3, "check args foo2"), - NewStep("post form data"). + hrp.NewStep("post form data"). POST("/post"). WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). WithBody(map[string]interface{}{ @@ -84,15 +84,15 @@ var demoTestCaseWithPlugin = &TestCase{ AssertEqual("status_code", 200, "check status code"). AssertLengthEqual("body.form.foo1", 5, "check args foo1"). AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string - NewStep("get with timestamp"). + hrp.NewStep("get with timestamp"). GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). Validate(). AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), }, } -var demoTestCaseWithoutPlugin = &TestCase{ - Config: NewConfig("demo without custom function plugin"). +var demoTestCaseWithoutPlugin = &hrp.TestCase{ + Config: hrp.NewConfig("demo without custom function plugin"). SetBaseURL("https://postman-echo.com"). WithVariables(map[string]interface{}{ // global level variables "n": 5, @@ -101,9 +101,9 @@ var demoTestCaseWithoutPlugin = &TestCase{ "varFoo1": "${gen_random_string($n)}", "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function }), - TestSteps: []IStep{ - NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction - NewStep("get with params"). + TestSteps: []hrp.IStep{ + hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction + hrp.NewStep("get with params"). WithVariables(map[string]interface{}{ // step level variables "n": 3, // inherit config level variables if not set in step level, a/varFoo1 "b": 34.5, // override config level variable if existed, n/b/varFoo2 @@ -121,8 +121,8 @@ var demoTestCaseWithoutPlugin = &TestCase{ AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string - NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction - NewStep("post json data"). + hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction + hrp.NewStep("post json data"). POST("/post"). WithBody(map[string]interface{}{ "foo1": "$varFoo1", // reference former extracted variable @@ -132,7 +132,7 @@ var demoTestCaseWithoutPlugin = &TestCase{ AssertEqual("status_code", 200, "check status code"). AssertLengthEqual("body.json.foo1", 5, "check args foo1"). AssertEqual("body.json.foo2", 12.3, "check args foo2"), - NewStep("post form data"). + hrp.NewStep("post form data"). POST("/post"). WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). WithBody(map[string]interface{}{ @@ -146,7 +146,7 @@ var demoTestCaseWithoutPlugin = &TestCase{ AssertEqual("status_code", 200, "check status code"). AssertLengthEqual("body.form.foo1", 5, "check args foo1"). AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string - NewStep("get with timestamp"). + hrp.NewStep("get with timestamp"). GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). Validate(). AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), @@ -172,29 +172,3 @@ func TestGenDemoTestCase(t *testing.T) { t.Fatal() } } - -func TestConvertCheckExpr(t *testing.T) { - exprs := []struct { - before string - after string - }{ - // normal check expression - {"a.b.c", "a.b.c"}, - {"a.\"b-c\".d", "a.\"b-c\".d"}, - {"a.b-c.d", "a.b-c.d"}, - {"body.args.a[-1]", "body.args.a[-1]"}, - // check expression using regex - {"covering (.*) testing,", "covering (.*) testing,"}, - {" (.*) a-b-c", " (.*) a-b-c"}, - // abnormal check expression - {"headers.Content-Type", "headers.\"Content-Type\""}, - {"headers.\"Content-Type", "headers.\"Content-Type\""}, - {"headers.Content-Type\"", "headers.\"Content-Type\""}, - {"headers.User-Agent", "headers.\"User-Agent\""}, - } - for _, expr := range exprs { - if !assert.Equal(t, expr.after, convertJmespathExpr(expr.before)) { - t.Fatal() - } - } -} diff --git a/pkg/uixt/README.md b/uixt/README.md similarity index 100% rename from pkg/uixt/README.md rename to uixt/README.md diff --git a/pkg/uixt/ai/ai.go b/uixt/ai/ai.go similarity index 99% rename from pkg/uixt/ai/ai.go rename to uixt/ai/ai.go index 3b7ae92f..011cae8f 100644 --- a/pkg/uixt/ai/ai.go +++ b/uixt/ai/ai.go @@ -3,8 +3,9 @@ package ai import ( "os" - "github.com/httprunner/httprunner/v5/code" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/code" ) func NewAIService(opts ...AIServiceOption) *AIServices { diff --git a/pkg/uixt/ai/ai_test.go b/uixt/ai/ai_test.go similarity index 100% rename from pkg/uixt/ai/ai_test.go rename to uixt/ai/ai_test.go diff --git a/pkg/uixt/ai/cv.go b/uixt/ai/cv.go similarity index 98% rename from pkg/uixt/ai/cv.go rename to uixt/ai/cv.go index 5a5cd6b4..fa314855 100644 --- a/pkg/uixt/ai/cv.go +++ b/uixt/ai/cv.go @@ -7,11 +7,12 @@ import ( "math" "regexp" + "github.com/pkg/errors" + "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" - "github.com/pkg/errors" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) type ICVService interface { diff --git a/pkg/uixt/ai/cv_vedem.go b/uixt/ai/cv_vedem.go similarity index 99% rename from pkg/uixt/ai/cv_vedem.go rename to uixt/ai/cv_vedem.go index d9169a7f..1bad4ddc 100644 --- a/pkg/uixt/ai/cv_vedem.go +++ b/uixt/ai/cv_vedem.go @@ -16,7 +16,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) var client = &http.Client{ diff --git a/pkg/uixt/ai/cv_vedem_test.go b/uixt/ai/cv_vedem_test.go similarity index 100% rename from pkg/uixt/ai/cv_vedem_test.go rename to uixt/ai/cv_vedem_test.go diff --git a/pkg/uixt/ai/llm.go b/uixt/ai/llm.go similarity index 85% rename from pkg/uixt/ai/llm.go rename to uixt/ai/llm.go index 3b787400..745fe2c8 100644 --- a/pkg/uixt/ai/llm.go +++ b/uixt/ai/llm.go @@ -7,9 +7,6 @@ type ILLMService interface { } func NewGPT4oLLMService() (*openaiLLMService, error) { - if err := checkEnv(); err != nil { - return nil, err - } return &openaiLLMService{}, nil } diff --git a/pkg/uixt/android_device.go b/uixt/android_device.go similarity index 96% rename from pkg/uixt/android_device.go rename to uixt/android_device.go index ae1f3535..a53134a0 100644 --- a/pkg/uixt/android_device.go +++ b/uixt/android_device.go @@ -24,8 +24,8 @@ import ( "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/json" "github.com/httprunner/httprunner/v5/pkg/gadb" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) const ( @@ -100,7 +100,7 @@ type AndroidDevice struct { func (dev *AndroidDevice) Setup() error { dev.Device.RunShellCommand("ime", "enable", option.UnicodeImePackageName) - dev.Device.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath) + dev.Device.RunShellCommand("rm", "-r", config.GetConfig().DeviceActionLogFilePath) // setup evalite evalToolRaw, err := evalite.ReadFile("evalite") @@ -111,7 +111,6 @@ func (dev *AndroidDevice) Setup() error { if err != nil { return errors.Wrap(code.DeviceShellExecError, err.Error()) } - return nil } @@ -339,6 +338,15 @@ func (dev *AndroidDevice) GetPackageInfo(packageName string) (types.AppInfo, err return appInfo, nil } +func (dev *AndroidDevice) ScreenShot() (*bytes.Buffer, error) { + raw, err := dev.Device.ScreenCap() + if err != nil { + return nil, errors.Wrapf(code.DeviceScreenShotError, + "adb screencap failed %v", err) + } + return bytes.NewBuffer(raw), nil +} + func (dev *AndroidDevice) GetAppInfo(packageName string) (app types.AppInfo, err error) { packageInfo, err := dev.RunShellCommand( "CLASSPATH=/data/local/tmp/evalite", "app_process", "/", diff --git a/pkg/uixt/android_driver_adb.go b/uixt/android_driver_adb.go similarity index 94% rename from pkg/uixt/android_driver_adb.go rename to uixt/android_driver_adb.go index 7bb66216..1026ce7c 100644 --- a/pkg/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -24,8 +24,8 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/utf7" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func NewADBDriver(device *AndroidDevice) (*ADBDriver, error) { @@ -312,9 +312,10 @@ func (ad *ADBDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { return nil } -func (ad *ADBDriver) DoubleTapXY(x, y float64, opts ...option.ActionOption) error { +func (ad *ADBDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { var err error - if x, y, err = convertToAbsolutePoint(ad, x, y); err != nil { + x, y, err = convertToAbsolutePoint(ad, x, y) + if err != nil { return err } actionOptions := option.NewActionOptions(opts...) @@ -359,11 +360,9 @@ func (ad *ADBDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (er func (ad *ADBDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) (err error) { actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ad, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ad, fromX, fromY, toX, toY) + if err != nil { + return err } duration := 200.0 @@ -389,12 +388,9 @@ func (ad *ADBDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO func (ad *ADBDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { var err error - actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ad, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ad, fromX, fromY, toX, toY) + if err != nil { + return err } // adb shell input swipe fromX fromY toX toY @@ -542,24 +538,12 @@ func (ad *ADBDriver) SetRotation(rotation types.Rotation) (err error) { } func (ad *ADBDriver) ScreenShot(opts ...option.ActionOption) (raw *bytes.Buffer, err error) { - resp, err := ad.runShellCommand("screencap", "-p") + resp, err := ad.Device.ScreenCap() if err != nil { return nil, errors.Wrapf(code.DeviceScreenShotError, "adb screencap failed %v", err) } - raw = bytes.NewBuffer([]byte(resp)) - - actionOptions := option.NewActionOptions(opts...) - if actionOptions.ScreenShotFileName != "" { - // save screenshot to file - path, err := saveScreenShot(raw, actionOptions.ScreenShotFileName) - if err != nil { - return nil, errors.Wrapf(code.DeviceScreenShotError, - "save screenshot file failed %v", err) - } - log.Info().Str("path", path).Msg("screenshot saved") - } - + raw = bytes.NewBuffer(resp) return raw, nil } @@ -674,10 +658,10 @@ func (ad *ADBDriver) StopCaptureLog() (result interface{}, err error) { // 没有解析到打点日志,走兜底逻辑 if len(pointRes) == 0 { log.Info().Msg("action log is null, use action file >>>") - logFilePathPrefix := fmt.Sprintf("%v/data", config.ActionLogFilePath) + logFilePathPrefix := fmt.Sprintf("%v/data", config.GetConfig().ActionLogFilePath) files := []string{} - ad.Device.RunShellCommand("pull", config.DeviceActionLogFilePath, config.ActionLogFilePath) - err = filepath.Walk(config.ActionLogFilePath, func(path string, info fs.FileInfo, err error) error { + ad.Device.RunShellCommand("pull", config.GetConfig().DeviceActionLogFilePath, config.GetConfig().ActionLogFilePath) + err = filepath.Walk(config.GetConfig().ActionLogFilePath, func(path string, info fs.FileInfo, err error) error { // 只是需要日志文件 if ok := strings.Contains(path, logFilePathPrefix); ok { files = append(files, path) @@ -797,7 +781,7 @@ func (ad *ADBDriver) GetIme() (ime string, err error) { func (ad *ADBDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) - fileName := filepath.Join(config.ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) + fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) file, err := os.Create(fileName) if err != nil { diff --git a/pkg/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go similarity index 96% rename from pkg/uixt/android_driver_uia2.go rename to uixt/android_driver_uia2.go index e58e12a0..938afc13 100644 --- a/pkg/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -12,8 +12,8 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/utf7" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func NewUIA2Driver(device *AndroidDevice) (*UIA2Driver, error) { @@ -252,9 +252,10 @@ func (ud *UIA2Driver) Orientation() (orientation types.Orientation, err error) { return } -func (ud *UIA2Driver) DoubleTapXY(x, y float64, opts ...option.ActionOption) error { +func (ud *UIA2Driver) DoubleTap(x, y float64, opts ...option.ActionOption) error { var err error - if x, y, err = convertToAbsolutePoint(ud, x, y); err != nil { + x, y, err = convertToAbsolutePoint(ud, x, y) + if err != nil { return err } actionOptions := option.NewActionOptions(opts...) @@ -348,12 +349,9 @@ func (ud *UIA2Driver) TouchAndHold(x, y float64, opts ...option.ActionOption) (e // steps, the swipe will take around 0.5 seconds to complete. func (ud *UIA2Driver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { var err error - actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY) + if err != nil { + return err } data := map[string]interface{}{ "startX": fromX, @@ -378,11 +376,9 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) var err error actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY) + if err != nil { + return err } duration := 200.0 diff --git a/pkg/uixt/android_key.go b/uixt/android_key.go similarity index 100% rename from pkg/uixt/android_key.go rename to uixt/android_key.go diff --git a/pkg/uixt/android_layout.go b/uixt/android_layout.go similarity index 100% rename from pkg/uixt/android_layout.go rename to uixt/android_layout.go diff --git a/pkg/uixt/android_test.go b/uixt/android_test.go similarity index 97% rename from pkg/uixt/android_test.go rename to uixt/android_test.go index 9faa7320..f492740c 100644 --- a/pkg/uixt/android_test.go +++ b/uixt/android_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func setupADBDriverExt(t *testing.T) *XTDriver { diff --git a/uixt/browser_device.go b/uixt/browser_device.go new file mode 100644 index 00000000..b3aeebb4 --- /dev/null +++ b/uixt/browser_device.go @@ -0,0 +1,75 @@ +package uixt + +import ( + "bytes" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" +) + +type BrowserDevice struct { + Options *option.BrowserDeviceOptions +} + +func NewBrowserDevice(opts ...option.BrowserDeviceOption) (device *BrowserDevice, err error) { + options := option.NewBrowserDeviceOptions(opts...) + if options.BrowserID == "" { + browserInfo, err := CreateBrowser(3600) + if err != nil { + log.Error().Err(err).Msg("failed to create browser") + return nil, err + } + options.BrowserID = browserInfo.ContextId + } + + device = &BrowserDevice{ + Options: options, + } + log.Info().Str("browserID", device.Options.BrowserID).Msg("init browser device") + + return device, nil +} + +func (dev *BrowserDevice) UUID() string { + return dev.Options.BrowserID +} + +func (dev *BrowserDevice) Setup() error { + return nil +} + +func (dev *BrowserDevice) LogEnabled() bool { + return dev.Options.LogOn +} + +func (dev *BrowserDevice) Teardown() error { + return nil +} + +func (dev *BrowserDevice) Install(appPath string, opts ...option.InstallOption) error { + return errors.New("not support") +} + +func (dev *BrowserDevice) Uninstall(packageName string) error { + return errors.New("not support") +} + +func (dev *BrowserDevice) GetPackageInfo(packageName string) (types.AppInfo, error) { + return types.AppInfo{}, errors.New("not support") +} + +func (dev *BrowserDevice) NewDriver() (driver IDriver, err error) { + // var driver WebDriver + driver, err = NewBrowserDriver(dev) + if err != nil { + return nil, err + } + return driver, nil +} + +func (dev *BrowserDevice) ScreenShot() (*bytes.Buffer, error) { + return nil, errors.New("not support") +} diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go new file mode 100644 index 00000000..95d08123 --- /dev/null +++ b/uixt/browser_driver.go @@ -0,0 +1,639 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" +) + +const BROWSER_LOCAL_ADDRESS = "localhost:8093" + +type WebAgentResponse struct { + Code int `json:"code"` + Message string `json:"msg"` + Data interface{} `json:"data"` + Result interface{} `json:"result"` +} + +type CreateBrowserResponse struct { + Code int `json:"code"` + Message string `json:"msg"` + Data BrowserInfo `json:"data"` +} + +type BrowserDriver struct { + urlPrefix *url.URL + sessionId string +} + +type BrowserInfo struct { + ContextId string `json:"context_id"` +} + +func CreateBrowser(timeout int) (browserInfo *BrowserInfo, err error) { + data := map[string]interface{}{ + "timeout": timeout, + } + + var bsJSON []byte = nil + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + + rawURL := "http://" + BROWSER_LOCAL_ADDRESS + "/api/v1/create_browser" + req, err := http.NewRequest(http.MethodPost, rawURL, bytes.NewBuffer(bsJSON)) + req.Header.Set("Content-Type", "application/json") + + if err != nil { + return nil, err + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + rawResp, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + + var result CreateBrowserResponse + if err = json.Unmarshal(rawResp, &result); err != nil { + return nil, err + } + + if result.Code != 0 { + return nil, errors.New(result.Message) + } + + return &result.Data, nil +} + +func NewBrowserDriver(device *BrowserDevice) (driver *BrowserDriver, err error) { + log.Info().Msg("init NewBrowserDriver driver") + driver = new(BrowserDriver) + driver.urlPrefix = &url.URL{} + driver.urlPrefix.Host = BROWSER_LOCAL_ADDRESS + driver.urlPrefix.Scheme = "http" + driver.sessionId = device.UUID() + return driver, nil +} + +func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option.ActionOption) (err error) { + data := map[string]interface{}{ + "from_x": fromX, + "from_y": fromY, + "to_x": toX, + "to_y": toY, + } + + actionOptions := option.NewActionOptions(options...) + + if actionOptions.Duration > 0 { + data["duration"] = actionOptions.Duration + } + + _, err = wd.HttpPOST(data, wd.sessionId, "ui/drag") + return +} + +func (wd *BrowserDriver) AppLaunch(packageName string) (err error) { + data := map[string]interface{}{ + "url": packageName, + } + + _, err = wd.HttpPOST(data, wd.sessionId, "ui/page_launch") + return +} + +func (wd *BrowserDriver) DeleteSession() (err error) { + url := wd.concatURL("context", wd.sessionId) + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + panic(err) + } + client := &http.Client{ + Timeout: 60 * time.Second, // 设置超时时间为5秒 + } + resp, err := client.Do(req) + if err != nil { + return err + } + + rawResp, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + + var result CreateBrowserResponse + if err = json.Unmarshal(rawResp, &result); err != nil { + return err + } + + if result.Code != 0 { + return errors.New(result.Message) + } + return nil +} + +func (wd *BrowserDriver) Scroll(delta int) (err error) { + data := map[string]interface{}{ + "delta": delta, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/scroll") + return err +} + +func (wd *BrowserDriver) CreateNetListener() (*websocket.Conn, error) { + webSocketUrl := "ws://localhost:8093/websocket_net_listen" + c, _, err := websocket.DefaultDialer.Dial(webSocketUrl, nil) + if err != nil { + return nil, err + } + // 发送消息 + initMessage := fmt.Sprintf(`{ + "type":"create_net_listener", + "context_id":"%v" + }`, wd.sessionId) + err = c.WriteMessage(websocket.TextMessage, []byte(initMessage)) + return c, err +} + +func (wd *BrowserDriver) ClosePage(pageIndex int) (err error) { + data := map[string]interface{}{ + "page_index": pageIndex, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/page_close") + return err +} + +func (wd *BrowserDriver) HoverBySelector(selector string, options ...option.ActionOption) (err error) { + data := map[string]interface{}{ + "selector": selector, + } + actionOptions := option.NewActionOptions(options...) + if actionOptions.Index > 0 { + data["element_index"] = actionOptions.Index + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/hover") + return err +} + +func (wd *BrowserDriver) tapBySelector(selector string, options ...option.ActionOption) (err error) { + data := map[string]interface{}{ + "selector": selector, + } + actionOptions := option.NewActionOptions(options...) + if actionOptions.Index > 0 { + data["element_index"] = actionOptions.Index + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/tap") + return err +} + +func (wd *BrowserDriver) RightClick(x, y float64) (err error) { + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/right_click") + return err +} + +func (wd *BrowserDriver) RightclickbySelector(selector string, options ...option.ActionOption) (err error) { + data := map[string]interface{}{ + "selector": selector, + } + actionOptions := option.NewActionOptions(options...) + if actionOptions.Index > 0 { + data["element_index"] = actionOptions.Index + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/right_click") + return err +} + +func (wd *BrowserDriver) GetElementTextBySelector(selector string, options ...option.ActionOption) (text string, err error) { + actionOptions := option.NewActionOptions(options...) + uri := "ui/element_text?selector=" + selector + if actionOptions.Index > 0 { + uri = uri + "&element_index=" + fmt.Sprintf("%v", actionOptions.Index) + } + resp, err := wd.HttpGet(http.MethodGet, wd.sessionId, uri) + if err != nil { + return "", err + } + data := resp.Data.(map[string]interface{}) + return data["text"].(string), nil +} + +func (wd *BrowserDriver) GetPageUrl(options ...option.ActionOption) (text string, err error) { + uri := "ui/page_url" + actionOptions := option.NewActionOptions(options...) + if actionOptions.Index > 0 { + uri = uri + "?page_index=" + fmt.Sprintf("%v", actionOptions.Index) + } + resp, err := wd.HttpGet(http.MethodGet, wd.sessionId, uri) + if err != nil { + return "", err + } + data := resp.Data.(map[string]interface{}) + return data["url"].(string), nil +} + +func (wd *BrowserDriver) IsElementExistBySelector(selector string) (bool, error) { + resp, err := wd.HttpGet(wd.sessionId, "ui/element_exist", "?selector=", selector) + if err != nil { + return false, err + } + data := resp.Data.(map[string]interface{}) + return data["exist"].(bool), nil +} + +func (wd *BrowserDriver) Hover(x, y float64) (err error) { + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/hover") + return err +} + +func (wd *BrowserDriver) Input(text string, option ...option.ActionOption) (err error) { + data := map[string]interface{}{ + "text": text, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/input") + return err +} + +// Source Return application elements tree +func (wd *BrowserDriver) Source(srcOpt ...option.SourceOption) (string, error) { + resp, err := wd.HttpGet(http.MethodGet, wd.sessionId, "stub/source") + if err != nil { + return "", err + } + + jsonData, err := json.Marshal(resp.Data) + if err != nil { + return "", err + } + + return string(jsonData), err +} + +func (wd *BrowserDriver) ScreenShot(options ...option.ActionOption) (*bytes.Buffer, error) { + resp, err := wd.HttpGet(http.MethodGet, wd.sessionId, "screenshot") + if err != nil { + return nil, err + } + data := resp.Data.(map[string]interface{}) + screenshotBase64 := data["screenshot"].(string) + screenRaw, err := base64.StdEncoding.DecodeString(screenshotBase64) + if err != nil { + return nil, err + } + return bytes.NewBuffer(screenRaw), nil +} + +func (wd *BrowserDriver) HttpPOST(data interface{}, pathElem ...string) (response *WebAgentResponse, err error) { + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + + return wd.httpRequest(http.MethodPost, wd.concatURL(pathElem...), bsJSON) +} + +func (wd *BrowserDriver) HttpGet(data interface{}, pathElem ...string) (response *WebAgentResponse, err error) { + return wd.httpRequest(http.MethodGet, wd.concatURL(pathElem...), nil) +} + +func (wd *BrowserDriver) concatURL(elem ...string) string { + tmp, _ := url.Parse(wd.urlPrefix.String()) + commonPath := path.Join(append([]string{wd.urlPrefix.Path}, "api/v1/")...) + tmp.Path = path.Join(append([]string{commonPath}, elem...)...) + return tmp.String() +} + +func (wd *BrowserDriver) httpRequest(method string, rawURL string, rawBody []byte, disableRetry ...bool) (response *WebAgentResponse, err error) { + req, err := http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)) + req.Header.Set("Content-Type", "application/json") + + if err != nil { + return nil, err + } + + // 新建http client + client := &http.Client{ + Timeout: 60 * time.Second, // 设置超时时间为5秒 + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + rawResp, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + + // 将结果解析为 JSON + var result WebAgentResponse + if err = json.Unmarshal(rawResp, &result); err != nil { + return nil, err + } + + if result.Code != 0 { + log.Info().Msgf("%v", result.Message) + return nil, errors.New(result.Message) + } + + if err != nil { + return nil, err + } + return &result, err +} + +func (wd *BrowserDriver) Status() (deviceStatus types.DeviceStatus, err error) { + log.Warn().Msg("Status not implemented in ADBDriver") + return +} + +func (wd *BrowserDriver) DeviceInfo() (deviceInfo types.DeviceInfo, err error) { + log.Warn().Msg("DeviceInfo not implemented in ADBDriver") + return +} + +func (wd *BrowserDriver) BatteryInfo() (batteryInfo types.BatteryInfo, err error) { + log.Warn().Msg("BatteryInfo not implemented in ADBDriver") + return +} + +func (wd *BrowserDriver) WindowSize() (types.Size, error) { + resp, err := wd.HttpGet(http.MethodGet, wd.sessionId, "window_size") + if err != nil { + return types.Size{}, err + } + data := resp.Data.(map[string]interface{}) + width := data["width"] + height := data["height"] + return types.Size{ + Width: int(width.(float64)), + Height: int(height.(float64)), + }, nil +} + +func (wd *BrowserDriver) Screen() (Screen, error) { + return Screen{}, errors.New("not support") +} + +func (wd *BrowserDriver) Scale() (float64, error) { + return 0, errors.New("not support") +} + +// GetTimestamp returns the timestamp of the mobile device +func (wd *BrowserDriver) GetTimestamp() (timestamp int64, err error) { + return 0, errors.New("not support") +} + +// Homescreen Forces the device under test to switch to the home screen +func (wd *BrowserDriver) Homescreen() error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Unlock() (err error) { + return errors.New("not support") +} + +// AppTerminate Terminate an application with the given package name. +// Either `true` if the app has been successfully terminated or `false` if it was not running +func (wd *BrowserDriver) AppTerminate(packageName string) (bool, error) { + return false, errors.New("not support") +} + +// AssertForegroundApp returns nil if the given package and activity are in foreground +func (wd *BrowserDriver) AssertForegroundApp(packageName string, activityType ...string) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Back() error { + return errors.New("not support") +} + +func (wd *BrowserDriver) AppClear(packageName string) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) ClearImages() error { + return errors.New("not support") +} + +func (wd *BrowserDriver) PushImage(localPath string) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Orientation() (orientation types.Orientation, err error) { + log.Warn().Msg("Orientation not implemented in ADBDriver") + return +} + +// Tap Sends a tap event at the coordinate. +func (wd *BrowserDriver) Tap(x, y int, options ...option.ActionOption) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) TapFloat(x, y float64, options ...option.ActionOption) error { + actionOptions := option.NewActionOptions(options...) + duration := 0.1 + if actionOptions.Duration > 0 { + duration = actionOptions.Duration + } + data := map[string]interface{}{ + "x": x, + "y": y, + "duration": duration, + } + _, err := wd.HttpPOST(data, wd.sessionId, "ui/tap") + return err +} + +// DoubleTap Sends a double tap event at the coordinate. +func (wd *BrowserDriver) DoubleTap(x, y float64, options ...option.ActionOption) error { + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err := wd.HttpPOST(data, wd.sessionId, "ui/double_tap") + return err +} + +func (wd *BrowserDriver) UploadFile(x, y float64, FileUrl, FileFormat string) (err error) { + data := map[string]interface{}{ + "x": x, + "y": y, + "file_url": FileUrl, + "file_format": FileFormat, + } + _, err = wd.HttpPOST(data, wd.sessionId, "ui/upload") + return err +} + +// TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration. +// +// second: The default value is 1 +func (wd *BrowserDriver) TouchAndHold(x, y float64, options ...option.ActionOption) error { + return errors.New("not support") +} + +// Swipe works like Drag, but `pressForDuration` value is 0 +func (wd *BrowserDriver) Swipe(fromX, fromY, toX, toY float64, options ...option.ActionOption) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...option.ActionOption) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) SetIme(ime string) error { + return errors.New("not support") +} + +// SendKeys Types a string into active element. There must be element with keyboard focus, +// otherwise an error is raised. +// WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60 +func (wd *BrowserDriver) SendKeys(text string, options ...option.ActionOption) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Clear(packageName string) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Setup() error { + return nil +} + +func (wd *BrowserDriver) GetDevice() IDevice { + return nil +} + +func (wd *BrowserDriver) ForegroundInfo() (app types.AppInfo, err error) { + return +} + +// PressBack Presses the back button +func (wd *BrowserDriver) PressBack(options ...option.ActionOption) error { + _, err := wd.HttpPOST(map[string]interface{}{}, wd.sessionId, "ui/back") + return err +} + +func (wd *BrowserDriver) PressKeyCode(keyCode KeyCode) (err error) { + return errors.New("not support") +} + +func (wd *BrowserDriver) Backspace(count int, options ...option.ActionOption) (err error) { + return errors.New("not support") +} + +func (wd *BrowserDriver) LogoutNoneUI(packageName string) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) TapByText(text string, options ...option.ActionOption) error { + return errors.New("not support") +} + +// AccessibleSource Return application elements accessibility tree +func (wd *BrowserDriver) AccessibleSource() (string, error) { + return "", errors.New("not support") +} + +// HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions +// +// Checks health of XCTest by: +// 1) Querying application for some elements, +// 2) Triggering some device events. +func (wd *BrowserDriver) HealthCheck() error { + return errors.New("not support") +} + +func (wd *BrowserDriver) GetAppiumSettings() (map[string]interface{}, error) { + return nil, errors.New("not support") +} + +func (wd *BrowserDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) { + return nil, errors.New("not support") +} + +func (wd *BrowserDriver) IsHealthy() (bool, error) { + return false, errors.New("not support") +} + +// triggers the log capture and returns the log entries +func (wd *BrowserDriver) StartCaptureLog(identifier ...string) (err error) { + return errors.New("not support") +} + +func (wd *BrowserDriver) StopCaptureLog() (result interface{}, err error) { + return nil, errors.New("not support") +} + +func (wd *BrowserDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) { + return "", errors.New("not support") +} + +func (wd *BrowserDriver) TearDown() error { + return nil +} + +func (wd *BrowserDriver) InitSession(capabilities option.Capabilities) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) GetSession() *DriverSession { + return nil +} + +func (wd *BrowserDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { + return +} + +func (wd *BrowserDriver) Rotation() (rotation types.Rotation, err error) { + return +} + +func (wd *BrowserDriver) SetRotation(rotation types.Rotation) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) Home() error { + return errors.New("not support") +} + +func (wd *BrowserDriver) TapXY(x, y float64, opts ...option.ActionOption) error { + return errors.New("not support") +} + +func (wd *BrowserDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { + return wd.TapFloat(x, y, opts...) +} diff --git a/pkg/uixt/demo/main_test.go b/uixt/demo/main_test.go similarity index 87% rename from pkg/uixt/demo/main_test.go rename to uixt/demo/main_test.go index 993bad79..a74909de 100644 --- a/pkg/uixt/demo/main_test.go +++ b/uixt/demo/main_test.go @@ -8,9 +8,9 @@ import ( "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) func TestIOSDemo(t *testing.T) { diff --git a/pkg/uixt/device.go b/uixt/device.go similarity index 73% rename from pkg/uixt/device.go rename to uixt/device.go index 652e5453..7e44857a 100644 --- a/pkg/uixt/device.go +++ b/uixt/device.go @@ -1,8 +1,10 @@ package uixt import ( - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "bytes" + + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) // current implemeted device: IOSDevice, AndroidDevice, HarmonyDevice @@ -17,7 +19,7 @@ type IDevice interface { Uninstall(packageName string) error GetPackageInfo(packageName string) (types.AppInfo, error) - + ScreenShot() (*bytes.Buffer, error) // TODO: remove? LogEnabled() bool } diff --git a/pkg/uixt/driver.go b/uixt/driver.go similarity index 83% rename from pkg/uixt/driver.go rename to uixt/driver.go index 41139d86..a27a9a48 100644 --- a/pkg/uixt/driver.go +++ b/uixt/driver.go @@ -6,9 +6,9 @@ import ( _ "image/png" "time" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) var ( @@ -16,6 +16,7 @@ var ( _ IDriver = (*UIA2Driver)(nil) _ IDriver = (*WDADriver)(nil) _ IDriver = (*HDCDriver)(nil) + _ IDriver = (*BrowserDriver)(nil) ) // current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver @@ -50,9 +51,9 @@ type IDriver interface { Unlock() error Back() error // tap - TapXY(x, y float64, opts ...option.ActionOption) error // by percentage - TapAbsXY(x, y float64, opts ...option.ActionOption) error // by absolute coordinate - DoubleTapXY(x, y float64, opts ...option.ActionOption) error // by percentage + TapXY(x, y float64, opts ...option.ActionOption) error // by percentage or absolute coordinate + TapAbsXY(x, y float64, opts ...option.ActionOption) error // by absolute coordinate + DoubleTap(x, y float64, opts ...option.ActionOption) error // by absolute coordinate TouchAndHold(x, y float64, opts ...option.ActionOption) error // swipe Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error diff --git a/pkg/uixt/driver_action.go b/uixt/driver_action.go similarity index 99% rename from pkg/uixt/driver_action.go rename to uixt/driver_action.go index 67d4fa84..e20e261c 100644 --- a/pkg/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -11,7 +11,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) type ActionMethod string @@ -220,7 +220,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { return fmt.Errorf("invalid tap location params: %v", params) } x, y := params[0], params[1] - return dExt.DoubleTapXY(x, y) + return dExt.DoubleTap(x, y) } return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params) case ACTION_Swipe: diff --git a/pkg/uixt/driver_ext/android_stub_driver.go b/uixt/driver_ext/android_stub_driver.go similarity index 94% rename from pkg/uixt/driver_ext/android_stub_driver.go rename to uixt/driver_ext/android_stub_driver.go index 5d13964d..bf838139 100644 --- a/pkg/uixt/driver_ext/android_stub_driver.go +++ b/uixt/driver_ext/android_stub_driver.go @@ -9,9 +9,9 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) type StubAndroidDriver struct { @@ -46,16 +46,17 @@ func NewStubAndroidDriver(dev *uixt.AndroidDevice) (*StubAndroidDriver, error) { } // setup driver - if err := driver.Setup(); err != nil { + if err = driver.Setup(); err != nil { return nil, err } - // register driver session reset handler - driver.Session.RegisterResetHandler(driver.Setup) - return driver, nil } +func (sad *StubAndroidDriver) GetDriver() uixt.IDriver { + return sad.ADBDriver +} + func (sad *StubAndroidDriver) Setup() error { socketLocalPort, err := sad.Device.Forward(StubSocketName) if err != nil { @@ -63,10 +64,6 @@ func (sad *StubAndroidDriver) Setup() error { fmt.Sprintf("forward port %d->%s failed: %v", socketLocalPort, StubSocketName, err)) } - err = sad.Session.SetupPortForward(socketLocalPort) - if err != nil { - return err - } douyinLocalPort, err := sad.Device.Forward(AndroidDouyinPort) if err != nil { @@ -173,7 +170,7 @@ func (sad *StubAndroidDriver) Source(srcOpt ...option.SourceOption) (source stri return res.(string), nil } -func (sad *StubAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) ( +func (sad *StubAndroidDriver) LoginNoneUI(packageName, phoneNumber, captcha, password string) ( info AppLoginInfo, err error) { app, err := sad.ForegroundInfo() if err != nil { @@ -189,7 +186,7 @@ func (sad *StubAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captc } } -func (sad *StubAndroidDriver) LoginXigua(packageName, phoneNumber string, captcha, password string) ( +func (sad *StubAndroidDriver) LoginXigua(packageName, phoneNumber, captcha, password string) ( info AppLoginInfo, err error) { loginSchema := "" if captcha != "" { @@ -205,7 +202,7 @@ func (sad *StubAndroidDriver) LoginXigua(packageName, phoneNumber string, captch return info, sad.OpenUrl(loginSchema) } -func (sad *StubAndroidDriver) LoginDouyin(packageName, phoneNumber string, captcha, password string) ( +func (sad *StubAndroidDriver) LoginDouyin(packageName, phoneNumber, captcha, password string) ( info AppLoginInfo, err error) { params := map[string]interface{}{ diff --git a/uixt/driver_ext/android_stub_driver_test.go b/uixt/driver_ext/android_stub_driver_test.go new file mode 100644 index 00000000..4f88a8e3 --- /dev/null +++ b/uixt/driver_ext/android_stub_driver_test.go @@ -0,0 +1,27 @@ +package driver_ext + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/httprunner/httprunner/v5/uixt" +) + +func setupAndroidStubDriver(t *testing.T) *StubAndroidDriver { + device, err := uixt.NewAndroidDevice() + require.Nil(t, err) + device.Options.UIA2 = false + device.Options.LogOn = false + driver, err := NewStubAndroidDriver(device) + require.Nil(t, err) + return driver +} + +func TestAndroidStubDriver_LoginNoneUI(t *testing.T) { + androidStubDriver := setupAndroidStubDriver(t) + info, err := androidStubDriver.LoginNoneUI("com.ss.android.ugc.aweme", "12343418541", "", "im112233") + assert.Nil(t, err) + t.Logf("login info: %+v", info) +} diff --git a/uixt/driver_ext/browser_sub_driver.go b/uixt/driver_ext/browser_sub_driver.go new file mode 100644 index 00000000..c57c7e25 --- /dev/null +++ b/uixt/driver_ext/browser_sub_driver.go @@ -0,0 +1,71 @@ +package driver_ext + +import ( + "encoding/json" + "net/http" + + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +type StubBrowserDriver struct { + *uixt.BrowserDriver + + sessionId string +} + +func NewStubBrowserDriver(device *uixt.BrowserDevice) (driver *StubBrowserDriver, err error) { + browserDriver, err := uixt.NewBrowserDriver(device) + if err != nil { + return nil, errors.Wrap(err, "create browser session failed") + } + driver = &StubBrowserDriver{ + BrowserDriver: browserDriver, + } + driver.sessionId = device.UUID() + return driver, nil +} + +func (wd *StubBrowserDriver) GetDriver() uixt.IDriver { + return wd.BrowserDriver +} + +// Source Return application elements tree +func (wd *StubBrowserDriver) Source(srcOpt ...option.SourceOption) (string, error) { + resp, err := wd.BrowserDriver.HttpGet(http.MethodGet, wd.sessionId, "stub/source") + if err != nil { + return "", err + } + + jsonData, err := json.Marshal(resp.Data) + if err != nil { + return "", err + } + + return string(jsonData), err +} + +func (wd *StubBrowserDriver) LoginNoneUI(packageName, phoneNumber, captcha, password string) ( + info AppLoginInfo, err error) { + data := map[string]interface{}{ + "url": packageName, + "web_cookie": password, + } + resp, err := wd.HttpPOST(data, wd.sessionId, "stub/login") + if err != nil { + return info, err + } + respdata := resp.Data.(map[string]interface{}) + loginSuccss := AppLoginInfo{ + IsLogin: true, + Uid: respdata["webid"].(string), + Did: password, + } + return loginSuccss, err +} + +func (wd *StubBrowserDriver) LogoutNoneUI(packageName string) error { + return errors.New("not implemented") +} diff --git a/uixt/driver_ext/ext.go b/uixt/driver_ext/ext.go new file mode 100644 index 00000000..45d525eb --- /dev/null +++ b/uixt/driver_ext/ext.go @@ -0,0 +1,81 @@ +package driver_ext + +import ( + "time" + + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +var ( + _ IStubDriver = (*StubAndroidDriver)(nil) + _ IStubDriver = (*StubIOSDriver)(nil) + _ IStubDriver = (*StubBrowserDriver)(nil) +) + +type IStubDriver interface { + GetDriver() uixt.IDriver + LoginNoneUI(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error) + LogoutNoneUI(packageName string) error +} + +func NewStubXTDriver(stubDriver IStubDriver, opts ...ai.AIServiceOption) *StubXTDriver { + services := ai.NewAIService(opts...) + driverExt := &StubXTDriver{ + XTDriver: &uixt.XTDriver{ + IDriver: stubDriver.GetDriver(), + CVService: services.ICVService, + LLMService: services.ILLMService, + }, + IStubDriver: stubDriver, + } + return driverExt +} + +type StubXTDriver struct { + *uixt.XTDriver + IStubDriver +} + +func (dExt *StubXTDriver) InstallByUrl(url string, opts ...option.InstallOption) error { + appPath, err := uixt.DownloadFileByUrl(url) + if err != nil { + return err + } + err = dExt.Install(appPath, opts...) + if err != nil { + return err + } + return nil +} + +func (dExt *StubXTDriver) Install(filePath string, opts ...option.InstallOption) error { + if _, ok := dExt.GetDevice().(*uixt.AndroidDevice); ok { + stopChan := make(chan struct{}) + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + go func() { + _ = dExt.TapByOCR("^(.*无视风险安装|正在扫描.*|我知道了|稍后继续|稍后提醒|继续安装|知道了|确定|继续|完成|点击继续安装|继续安装旧版本|替换|.*正在安装|安装|授权本次安装|重新安装|仍要安装|更多详情|我知道了|已了解此应用未经检测.)$", option.WithRegex(true), option.WithIgnoreNotFoundError(true)) + //_ = dExt.IDriver.TapByHierarchy("^(.*无视风险安装|正在扫描.*|我知道了|稍后继续|稍后提醒|继续安装|知道了|确定|继续|完成|点击继续安装|继续安装旧版本|替换|.*正在安装|安装|授权本次安装|重新安装|仍要安装|更多详情|我知道了|已了解此应用未经检测.)$", option.WithRegex(true), option.WithIgnoreNotFoundError(true)) + }() + case <-stopChan: + log.Info().Msg("install complete") + return + } + } + }() + defer func() { + close(stopChan) + }() + } + + return dExt.GetDevice().Install(filePath, opts...) +} diff --git a/uixt/driver_ext/ios_stub_driver.go b/uixt/driver_ext/ios_stub_driver.go new file mode 100644 index 00000000..948ff9be --- /dev/null +++ b/uixt/driver_ext/ios_stub_driver.go @@ -0,0 +1,518 @@ +package driver_ext + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +type StubIOSDriver struct { + Device *uixt.IOSDevice + Session *uixt.DriverSession + WDADriver *uixt.WDADriver + timeout time.Duration + douyinUrlPrefix string + douyinLiteUrlPrefix string +} + +const ( + IOSDouyinPort = 32921 + IOSDouyinLitePort = 33461 + defaultBightInsightPort = 8000 +) + +func NewStubIOSDriver(dev *uixt.IOSDevice) (*StubIOSDriver, error) { + driver := &StubIOSDriver{ + Device: dev, + timeout: 10 * time.Second, + Session: uixt.NewDriverSession(), + } + + // setup driver + if err := driver.Setup(); err != nil { + return nil, err + } + + return driver, nil +} + +func (s *StubIOSDriver) SetupWda() (err error) { + if s.WDADriver != nil { + return nil + } + s.WDADriver, err = uixt.NewWDADriver(s.Device) + return err +} + +func (s *StubIOSDriver) GetDriver() uixt.IDriver { + return s.WDADriver +} + +func (s *StubIOSDriver) Setup() error { + localPort, err := s.getLocalPort() + if err != nil { + return err + } + s.Session.SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", localPort)) + + localDouyinPort, err := builtin.GetFreePort() + if err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = s.Device.Forward(localDouyinPort, IOSDouyinPort); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + s.douyinUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinPort) + + localDouyinLitePort, err := builtin.GetFreePort() + if err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = s.Device.Forward(localDouyinLitePort, IOSDouyinLitePort); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + s.douyinLiteUrlPrefix = fmt.Sprintf("http://127.0.0.1:%d", localDouyinLitePort) + return nil +} + +func (s *StubIOSDriver) getLocalPort() (int, error) { + localStubPort, err := builtin.GetFreePort() + if err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = s.Device.Forward(localStubPort, defaultBightInsightPort); err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + return localStubPort, nil +} + +func (s *StubIOSDriver) Source(srcOpt ...option.SourceOption) (string, error) { + resp, err := s.Session.GET("/source?format=json&onlyWeb=false") + if err != nil { + log.Error().Err(err).Msg("get source err") + return "", nil + } + return string(resp), nil +} + +func (s *StubIOSDriver) OpenUrl(urlStr string, opts ...option.ActionOption) (err error) { + targetUrl := fmt.Sprintf("/openURL?url=%s", url.QueryEscape(urlStr)) + _, err = s.Session.GET(targetUrl) + if err != nil { + log.Error().Err(err).Msg("get source err") + return nil + } + return nil +} + +func (s *StubIOSDriver) LoginNoneUI(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error) { + appInfo, err := s.ForegroundInfo() + if err != nil { + return info, err + } + if appInfo.BundleId == "com.ss.iphone.ugc.AwemeInhouse" || appInfo.BundleId == "com.ss.iphone.ugc.awemeinhouse.lite" { + return s.LoginDouyin(appInfo.BundleId, phoneNumber, captcha, password) + } else if appInfo.BundleId == "com.ss.iphone.InHouse.article.Video" { + return s.LoginXigua(appInfo.BundleId, phoneNumber, captcha, password) + } else { + return info, fmt.Errorf("not support app") + } +} + +func (s *StubIOSDriver) LoginXigua(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error) { + loginSchema := "" + if captcha != "" { + loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=1&account=%s&smscode=%s", phoneNumber, captcha) + } else if password != "" { + loginSchema = fmt.Sprintf("snssdk32://local_channel_autologin?login_type=2&account=%s&password=%s", phoneNumber, password) + } else { + return info, fmt.Errorf("password and capcha is empty") + } + info.IsLogin = true + return info, s.OpenUrl(loginSchema) +} + +func (s *StubIOSDriver) LoginDouyin(packageName, phoneNumber, captcha, password string) (info AppLoginInfo, err error) { + params := map[string]interface{}{ + "phone": phoneNumber, + } + if captcha != "" { + params["captcha"] = captcha + } else if password != "" { + params["password"] = password + } else { + return info, fmt.Errorf("password and capcha is empty") + } + urlPrefix, err := s.getUrlPrefix(packageName) + if err != nil { + return info, err + } + fullUrl := urlPrefix + "/host/login/account/" + resp, err := s.Session.POST(params, fullUrl) + if err != nil { + return info, err + } + res, err := resp.ValueConvertToJsonObject() + if err != nil { + return info, err + } + log.Info().Msgf("%v", res) + // {'isSuccess': True, 'data': '登录成功', 'code': 0} + if res["isSuccess"] != true { + err = fmt.Errorf("falied to logout %s", res["data"]) + log.Err(err).Msgf("%v", res) + return info, err + } + time.Sleep(20 * time.Second) + info, err = s.getLoginAppInfo(packageName) + if err != nil || !info.IsLogin { + return info, fmt.Errorf("falied to login %v", info) + } + return info, nil +} + +func (s *StubIOSDriver) LogoutNoneUI(packageName string) error { + urlPrefix, err := s.getUrlPrefix(packageName) + if err != nil { + return err + } + fullUrl := urlPrefix + "/host/loginout/" + resp, err := s.Session.GET(fullUrl) + if err != nil { + return err + } + res, err := resp.ValueConvertToJsonObject() + if err != nil { + return err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to logout %s", res["data"]) + log.Err(err).Msgf("%v", res) + return err + } + time.Sleep(10 * time.Second) + return nil +} + +func (s *StubIOSDriver) EnableDevtool(packageName string, enable bool) (err error) { + urlPrefix, err := s.getUrlPrefix(packageName) + if err != nil { + return err + } + fullUrl := urlPrefix + "/host/devtool/enable" + + params := map[string]interface{}{ + "enable": enable, + } + resp, err := s.Session.POST(params, fullUrl) + if err != nil { + return err + } + res, err := resp.ValueConvertToJsonObject() + if err != nil { + return err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to enable devtool %s", res["data"]) + log.Err(err).Msgf("%v", res) + return err + } + return nil +} + +func (s *StubIOSDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) { + urlPrefix, err := s.getUrlPrefix(packageName) + if err != nil { + return info, err + } + fullUrl := urlPrefix + "/host/app/info/" + + resp, err := s.Session.GET(fullUrl) + if err != nil { + return info, err + } + res, err := resp.ValueConvertToJsonObject() + if err != nil { + return info, err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to get is login %s", res["data"]) + log.Err(err).Msgf("%v", res) + return info, err + } + err = json.Unmarshal([]byte(res["data"].(string)), &info) + if err != nil { + return info, err + } + return info, nil +} + +func (s *StubIOSDriver) getUrlPrefix(packageName string) (urlPrefix string, err error) { + if packageName == "com.ss.iphone.ugc.AwemeInhouse" { + urlPrefix = s.douyinUrlPrefix + } else if packageName == "com.ss.iphone.ugc.awemeinhouse.lite" { + urlPrefix = s.douyinLiteUrlPrefix + } else { + return "", fmt.Errorf("not support app %s", packageName) + } + return urlPrefix, nil +} + +func (s *StubIOSDriver) ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error) { + if err := s.SetupWda(); err != nil { + return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.ScreenShot(opts...) +} + +func (s *StubIOSDriver) AppLaunch(packageName string) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + err := s.WDADriver.AppLaunch(packageName) + if err != nil { + return err + } + _ = s.EnableDevtool(packageName, true) + return nil +} + +func (s *StubIOSDriver) GetDevice() uixt.IDevice { + return s.Device +} + +func (s *StubIOSDriver) TearDown() error { + return nil +} + +// session +func (s *StubIOSDriver) InitSession(capabilities option.Capabilities) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.InitSession(capabilities) +} + +func (s *StubIOSDriver) GetSession() *uixt.DriverSession { + if err := s.SetupWda(); err != nil { + _ = errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + return nil + } + return s.WDADriver.GetSession() +} + +func (s *StubIOSDriver) DeleteSession() error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.DeleteSession() +} + +// device info and status +func (s *StubIOSDriver) Status() (types.DeviceStatus, error) { + if err := s.SetupWda(); err != nil { + return types.DeviceStatus{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Status() +} + +func (s *StubIOSDriver) DeviceInfo() (types.DeviceInfo, error) { + if err := s.SetupWda(); err != nil { + return types.DeviceInfo{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.DeviceInfo() +} + +func (s *StubIOSDriver) BatteryInfo() (types.BatteryInfo, error) { + if err := s.SetupWda(); err != nil { + return types.BatteryInfo{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.BatteryInfo() +} + +func (s *StubIOSDriver) ForegroundInfo() (types.AppInfo, error) { + if err := s.SetupWda(); err != nil { + return types.AppInfo{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.ForegroundInfo() +} + +func (s *StubIOSDriver) WindowSize() (types.Size, error) { + if err := s.SetupWda(); err != nil { + return types.Size{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.WindowSize() +} + +func (s *StubIOSDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { + if err := s.SetupWda(); err != nil { + return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.ScreenRecord(duration) +} + +func (s *StubIOSDriver) Orientation() (types.Orientation, error) { + if err := s.SetupWda(); err != nil { + return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Orientation() +} + +func (s *StubIOSDriver) Rotation() (types.Rotation, error) { + if err := s.SetupWda(); err != nil { + return types.Rotation{}, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Rotation() +} + +func (s *StubIOSDriver) SetRotation(rotation types.Rotation) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.SetRotation(rotation) +} + +func (s *StubIOSDriver) SetIme(ime string) error { + return types.ErrDriverNotImplemented +} + +func (s *StubIOSDriver) Home() error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Home() +} + +func (s *StubIOSDriver) Unlock() error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Unlock() +} + +func (s *StubIOSDriver) Back() error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Back() +} + +func (s *StubIOSDriver) TapXY(x, y float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.TapXY(x, y, opts...) +} + +func (s *StubIOSDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.TapAbsXY(x, y, opts...) +} + +func (s *StubIOSDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.DoubleTap(x, y, opts...) +} + +func (s *StubIOSDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.TouchAndHold(x, y, opts...) +} + +func (s *StubIOSDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Drag(fromX, fromY, toX, toY, opts...) +} + +func (s *StubIOSDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Swipe(fromX, fromY, toX, toY, opts...) +} + +func (s *StubIOSDriver) Input(text string, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Input(text, opts...) +} + +func (s *StubIOSDriver) Backspace(count int, opts ...option.ActionOption) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.Backspace(count, opts...) +} + +func (s *StubIOSDriver) AppTerminate(packageName string) (bool, error) { + if err := s.SetupWda(); err != nil { + return false, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.AppTerminate(packageName) +} + +func (s *StubIOSDriver) AppClear(packageName string) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.AppClear(packageName) +} + +// image related +func (s *StubIOSDriver) PushImage(localPath string) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.PushImage(localPath) +} + +func (s *StubIOSDriver) ClearImages() error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.ClearImages() +} + +// triggers the log capture and returns the log entries +func (s *StubIOSDriver) StartCaptureLog(identifier ...string) error { + if err := s.SetupWda(); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.StartCaptureLog(identifier...) +} + +func (s *StubIOSDriver) StopCaptureLog() (interface{}, error) { + if err := s.SetupWda(); err != nil { + return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + return s.WDADriver.StopCaptureLog() +} diff --git a/uixt/driver_ext/ios_stub_driver_test.go b/uixt/driver_ext/ios_stub_driver_test.go new file mode 100644 index 00000000..abeac4c7 --- /dev/null +++ b/uixt/driver_ext/ios_stub_driver_test.go @@ -0,0 +1,29 @@ +package driver_ext + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/httprunner/httprunner/v5/uixt" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +func setupIOSStubDriver(t *testing.T) *StubIOSDriver { + iOSDevice, err := uixt.NewIOSDevice( + option.WithWDAPort(8700), + option.WithWDAMjpegPort(8800), + option.WithResetHomeOnStartup(false)) + require.Nil(t, err) + iOSStubDriver, err := NewStubIOSDriver(iOSDevice) + require.Nil(t, err) + return iOSStubDriver +} + +func TestIOSStubDriver_LoginNoneUI(t *testing.T) { + iOSStubDriver := setupIOSStubDriver(t) + info, err := iOSStubDriver.LoginNoneUI("com.ss.iphone.ugc.AwemeInhouse", "12343418541", "", "im112233") + assert.Nil(t, err) + t.Logf("login info: %+v", info) +} diff --git a/pkg/uixt/driver_ext_popups.go b/uixt/driver_ext_popups.go similarity index 97% rename from pkg/uixt/driver_ext_popups.go rename to uixt/driver_ext_popups.go index b42b1629..a2c014cb 100644 --- a/pkg/uixt/driver_ext_popups.go +++ b/uixt/driver_ext_popups.go @@ -5,8 +5,8 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) // TODO: add more popup texts diff --git a/pkg/uixt/driver_ext_screenshot.go b/uixt/driver_ext_screenshot.go similarity index 77% rename from pkg/uixt/driver_ext_screenshot.go rename to uixt/driver_ext_screenshot.go index 9ac9d31c..e16fb763 100644 --- a/pkg/uixt/driver_ext_screenshot.go +++ b/uixt/driver_ext_screenshot.go @@ -11,7 +11,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -19,9 +18,9 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) type ScreenResult struct { @@ -50,6 +49,21 @@ func (s *ScreenResult) FilterTextsByScope(x1, y1, x2, y2 float64) ai.OCRTexts { func (dExt *XTDriver) GetScreenResult(opts ...option.ActionOption) (screenResult *ScreenResult, err error) { screenshotOptions := option.NewActionOptions(opts...) + // take screenshot + bufSource, err := dExt.ScreenShot() + if err != nil { + return nil, errors.Wrapf(code.DeviceScreenShotError, + "take screenshot failed %v", err) + } + + // compress screenshot + compressBufSource, err := compressImageBuffer(bufSource) + if err != nil { + return nil, errors.Wrapf(code.DeviceScreenShotError, + "compress screenshot failed %v", err) + } + + // save compressed screenshot to file var fileName string optionsList := screenshotOptions.List() if screenshotOptions.ScreenShotFileName != "" { @@ -59,52 +73,33 @@ func (dExt *XTDriver) GetScreenResult(opts ...option.ActionOption) (screenResult } else { fileName = builtin.GenNameWithTimestamp("%d_screenshot") } - - var bufSource *bytes.Buffer - var imageResult *ai.CVResult - var imagePath string - var windowSize types.Size - var lastErr error - - // get screenshot info with retry - for i := 0; i < 3; i++ { - imagePath = filepath.Join(config.ScreenShotsPath, fileName) - bufSource, err = dExt.ScreenShot(option.WithScreenShotFileName(imagePath)) + imagePath := filepath.Join(config.GetConfig().ScreenShotsPath, fileName) + go func() { + path, err := saveScreenShot(compressBufSource, imagePath) if err != nil { - lastErr = err - time.Sleep(time.Second * 1) - continue + log.Error().Err(err).Msg("save screenshot file failed") + } else { + log.Info().Str("path", path).Msg("screenshot saved") } + }() - windowSize, err = dExt.WindowSize() - if err != nil { - lastErr = errors.Wrap(code.DeviceGetInfoError, err.Error()) - continue - } - - screenResult = &ScreenResult{ - bufSource: bufSource, - ImagePath: imagePath, - Tags: nil, - Resolution: windowSize, - } - imageResult, err = dExt.CVService.ReadFromBuffer(bufSource, opts...) - if err != nil { - log.Error().Err(err).Msg("ReadFromBuffer from ImageService failed") - lastErr = err - continue - } - // success, break the loop - lastErr = nil - break - } - if lastErr != nil { - return nil, lastErr + windowSize, err := dExt.WindowSize() + if err != nil { + return nil, errors.Wrap(code.DeviceGetInfoError, err.Error()) } - // cache screen result - dExt.screenResults = append(dExt.screenResults, screenResult) - + // read image from buffer with CV + screenResult = &ScreenResult{ + bufSource: compressBufSource, + ImagePath: imagePath, + Tags: nil, + Resolution: windowSize, + } + imageResult, err := dExt.CVService.ReadFromBuffer(compressBufSource, opts...) + if err != nil { + log.Error().Err(err).Msg("ReadFromBuffer from ImageService failed") + return nil, err + } if imageResult != nil { screenResult.Texts = imageResult.OCRResult.ToOCRTexts() screenResult.UploadedURL = imageResult.URL @@ -124,6 +119,9 @@ func (dExt *XTDriver) GetScreenResult(opts ...option.ActionOption) (screenResult } } + // cache screen result + dExt.screenResults = append(dExt.screenResults, screenResult) + log.Debug(). Str("imagePath", imagePath). Str("imageUrl", screenResult.UploadedURL). @@ -246,3 +244,28 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { return screenshotPath, nil } + +func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) { + // decode image from buffer + img, format, err := image.Decode(raw) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + + switch format { + // compress image + case "jpeg", "png": + jpegOptions := &jpeg.Options{Quality: 60} + err = jpeg.Encode(&buf, img, jpegOptions) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported image format: %s", format) + } + + // return compressed image buffer + return &buf, nil +} diff --git a/pkg/uixt/driver_ext_swipe.go b/uixt/driver_ext_swipe.go similarity index 97% rename from pkg/uixt/driver_ext_swipe.go rename to uixt/driver_ext_swipe.go index bfde83ea..9cb43574 100644 --- a/pkg/uixt/driver_ext_swipe.go +++ b/uixt/driver_ext_swipe.go @@ -10,8 +10,8 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" ) type Action func(driver *XTDriver) error diff --git a/pkg/uixt/driver_ext_tap.go b/uixt/driver_ext_tap.go similarity index 93% rename from pkg/uixt/driver_ext_tap.go rename to uixt/driver_ext_tap.go index 561faf37..b4e8f151 100644 --- a/pkg/uixt/driver_ext_tap.go +++ b/uixt/driver_ext_tap.go @@ -3,7 +3,7 @@ package uixt import ( "fmt" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/option" ) func (dExt *XTDriver) TapByOCR(text string, opts ...option.ActionOption) error { diff --git a/pkg/uixt/driver_ext_test.go b/uixt/driver_ext_test.go similarity index 82% rename from pkg/uixt/driver_ext_test.go rename to uixt/driver_ext_test.go index 3857cde6..d625f11d 100644 --- a/pkg/uixt/driver_ext_test.go +++ b/uixt/driver_ext_test.go @@ -4,9 +4,10 @@ package uixt import ( "testing" + "time" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -82,6 +83,34 @@ func setupDriverExt(t *testing.T) *XTDriver { } } +func TestDriverExt_FindScreenText(t *testing.T) { + driver := setupDriverExt(t) + point, err := driver.FindScreenText("首页") + assert.Nil(t, err) + t.Log(point) +} + +func TestDriverExt_Seek(t *testing.T) { + driver := setupDriverExt(t) + + point, err := driver.FindScreenText("首页") + assert.Nil(t, err) + + size, err := driver.WindowSize() + assert.Nil(t, err) + width := size.Width + + y := point.Y - 40 + for i := 0; i < 5; i++ { + err := driver.Swipe(0.5, 0.8, 0.5, 0.2) + assert.Nil(t, err) + time.Sleep(1 * time.Second) + err = driver.Swipe(20, y, float64(width)*0.6, y) + assert.Nil(t, err) + time.Sleep(1 * time.Second) + } +} + func TestDriverExt_TapByOCR(t *testing.T) { driver := setupDriverExt(t) err := driver.TapByOCR("天气") diff --git a/pkg/uixt/driver_session.go b/uixt/driver_session.go similarity index 96% rename from pkg/uixt/driver_session.go rename to uixt/driver_session.go index 0bf5e80b..0ff6a068 100644 --- a/pkg/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -38,15 +38,11 @@ type DriverRequests struct { Error string `json:"error,omitempty"` } -const ( - emptySessionID = "" -) - func NewDriverSession() *DriverSession { timeout := 30 * time.Second session := &DriverSession{ ctx: context.Background(), - ID: emptySessionID, + ID: "", timeout: timeout, client: &http.Client{ Timeout: timeout, @@ -101,8 +97,11 @@ func (s *DriverSession) concatURL(urlStr string) (string, error) { } // replace with session ID - if s.ID != emptySessionID { - urlStr = strings.Replace(urlStr, emptySessionID, s.ID, 1) + if s.ID != "" && !strings.Contains(urlStr, s.ID) { + sessionPattern := regexp.MustCompile(`/session/([^/]+)/`) + if matches := sessionPattern.FindStringSubmatch(urlStr); len(matches) != 0 { + urlStr = strings.Replace(urlStr, matches[1], s.ID, 1) + } } // 处理完整 URL diff --git a/pkg/uixt/driver_session_test.go b/uixt/driver_session_test.go similarity index 100% rename from pkg/uixt/driver_session_test.go rename to uixt/driver_session_test.go diff --git a/pkg/uixt/driver_utils.go b/uixt/driver_utils.go similarity index 67% rename from pkg/uixt/driver_utils.go rename to uixt/driver_utils.go index d343ac6d..127e2cd4 100644 --- a/pkg/uixt/driver_utils.go +++ b/uixt/driver_utils.go @@ -1,16 +1,24 @@ package uixt import ( + "crypto/md5" "fmt" + "io" "math" "math/rand/v2" + "net/http" + "os" + "path/filepath" + "sync" "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/internal/config" + "github.com/httprunner/httprunner/v5/uixt/option" ) func convertToAbsoluteScope(driver IDriver, opts ...option.ActionOption) []option.ActionOption { @@ -32,48 +40,62 @@ func convertToAbsoluteScope(driver IDriver, opts ...option.ActionOption) []optio } func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, err error) { - if !assertRelative(x) || !assertRelative(y) { - err = errors.Wrap(code.InvalidCaseError, - fmt.Sprintf("x(%f), y(%f) must be less than 1", x, y)) - return + // absolute coordinates + if x > 1 || y > 1 { + return x, y, nil } - windowSize, err := driver.WindowSize() - if err != nil { - err = errors.Wrap(code.DeviceGetInfoError, err.Error()) - return + // relative coordinates + if assertRelative(x) && assertRelative(y) { + windowSize, err := driver.WindowSize() + if err != nil { + err = errors.Wrap(code.DeviceGetInfoError, err.Error()) + return 0, 0, err + } + + absX = math.Round(float64(windowSize.Width)*x*10) / 10 + absY = math.Round(float64(windowSize.Height)*y*10) / 10 + return absX, absY, nil } - absX = math.Round(float64(windowSize.Width)*x*10) / 10 - absY = math.Round(float64(windowSize.Height)*y*10) / 10 + // invalid coordinates + err = errors.Wrap(code.InvalidCaseError, + fmt.Sprintf("invalid coordinates x(%f), y(%f)", x, y)) return } func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) ( absFromX, absFromY, absToX, absToY float64, err error) { - if !assertRelative(fromX) || !assertRelative(fromY) || - !assertRelative(toX) || !assertRelative(toY) { - err = errors.Wrap(code.InvalidCaseError, - fmt.Sprintf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1", - fromX, fromY, toX, toY)) - return + // absolute coordinates + if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 { + return fromX, fromY, toX, toY, nil } - windowSize, err := driver.WindowSize() - if err != nil { - err = errors.Wrap(code.DeviceGetInfoError, err.Error()) - return + // relative coordinates + if assertRelative(fromX) && assertRelative(fromY) && + assertRelative(toX) && assertRelative(toY) { + windowSize, err := driver.WindowSize() + if err != nil { + err = errors.Wrap(code.DeviceGetInfoError, err.Error()) + return 0, 0, 0, 0, err + } + width := windowSize.Width + height := windowSize.Height + + absFromX = float64(width) * fromX + absFromY = float64(height) * fromY + absToX = float64(width) * toX + absToY = float64(height) * toY + + return absFromX, absFromY, absToX, absToY, nil } - width := windowSize.Width - height := windowSize.Height - absFromX = float64(width) * fromX - absFromY = float64(height) * fromY - absToX = float64(width) * toX - absToY = float64(height) * toY - - return absFromX, absFromY, absToX, absToY, nil + // invalid coordinates + err = errors.Wrap(code.InvalidCaseError, + fmt.Sprintf("invalid coordinates fromX(%f), fromY(%f), toX(%f), toY(%f)", + fromX, fromY, toX, toY)) + return } func assertRelative(p float64) bool { @@ -259,3 +281,63 @@ func sleepStrict(startTime time.Time, strictMilliseconds int64) { Msg("sleep remaining duration time") time.Sleep(time.Duration(dur) * time.Millisecond) } + +// global file lock +var ( + fileLocks sync.Map +) + +func DownloadFileByUrl(fileUrl string) (filePath string, err error) { + hash := md5.Sum([]byte(fileUrl)) + fileName := fmt.Sprintf("%x", hash) + filePath = filepath.Join(config.GetConfig().DownloadsPath, fileName) + + // get or create file lock + lockI, _ := fileLocks.LoadOrStore(filePath, &sync.Mutex{}) + lock := lockI.(*sync.Mutex) + lock.Lock() + defer lock.Unlock() + + if builtin.FileExists(filePath) { + return filePath, nil + } + + log.Info().Str("fileUrl", fileUrl).Str("filePath", filePath).Msg("downloading file") + + // Create an HTTP client with default settings. + client := &http.Client{} + + // Build the HTTP GET request. + req, err := http.NewRequest("GET", fileUrl, nil) + if err != nil { + return "", err + } + + // Perform the request. + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // Check the HTTP status code. + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to download file: %s", resp.Status) + } + + // Create the output file. + outFile, err := os.Create(filePath) + if err != nil { + return "", err + } + defer outFile.Close() + + // Copy the response body to the file. + _, err = io.Copy(outFile, resp.Body) + if err != nil { + return "", err + } + + log.Info().Str("filePath", filePath).Msg("download file success") + return filePath, nil +} diff --git a/pkg/uixt/driver_utils_test.go b/uixt/driver_utils_test.go similarity index 100% rename from pkg/uixt/driver_utils_test.go rename to uixt/driver_utils_test.go diff --git a/pkg/uixt/evalite b/uixt/evalite similarity index 100% rename from pkg/uixt/evalite rename to uixt/evalite diff --git a/pkg/uixt/harmony_device.go b/uixt/harmony_device.go similarity index 92% rename from pkg/uixt/harmony_device.go rename to uixt/harmony_device.go index 5c0869a5..545dca9b 100644 --- a/pkg/uixt/harmony_device.go +++ b/uixt/harmony_device.go @@ -1,13 +1,15 @@ package uixt import ( + "bytes" + "code.byted.org/iesqa/ghdc" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) type HarmonyDevice struct { @@ -106,3 +108,7 @@ func (dev *HarmonyDevice) NewDriver() (IDriver, error) { } return driver, nil } + +func (dev *HarmonyDevice) ScreenShot() (*bytes.Buffer, error) { + return nil, errors.New("not implemented") +} diff --git a/pkg/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go similarity index 90% rename from pkg/uixt/harmony_driver_hdc.go rename to uixt/harmony_driver_hdc.go index 6be1fa98..bb55eb6e 100644 --- a/pkg/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -12,8 +12,8 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func NewHDCDriver(device *HarmonyDevice) (*HDCDriver, error) { @@ -161,7 +161,7 @@ func (hd *HDCDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { ghdc.NewGesture().Start(ghdc.Point{X: int(x), Y: int(y)}).Pause(100)) } -func (hd *HDCDriver) DoubleTapXY(x, y float64, opts ...option.ActionOption) error { +func (hd *HDCDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { return types.ErrDriverNotImplemented } @@ -177,11 +177,9 @@ func (hd *HDCDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO func (hd *HDCDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { var err error actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(hd, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(hd, fromX, fromY, toX, toY) + if err != nil { + return err } duration := 200 @@ -239,18 +237,6 @@ func (hd *HDCDriver) ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, err return nil, err } rawBuffer := bytes.NewBuffer(raw) - - actionOptions := option.NewActionOptions(opts...) - if actionOptions.ScreenShotFileName != "" { - // save screenshot to file - path, err := saveScreenShot(rawBuffer, actionOptions.ScreenShotFileName) - if err != nil { - return nil, errors.Wrapf(code.DeviceScreenShotError, - "save screenshot file failed %v", err) - } - log.Info().Str("path", path).Msg("screenshot saved") - } - return rawBuffer, nil } diff --git a/pkg/uixt/harmony_test.go b/uixt/harmony_test.go similarity index 97% rename from pkg/uixt/harmony_test.go rename to uixt/harmony_test.go index 6a17b2bb..f3a5bfc0 100644 --- a/pkg/uixt/harmony_test.go +++ b/uixt/harmony_test.go @@ -6,7 +6,7 @@ import ( "fmt" "testing" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/ai" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/uixt/ios_device.go b/uixt/ios_device.go similarity index 89% rename from pkg/uixt/ios_device.go rename to uixt/ios_device.go index 0cb7adbc..79d3131e 100644 --- a/pkg/uixt/ios_device.go +++ b/uixt/ios_device.go @@ -1,6 +1,7 @@ package uixt import ( + "bytes" "context" "encoding/json" "fmt" @@ -23,11 +24,11 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) -func StartTunnel(recordsPath string, tunnelInfoPort int, userspaceTUN bool) (err error) { +func StartTunnel(ctx context.Context, recordsPath string, tunnelInfoPort int, userspaceTUN bool) (err error) { pm, err := tunnel.NewPairRecordManager(recordsPath) if err != nil { return err @@ -37,7 +38,7 @@ func StartTunnel(recordsPath string, tunnelInfoPort int, userspaceTUN bool) (err ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { - err := tm.UpdateTunnels(context.Background()) + err := tm.UpdateTunnels(ctx) if err != nil { log.Error().Err(err).Msg("failed to update tunnels") } @@ -61,7 +62,7 @@ func RebootTunnel() (err error) { if tunnelManager != nil { _ = tunnelManager.Close() } - return StartTunnel(os.TempDir(), ios.HttpApiPort(), true) + return StartTunnel(context.Background(), os.TempDir(), ios.HttpApiPort(), true) } func NewIOSDevice(opts ...option.IOSDeviceOption) (device *IOSDevice, err error) { @@ -150,22 +151,31 @@ const ( ) func (dev *IOSDevice) Setup() error { - images, err := dev.ListImages() - if err != nil { - return err - } version, err := dev.getVersion() if err != nil { return err } - if len(images) == 0 && version.LessThan(ios.IOS17()) { - // Notice: iOS 17.0+ does not need to mount developer image - err = dev.AutoMountImage(os.TempDir()) + if version.GreaterThan(semver.MustParse("17.4.0")) { + info, err := tunnel.TunnelInfoForDevice(dev.DeviceEntry.Properties.SerialNumber, ios.HttpApiHost(), ios.HttpApiPort()) if err != nil { return err } + dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort + dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN + rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry) + defer rsdService.Close() + rsdProvider, err := rsdService.Handshake() + if err != nil { + return err + } + device, err := ios.GetDeviceWithAddress(dev.DeviceEntry.Properties.SerialNumber, info.Address, rsdProvider) + if err != nil { + return err + } + device.UserspaceTUN = dev.DeviceEntry.UserspaceTUN + device.UserspaceTUNPort = dev.DeviceEntry.UserspaceTUNPort + dev.DeviceEntry = device } - return nil } @@ -326,6 +336,22 @@ func (dev *IOSDevice) ListApps(appType ApplicationType) (apps []installationprox return apps, nil } +func (dev *IOSDevice) ScreenShot() (*bytes.Buffer, error) { + screenshotService, err := instruments.NewScreenshotService(dev.DeviceEntry) + if err != nil { + log.Error().Err(err).Msg("Starting screenshot service failed") + return nil, err + } + defer screenshotService.Close() + + imageBytes, err := screenshotService.TakeScreenshot() + if err != nil { + log.Error().Err(err).Msg("failed to task screenshot") + return nil, err + } + return bytes.NewBuffer(imageBytes), nil +} + func (dev *IOSDevice) GetAppInfo(packageName string) (appInfo installationproxy.AppInfo, err error) { svc, _ := installationproxy.New(dev.DeviceEntry) defer svc.Close() diff --git a/pkg/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go similarity index 97% rename from pkg/uixt/ios_driver_wda.go rename to uixt/ios_driver_wda.go index f9f30505..5d6f6381 100644 --- a/pkg/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -26,8 +26,8 @@ import ( "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func NewWDADriver(device *IOSDevice) (*WDADriver, error) { @@ -157,7 +157,7 @@ func (wd *WDADriver) Setup() error { } func (wd *WDADriver) TearDown() error { - return wd.DeleteSession() + return nil } func (wd *WDADriver) InitSession(capabilities option.Capabilities) error { @@ -334,19 +334,7 @@ func (wd *WDADriver) ScreenShot(opts ...option.ActionOption) (raw *bytes.Buffer, return nil, errors.Wrap(code.DeviceScreenShotError, fmt.Sprintf("decode WDA screenshot data failed: %v", err)) } - - actionOptions := option.NewActionOptions(opts...) - if actionOptions.ScreenShotFileName != "" { - // save screenshot to file - path, err := saveScreenShot(raw, actionOptions.ScreenShotFileName) - if err != nil { - return nil, errors.Wrapf(code.DeviceScreenShotError, - "save screenshot file failed %v", err) - } - log.Info().Str("path", path).Msg("screenshot saved") - } - - return + return raw, nil } func (wd *WDADriver) toScale(x float64) float64 { @@ -611,15 +599,16 @@ func (wd *WDADriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { return err } -func (wd *WDADriver) DoubleTapXY(x, y float64, opts ...option.ActionOption) error { +func (wd *WDADriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { // [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)] var err error - if x, y, err = convertToAbsolutePoint(wd, x, y); err != nil { + x, y, err = convertToAbsolutePoint(wd, x, y) + if err != nil { return err } + actionOptions := option.NewActionOptions(opts...) x, y = actionOptions.ApplyOffset(x, y) - x = wd.toScale(x) y = wd.toScale(y) data := map[string]interface{}{ @@ -644,12 +633,9 @@ func (wd *WDADriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (er func (wd *WDADriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { // [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)] var err error - actionOptions := option.NewActionOptions(opts...) - if !actionOptions.AbsCoordinate { - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(wd, fromX, fromY, toX, toY) - if err != nil { - return err - } + fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(wd, fromX, fromY, toX, toY) + if err != nil { + return err } fromX = wd.toScale(fromX) fromY = wd.toScale(fromY) @@ -891,7 +877,7 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, func (wd *WDADriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) - fileName := filepath.Join(config.ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) + fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) file, err := os.Create(fileName) if err != nil { diff --git a/pkg/uixt/ios_test.go b/uixt/ios_test.go similarity index 97% rename from pkg/uixt/ios_test.go rename to uixt/ios_test.go index af736796..98a60a57 100644 --- a/pkg/uixt/ios_test.go +++ b/uixt/ios_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/pkg/uixt/types" + "github.com/httprunner/httprunner/v5/uixt/ai" + "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func setupWDADriverExt(t *testing.T) *XTDriver { @@ -196,7 +196,7 @@ func TestDriver_WDA_TapXY(t *testing.T) { func TestDriver_WDA_DoubleTapXY(t *testing.T) { driver := setupWDADriverExt(t) - err := driver.DoubleTapXY(0.2, 0.2) + err := driver.DoubleTap(0.2, 0.2) assert.Nil(t, err) } diff --git a/pkg/uixt/option/action.go b/uixt/option/action.go similarity index 96% rename from pkg/uixt/option/action.go rename to uixt/option/action.go index abd767f1..4447638a 100644 --- a/pkg/uixt/option/action.go +++ b/uixt/option/action.go @@ -19,7 +19,6 @@ type ActionOptions struct { Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"` - AbsCoordinate bool `json:"abs,omitempty" yaml:"abs,omitempty"` // use absolute coordinate ScreenOptions @@ -56,9 +55,6 @@ func (o *ActionOptions) Options() []ActionOption { if o.Steps != 0 { options = append(options, WithSteps(o.Steps)) } - if o.AbsCoordinate { - options = append(options, WithAbsoluteCoordinate(true)) - } switch v := o.Direction.(type) { case string: @@ -260,12 +256,6 @@ func WithDirection(direction string) ActionOption { } } -func WithAbsoluteCoordinate(abs bool) ActionOption { - return func(o *ActionOptions) { - o.AbsCoordinate = abs - } -} - // WithCustomDirection inputs sx, sy, ex, ey func WithCustomDirection(sx, sy, ex, ey float64) ActionOption { return func(o *ActionOptions) { diff --git a/pkg/uixt/option/android.go b/uixt/option/android.go similarity index 100% rename from pkg/uixt/option/android.go rename to uixt/option/android.go diff --git a/uixt/option/browser.go b/uixt/option/browser.go new file mode 100644 index 00000000..a3232762 --- /dev/null +++ b/uixt/option/browser.go @@ -0,0 +1,22 @@ +package option + +func NewBrowserDeviceOptions(opts ...BrowserDeviceOption) *BrowserDeviceOptions { + config := &BrowserDeviceOptions{} + for _, opt := range opts { + opt(config) + } + return config +} + +type BrowserDeviceOptions struct { + BrowserID string `json:"browser_id,omitempty" yaml:"browser_id,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +type BrowserDeviceOption func(*BrowserDeviceOptions) + +func WithBrowserID(serial string) BrowserDeviceOption { + return func(device *BrowserDeviceOptions) { + device.BrowserID = serial + } +} diff --git a/pkg/uixt/option/capabilities.go b/uixt/option/capabilities.go similarity index 100% rename from pkg/uixt/option/capabilities.go rename to uixt/option/capabilities.go diff --git a/pkg/uixt/option/harmony.go b/uixt/option/harmony.go similarity index 100% rename from pkg/uixt/option/harmony.go rename to uixt/option/harmony.go diff --git a/pkg/uixt/option/install.go b/uixt/option/install.go similarity index 100% rename from pkg/uixt/option/install.go rename to uixt/option/install.go diff --git a/pkg/uixt/option/ios.go b/uixt/option/ios.go similarity index 100% rename from pkg/uixt/option/ios.go rename to uixt/option/ios.go diff --git a/pkg/uixt/option/screen.go b/uixt/option/screen.go similarity index 99% rename from pkg/uixt/option/screen.go rename to uixt/option/screen.go index 3e30c7ec..60af4b31 100644 --- a/pkg/uixt/option/screen.go +++ b/uixt/option/screen.go @@ -1,6 +1,6 @@ package option -import "github.com/httprunner/httprunner/v5/pkg/uixt/types" +import "github.com/httprunner/httprunner/v5/uixt/types" type ScreenOptions struct { ScreenShotOptions diff --git a/pkg/uixt/option/source.go b/uixt/option/source.go similarity index 100% rename from pkg/uixt/option/source.go rename to uixt/option/source.go diff --git a/pkg/uixt/types/app.go b/uixt/types/app.go similarity index 100% rename from pkg/uixt/types/app.go rename to uixt/types/app.go diff --git a/pkg/uixt/types/device.go b/uixt/types/device.go similarity index 100% rename from pkg/uixt/types/device.go rename to uixt/types/device.go diff --git a/pkg/uixt/types/driver.go b/uixt/types/driver.go similarity index 100% rename from pkg/uixt/types/driver.go rename to uixt/types/driver.go diff --git a/pkg/uixt/types/ui.go b/uixt/types/ui.go similarity index 100% rename from pkg/uixt/types/ui.go rename to uixt/types/ui.go