From d0b2568797a9bd8bb5ca216360bc92641737f8df Mon Sep 17 00:00:00 2001 From: buyuxiang <347586493@qq.com> Date: Tue, 24 May 2022 13:36:34 +0800 Subject: [PATCH] refactor: hrp convert --- docs/CHANGELOG.md | 1 + docs/cmd/hrp.md | 4 +- docs/cmd/hrp_boom.md | 2 +- docs/cmd/hrp_convert.md | 14 +- docs/cmd/hrp_har2case.md | 2 +- docs/cmd/hrp_postman2case.md | 26 - docs/cmd/hrp_pytest.md | 2 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_startproject.md | 2 +- .../har/{profile.yml => profile_override.yml} | 1 + examples/data/postman2case/patch.yml | 4 - examples/data/postman2case/profile.yml | 4 +- .../data/postman2case/profile_override.yml | 5 + go.mod | 1 + go.sum | 27 +- hrp/cmd/convert.go | 55 +- hrp/cmd/har2case.go | 7 - hrp/cmd/postman2case.go | 79 -- hrp/internal/builtin/utils.go | 9 +- hrp/internal/convert/README.md | 68 ++ hrp/internal/convert/asset/flowgram.svg | 1 + hrp/internal/convert/case2script/main.go | 120 --- hrp/internal/convert/converter.go | 374 +++++++++ hrp/internal/convert/converter_gotest.go | 60 ++ hrp/internal/convert/converter_har.go | 716 ++++++++++++++++++ hrp/internal/convert/converter_har_test.go | 373 +++++++++ hrp/internal/convert/converter_json.go | 111 +++ .../core.go => converter_postman.go} | 343 +++++---- ...core_test.go => converter_postman_test.go} | 46 +- hrp/internal/convert/converter_pytest.go | 19 + hrp/internal/convert/converter_yaml.go | 94 +++ hrp/internal/convert/har2case/core.go | 87 +-- hrp/internal/convert/har2case/core_test.go | 32 +- .../convert/postman2case/collection.go | 74 -- .../convert/{case2script => }/testcase.tmpl | 0 hrp/step_api.go | 2 +- hrp/testcase.go | 104 ++- 37 files changed, 2245 insertions(+), 626 deletions(-) delete mode 100644 docs/cmd/hrp_postman2case.md rename examples/data/har/{profile.yml => profile_override.yml} (86%) delete mode 100644 examples/data/postman2case/patch.yml create mode 100644 examples/data/postman2case/profile_override.yml delete mode 100644 hrp/cmd/postman2case.go create mode 100644 hrp/internal/convert/README.md create mode 100644 hrp/internal/convert/asset/flowgram.svg delete mode 100644 hrp/internal/convert/case2script/main.go create mode 100644 hrp/internal/convert/converter.go create mode 100644 hrp/internal/convert/converter_gotest.go create mode 100644 hrp/internal/convert/converter_har.go create mode 100644 hrp/internal/convert/converter_har_test.go create mode 100644 hrp/internal/convert/converter_json.go rename hrp/internal/convert/{postman2case/core.go => converter_postman.go} (52%) rename hrp/internal/convert/{postman2case/core_test.go => converter_postman_test.go} (73%) create mode 100644 hrp/internal/convert/converter_pytest.go create mode 100644 hrp/internal/convert/converter_yaml.go delete mode 100644 hrp/internal/convert/postman2case/collection.go rename hrp/internal/convert/{case2script => }/testcase.tmpl (100%) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 118ede44..4a06287a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,7 @@ - fix: step request elapsed timing should contain ContentTransfer part - fix #1288: unable to go get httprunner v4 - feat: support converting Postman collection to HttpRunner testcase +- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter` **python version** diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 2620d07f..578091e9 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -30,10 +30,10 @@ Copyright 2017 debugtalk ### SEE ALSO * [hrp boom](hrp_boom.md) - run load test with boomer -* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts +* [hrp convert](hrp_convert.md) - convert external cases to JSON/YAML/gotest/pytest testcases * [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files * [hrp pytest](hrp_pytest.md) - run API test with pytest * [hrp run](hrp_run.md) - run API test with go engine * [hrp startproject](hrp_startproject.md) - create a scaffold project -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index ad27f7b2..37675fcc 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -41,4 +41,4 @@ hrp boom [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 7390e9cc..d4771aad 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -1,6 +1,6 @@ ## hrp convert -convert JSON/YAML testcases to pytest/gotest scripts +convert external cases to JSON/YAML/gotest/pytest testcases ``` hrp convert $path... [flags] @@ -9,13 +9,17 @@ hrp convert $path... [flags] ### Options ``` - --gotest convert to gotest scripts (TODO) - -h, --help help for convert - --pytest convert to pytest scripts (default true) + -h, --help help for convert + -d, --output-dir string specify output directory, default to the same dir with har file + -p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies + --to-gotest convert to gotest scripts (TODO) + --to-json convert to JSON scripts (default) + --to-pytest convert to pytest scripts + --to-yaml convert to YAML scripts ``` ### SEE ALSO * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index db6b8b10..0ef151a3 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -24,4 +24,4 @@ hrp har2case $har_path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_postman2case.md b/docs/cmd/hrp_postman2case.md deleted file mode 100644 index 23c196e7..00000000 --- a/docs/cmd/hrp_postman2case.md +++ /dev/null @@ -1,26 +0,0 @@ -## hrp postman2case - -convert postman collection to json/yaml testcase files - -### Synopsis - -convert postman collection to json/yaml testcase files - -``` -hrp postman2case $postman_path... [flags] -``` - -### Options - -``` - -h, --help help for postman2case - -d, --output-dir string specify output directory, default to the same dir with postman collection file - -j, --to-json convert to JSON format (default true) - -y, --to-yaml convert to YAML format -``` - -### SEE ALSO - -* [hrp](hrp.md) - Next-Generation API Testing Solution. - -###### Auto generated by spf13/cobra on 12-May-2022 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index b2217ca1..2ed3b104 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -16,4 +16,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 6ffdd6d2..ff4ba4a7 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -35,4 +35,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index 4987cd6d..d598c7aa 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -20,4 +20,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 9-May-2022 +###### Auto generated by spf13/cobra on 23-May-2022 diff --git a/examples/data/har/profile.yml b/examples/data/har/profile_override.yml similarity index 86% rename from examples/data/har/profile.yml rename to examples/data/har/profile_override.yml index 69963ba2..35236a52 100644 --- a/examples/data/har/profile.yml +++ b/examples/data/har/profile_override.yml @@ -1,3 +1,4 @@ +override: true headers: Content-Type: "application/x-www-form-urlencoded" cookies: diff --git a/examples/data/postman2case/patch.yml b/examples/data/postman2case/patch.yml deleted file mode 100644 index c657b5ef..00000000 --- a/examples/data/postman2case/patch.yml +++ /dev/null @@ -1,4 +0,0 @@ -headers: - User-Agent: "this header will be created or updated" -cookies: - Cookie1: "this cookie will be created or updated" diff --git a/examples/data/postman2case/profile.yml b/examples/data/postman2case/profile.yml index 42e2e9f4..c657b5ef 100644 --- a/examples/data/postman2case/profile.yml +++ b/examples/data/postman2case/profile.yml @@ -1,4 +1,4 @@ headers: - Header1: "all original headers will be overridden" + User-Agent: "this header will be created or updated" cookies: - Cookie1: "all original cookies will be overridden" \ No newline at end of file + Cookie1: "this cookie will be created or updated" diff --git a/examples/data/postman2case/profile_override.yml b/examples/data/postman2case/profile_override.yml new file mode 100644 index 00000000..bc620e50 --- /dev/null +++ b/examples/data/postman2case/profile_override.yml @@ -0,0 +1,5 @@ +override: true +headers: + Header1: "all original headers will be overridden" +cookies: + Cookie1: "all original cookies will be overridden" \ No newline at end of file diff --git a/go.mod b/go.mod index 5dc2859b..ddf4db03 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 + github.com/go-openapi/spec v0.20.6 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.1 github.com/httprunner/funplugin v0.4.5 diff --git a/go.sum b/go.sum index 62502254..26432fc7 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -131,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -262,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -288,16 +301,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU= github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -348,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -837,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -854,6 +874,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 48a9f4bc..31c536e4 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -2,48 +2,61 @@ package cmd import ( "errors" - "os" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/convert/case2script" + "github.com/httprunner/httprunner/v4/hrp/internal/convert" ) var convertCmd = &cobra.Command{ Use: "convert $path...", - Short: "convert JSON/YAML testcases to pytest/gotest scripts", - Args: cobra.ExactValidArgs(1), + Short: "convert external cases to JSON/YAML/gotest/pytest testcases", + Args: cobra.MinimumNArgs(1), PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, RunE: func(cmd *cobra.Command, args []string) error { - // TODO: integrate har2case, postman2case, etc. in convert command (forward compatibility) - if !pytestFlag && !gotestFlag { - return errors.New("please specify convertion type") + var flagCount int + var outputType convert.OutputType + if toJSONFlag { + flagCount++ } - - var err error - if gotestFlag { - err = case2script.Convert2TestScripts("gotest", args...) - } else { - err = case2script.Convert2TestScripts("pytest", args...) + if toYAMLFlag { + flagCount++ + outputType = convert.OutputTypeYAML } - if err != nil { - log.Error().Err(err).Msg("convert test scripts failed") - os.Exit(1) + if toGoTestFlag { + flagCount++ + outputType = convert.OutputTypeGoTest } + if toPyTestFlag { + flagCount++ + outputType = convert.OutputTypePyTest + } + if flagCount > 1 { + return errors.New("please specify at most one conversion flag") + } + iCaseConverters := convert.LoadConverters(outputType, outputDir, profilePath, args) + convert.Run(iCaseConverters) return nil }, } var ( - pytestFlag bool - gotestFlag bool + toJSONFlag bool + toYAMLFlag bool + toGoTestFlag bool + toPyTestFlag bool + outputDir string + profilePath string ) func init() { rootCmd.AddCommand(convertCmd) - convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts") - convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)") + convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts") + convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)") + convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)") + convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts") + convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") + convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers (except for auto-generated headers) and cookies") } diff --git a/hrp/cmd/har2case.go b/hrp/cmd/har2case.go index 42fab1bd..d26fc4ff 100644 --- a/hrp/cmd/har2case.go +++ b/hrp/cmd/har2case.go @@ -40,11 +40,6 @@ var har2caseCmd = &cobra.Command{ har.SetProfile(har2caseProfilePath) } - // specify profile - if har2casePatchPath != "" { - har.SetPatch(har2casePatchPath) - } - // generate json/yaml files if har2caseGenYAMLFlag { outputPath, err = har.GenYAML() @@ -66,7 +61,6 @@ var ( har2caseGenYAMLFlag bool har2caseOutputDir string har2caseProfilePath string - har2casePatchPath string ) func init() { @@ -75,5 +69,4 @@ func init() { har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format") har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies") - har2caseCmd.Flags().StringVarP(&har2casePatchPath, "patch", "r", "", "specify the path of the file used to replace headers and cookies") } diff --git a/hrp/cmd/postman2case.go b/hrp/cmd/postman2case.go deleted file mode 100644 index 2e0c1369..00000000 --- a/hrp/cmd/postman2case.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "errors" - - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/httprunner/httprunner/v4/hrp/internal/convert/postman2case" -) - -// postman2caseCmd represents the postman2case command -var postman2caseCmd = &cobra.Command{ - Use: "postman2case $postman_path...", - Short: "convert postman collection to json/yaml testcase files", - Long: `convert postman collection to json/yaml testcase files`, - Args: cobra.MinimumNArgs(1), - PreRun: func(cmd *cobra.Command, args []string) { - setLogLevel(logLevel) - }, - RunE: func(cmd *cobra.Command, args []string) error { - var outputFiles []string - for _, arg := range args { - // must choose one - if !postman2caseGenJSONFlag && !postman2caseGenYAMLFlag { - return errors.New("please select convert format type") - } - var outputPath string - var err error - - collection := postman2case.NewCollection(arg) - - // specify output dir - if postman2caseOutputDir != "" { - collection.SetOutputDir(postman2caseOutputDir) - } - - // specify profile path - if postman2caseProfilePath != "" { - collection.SetProfile(postman2caseProfilePath) - } - - // specify patch path - if postman2casePatchPath != "" { - collection.SetPatch(postman2casePatchPath) - } - - // generate json/yaml files - if postman2caseGenYAMLFlag { - outputPath, err = collection.GenYAML() - } else { - outputPath, err = collection.GenJSON() // default - } - if err != nil { - return err - } - outputFiles = append(outputFiles, outputPath) - } - log.Info().Strs("output", outputFiles).Msg("convert testcase success") - return nil - }, -} - -var ( - postman2caseGenJSONFlag bool - postman2caseGenYAMLFlag bool - postman2caseOutputDir string - postman2caseProfilePath string - postman2casePatchPath string -) - -func init() { - rootCmd.AddCommand(postman2caseCmd) - postman2caseCmd.Flags().BoolVarP(&postman2caseGenJSONFlag, "to-json", "j", true, "convert to JSON format") - postman2caseCmd.Flags().BoolVarP(&postman2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format") - postman2caseCmd.Flags().StringVarP(&postman2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with postman collection file") - postman2caseCmd.Flags().StringVarP(&postman2caseProfilePath, "profile", "p", "", "specify profile path to override original headers (except for Content-Type) and cookies") - postman2caseCmd.Flags().StringVarP(&postman2casePatchPath, "patch", "r", "", "specify patch path to create or update headers and cookies") -} diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index cacad024..d32adfde 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -285,7 +285,8 @@ func LoadFile(path string, structObj interface{}) (err error) { if err != nil { return errors.Wrap(err, "read file failed") } - + // remove BOM at the beginning of file + file = bytes.Trim(file, "\xef\xbb\xbf") ext := filepath.Ext(path) switch ext { case ".json", ".har": @@ -351,3 +352,9 @@ func readFile(path string) ([]byte, error) { } return file, nil } + +func GetOutputNameWithoutExtension(path string) string { + base := filepath.Base(path) + ext := filepath.Ext(base) + return base[0:len(base)-len(ext)] + "_test" +} diff --git a/hrp/internal/convert/README.md b/hrp/internal/convert/README.md new file mode 100644 index 00000000..474c8c0e --- /dev/null +++ b/hrp/internal/convert/README.md @@ -0,0 +1,68 @@ +# hrp convert + +## 快速上手 +```shell +$ hrp convert -h +convert external cases to JSON/YAML/gotest/pytest testcases + +Usage: + hrp convert $path... [flags] + +Flags: + -h, --help help for convert + -d, --output-dir string specify output directory, default to the same dir with har file + -p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies + --to-gotest convert to gotest scripts (TODO) + --to-json convert to JSON scripts (default true) + --to-pytest convert to pytest scripts + --to-yaml convert to YAML scripts + +Global Flags: + --log-json set log to json format + -l, --log-level string set log level (default "INFO") +``` +`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 等格式的外部脚本转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化,输出的测试用例文件名格式为 `不带扩展名的原文件名称` + `_test` + `json/yaml/go/py` 后缀。 + +该指令的所有参数的详细介绍如下: + +1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入的外部脚本转化为对应形态的测试用例,四个参数中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例 +2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹 +3. `--profile` 后接 `profile` 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,`profile` 文件的后缀可以为 `json/yaml/yml`,下面给出两类 `profile` 配置文件的示例: +- 根据 `profile` 替换指定的 `Headers` 和 `Cookies` 信息 +```yaml +headers: + Header1: "this header will be created or updated" +cookies: + Cookie1: "this cookie will be created or updated" + +``` +- 根据 `profile` 覆盖原有的 `Headers` 和 `Cookies` 信息 +```yaml +override: true +headers: + Header1: "all original headers will be overridden" +cookies: + Cookie1: "all original cookies will be overridden" +``` + +## 注意事项 +1. 指定 `override` 为 `false/true` 可以选择 `profile` 的修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 `profile` 的默认修改模式为**替换**模式, +2. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎之间的差异(请求体、断言部分的格式略有不同),输出的 JSON/YAML 则统一采用 Golang 引擎的风格 + + +## 转换流程图 + +![flow chart](asset/flowgram.svg) + +## 开发进度 + +| from \ to | JSON | YAML | GoTest | PyTest | +|:---------:|:----:|:----:|:------:|:------:| +| HAR | ✅ | ✅ | ❌ | ✅ | +| Postman | ✅ | ✅ | ❌ | ✅ | +| JMeter | ❌ | ❌ | ❌ | ❌ | +| Swagger | ❌ | ❌ | ❌ | ❌ | +| JSON | ✅ | ✅ | ❌ | ✅ | +| YAML | ✅ | ✅ | ❌ | ✅ | +| GoTest | ❌ | ❌ | ❌ | ❌ | +| PyTest | ❌ | ❌ | ❌ | ❌ | \ No newline at end of file diff --git a/hrp/internal/convert/asset/flowgram.svg b/hrp/internal/convert/asset/flowgram.svg new file mode 100644 index 00000000..76652f6b --- /dev/null +++ b/hrp/internal/convert/asset/flowgram.svg @@ -0,0 +1 @@ +
HTTP 存档格式文件
(.har)
Postman 项目文件
(.json)
JMeter 项目文件
(.jmx)
gotest 测试用例
(.go)
pytest 测试用例
(.py)
JSON 测试用例
(.json)
YAML 测试用例
(.yaml)
Swagger 脚本文件
(.json / .yaml)
外部脚本文件
JSON/YAML 测试用例
代码形态测试用例
\ No newline at end of file diff --git a/hrp/internal/convert/case2script/main.go b/hrp/internal/convert/case2script/main.go deleted file mode 100644 index bfc75b27..00000000 --- a/hrp/internal/convert/case2script/main.go +++ /dev/null @@ -1,120 +0,0 @@ -package case2script - -import ( - _ "embed" - "fmt" - "os" - - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/sdk" - "github.com/httprunner/httprunner/v4/hrp/internal/version" -) - -func Convert2TestScripts(destType string, paths ...string) error { - // report event - sdk.SendEvent(sdk.EventTracking{ - Category: "ConvertTests", - Action: fmt.Sprintf("hrp convert --%s", destType), - }) - - if destType == "gotest" { - return convert2GoTestScripts(paths...) - } else { - // default to pytest - return convert2PyTestScripts(paths...) - } -} - -func convert2PyTestScripts(paths ...string) error { - httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion) - python3, err := builtin.EnsurePython3Venv(httprunner) - if err != nil { - return err - } - - args := append([]string{"-m", "httprunner", "make"}, paths...) - return builtin.ExecCommand(python3, args...) -} - -func convert2GoTestScripts(paths ...string) error { - log.Warn().Msg("convert to gotest scripts is not supported yet") - os.Exit(1) - - // TODO - var testCasePaths []hrp.ITestCase - for _, path := range paths { - testCasePath := hrp.TestCasePath(path) - testCasePaths = append(testCasePaths, &testCasePath) - } - - testCases, err := hrp.LoadTestCases(testCasePaths...) - if err != nil { - log.Error().Err(err).Msg("failed to load testcases") - return err - } - - var pytestPaths []string - for _, testCase := range testCases { - tc := testCase.ToTCase() - converter := CaseConverter{ - TCase: tc, - } - pytestPath, err := converter.ToPyTest() - if err != nil { - log.Error().Err(err). - Str("originPath", tc.Config.Path). - Msg("convert to pytest failed") - continue - } - log.Info(). - Str("pytestPath", pytestPath). - Str("originPath", tc.Config.Path). - Msg("convert to pytest success") - pytestPaths = append(pytestPaths, pytestPath) - } - - // format pytest scripts with black - python3, err := builtin.EnsurePython3Venv("black") - if err != nil { - return err - } - args := append([]string{"-m", "black"}, pytestPaths...) - return builtin.ExecCommand(python3, args...) -} - -//go:embed testcase.tmpl -var testcaseTemplate string - -type CaseConverter struct { - *hrp.TCase -} - -func (c *CaseConverter) ToPyTest() (string, error) { - script := convertConfig(c.TCase.Config) - println(script) - return script, nil -} - -func (c *CaseConverter) ToGoTest() (string, error) { - return "", nil -} - -func convertConfig(config *hrp.TConfig) string { - script := fmt.Sprintf("Config('%s')", config.Name) - - if config.Variables != nil { - script += fmt.Sprintf(".variables(**{%v})", config.Variables) - } - if config.BaseURL != "" { - script += fmt.Sprintf(".base_url('%s')", config.BaseURL) - } - if config.Export != nil { - script += fmt.Sprintf(".export(*%v)", config.Export) - } - script += fmt.Sprintf(".verify(%v)", config.Verify) - - return script -} diff --git a/hrp/internal/convert/converter.go b/hrp/internal/convert/converter.go new file mode 100644 index 00000000..ac6831cc --- /dev/null +++ b/hrp/internal/convert/converter.go @@ -0,0 +1,374 @@ +package convert + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "reflect" + + "github.com/go-openapi/spec" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" +) + +const ( + suffixJSON = ".json" + suffixYAML = ".yaml" + suffixGoTest = ".go" + suffixPyTest = ".py" +) + +type InputType int + +const ( + InputTypeUnknown InputType = iota // default input type: unknown + InputTypeHAR + InputTypePostman + InputTypeSwagger + InputTypeJMeter + InputTypeJSON + InputTypeYAML + InputTypeGoTest + InputTypePyTest +) + +func (inputType InputType) String() string { + switch inputType { + case InputTypeHAR: + return "har" + case InputTypePostman: + return "postman" + case InputTypeSwagger: + return "swagger" + case InputTypeJMeter: + return "jmeter" + case InputTypeJSON: + return "json testcase" + case InputTypeYAML: + return "yaml testcase" + case InputTypeGoTest: + return "gotest script" + case InputTypePyTest: + return "pytest script" + default: + return "unknown" + } +} + +type OutputType int + +const ( + OutputTypeJSON OutputType = iota // default output type: JSON + OutputTypeYAML + OutputTypeGoTest + OutputTypePyTest +) + +func (outputType OutputType) String() string { + switch outputType { + case OutputTypeYAML: + return "yaml" + case OutputTypeGoTest: + return "gotest" + case OutputTypePyTest: + return "pytest" + default: + return "json" + } +} + +// TCaseConverter holds the common properties of case converter +type TCaseConverter struct { + InputPath string + OutputDir string + Profile *Profile + InputType InputType + OutputType OutputType + CaseHAR *CaseHar + CasePostman *CasePostman + CaseSwagger *spec.Swagger + TCase *hrp.TCase +} + +// Profile is used to override or update(create if not existed) original headers and cookies +type Profile struct { + Override bool `json:"override" yaml:"override"` + Headers map[string]string `json:"headers" yaml:"headers"` + Cookies map[string]string `json:"cookies" yaml:"cookies"` +} + +func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) { + tCaseConverter = &TCaseConverter{ + InputPath: path, + InputType: InputTypeUnknown, + } + extName := filepath.Ext(path) + if extName == "" { + log.Warn().Msg("extension name should be specified") + return + } + var err error + switch extName { + case ".har": + caseHAR := new(CaseHar) + err = builtin.LoadFile(path, caseHAR) + if err == nil && !reflect.DeepEqual(*caseHAR, CaseHar{}) { + tCaseConverter.InputType = InputTypeHAR + tCaseConverter.CaseHAR = caseHAR + } + case ".json": + tCase := new(hrp.TCase) + err = builtin.LoadFile(path, tCase) + if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) { + tCaseConverter.InputType = InputTypeJSON + tCaseConverter.TCase = tCase + break + } + casePostman := new(CasePostman) + err = builtin.LoadFile(path, casePostman) + if err == nil && !reflect.DeepEqual(*casePostman, CasePostman{}) { + tCaseConverter.InputType = InputTypePostman + tCaseConverter.CasePostman = casePostman + break + } + caseSwagger := new(spec.Swagger) + err = builtin.LoadFile(path, caseSwagger) + if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) { + tCaseConverter.InputType = InputTypeSwagger + tCaseConverter.CaseSwagger = caseSwagger + } + case ".yaml", ".yml": + tCase := new(hrp.TCase) + err = builtin.LoadFile(path, tCase) + if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) { + tCaseConverter.InputType = InputTypeYAML + tCaseConverter.TCase = tCase + break + } + caseSwagger := new(spec.Swagger) + err = builtin.LoadFile(path, caseSwagger) + if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) { + tCaseConverter.InputType = InputTypeSwagger + tCaseConverter.CaseSwagger = caseSwagger + } + case ".go": // TODO + tCaseConverter.InputType = InputTypeGoTest + case ".py": // TODO + tCaseConverter.InputType = InputTypePyTest + case ".jmx": // TODO + tCaseConverter.InputType = InputTypeJMeter + default: + log.Warn(). + Str("input path", tCaseConverter.InputPath). + Msgf("unsupported file type: %v", extName) + } + if tCaseConverter.InputType != InputTypeUnknown { + log.Info(). + Str("input path", tCaseConverter.InputPath). + Msgf("load case as: %s", tCaseConverter.InputType.String()) + } else { + log.Error().Err(err). + Str("input path", tCaseConverter.InputPath). + Msgf("failed to load case") + } + return +} + +func (c *TCaseConverter) SetProfile(path string) { + log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile") + profile := new(Profile) + err := builtin.LoadFile(path, profile) + if err != nil { + log.Warn().Str("path", path). + Msg("failed to load profile, ignore!") + return + } + c.Profile = profile +} + +func (c *TCaseConverter) SetOutputDir(dir string) { + log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory") + c.OutputDir = dir +} + +func (c *TCaseConverter) genOutputPath(suffix string) string { + outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix + if c.OutputDir != "" { + return filepath.Join(c.OutputDir, outFileFullName) + } else { + return filepath.Join(filepath.Dir(c.InputPath), outFileFullName) + } + // TODO avoid outFileFullName conflict? +} + +func (c *TCaseConverter) ToPyTest() (string, error) { + script := convertConfig(c.TCase.Config) + println(script) + return script, nil +} + +func convertConfig(config *hrp.TConfig) string { + script := fmt.Sprintf("Config('%s')", config.Name) + + if config.Variables != nil { + script += fmt.Sprintf(".variables(**{%v})", config.Variables) + } + if config.BaseURL != "" { + script += fmt.Sprintf(".base_url('%s')", config.BaseURL) + } + if config.Export != nil { + script += fmt.Sprintf(".export(*%v)", config.Export) + } + script += fmt.Sprintf(".verify(%v)", config.Verify) + + return script +} + +func (c *TCaseConverter) ToGoTest() (string, error) { + return "", nil +} + +// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format +type ICaseConverter interface { + Struct() *TCaseConverter + ToJSON() (string, error) + ToJSONTemp() (string, error) + ToYAML() (string, error) + ToGoTest() (string, error) + ToPyTest() (string, error) +} + +func LoadConverters(outputType OutputType, outputDir, profilePath string, args []string) []ICaseConverter { + // report event + sdk.SendEvent(sdk.EventTracking{ + Category: "ConvertTests", + Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()), + }) + + var iCaseConverters []ICaseConverter + for _, arg := range args { + tCaseConverter := NewTCaseConverter(arg) + tCaseConverter.OutputType = outputType + if outputDir != "" { + tCaseConverter.SetOutputDir(outputDir) + } + if profilePath != "" { + tCaseConverter.SetProfile(profilePath) + } + switch tCaseConverter.InputType { + case InputTypeHAR: + iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter)) + case InputTypePostman: + iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter)) + case InputTypeJSON: + iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter)) + case InputTypeYAML: + iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter)) + case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest: + log.Warn(). + Str("input path", tCaseConverter.InputPath). + Msg("case type not supported yet, ignore!") + default: + log.Warn(). + Str("input path", tCaseConverter.InputPath). + Msg("unknown case type, ignore!") + } + } + return iCaseConverters +} + +func Run(iCaseConverters []ICaseConverter) { + var outputFiles []string + var err error + for _, iCaseConverter := range iCaseConverters { + log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting") + var outputFile string + switch iCaseConverter.Struct().OutputType { + case OutputTypeYAML: + outputFile, err = iCaseConverter.ToYAML() + case OutputTypeGoTest: + outputFile, err = iCaseConverter.ToGoTest() + case OutputTypePyTest: + outputFile, err = iCaseConverter.ToPyTest() + default: + outputFile, err = iCaseConverter.ToJSON() + } + if err != nil { + log.Error().Err(err). + Str("input path", iCaseConverter.Struct().InputPath). + Msg("error occurs during converting") + continue + } + outputFiles = append(outputFiles, outputFile) + } + log.Info().Strs("output files", outputFiles).Msg("conversion completed") +} + +func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) { + tCase := iCaseConverter.Struct().TCase + if tCase == nil { + return nil, errors.Errorf("empty json/yaml testcase occurs") + } + profile := iCaseConverter.Struct().Profile + if profile == nil { + return tCase, nil + } + for _, step := range tCase.TestSteps { + // override original headers and cookies + if profile.Override { + step.Request.Headers = make(map[string]string) + step.Request.Cookies = make(map[string]string) + } + // update (create if not existed) original headers and cookies + if step.Request.Headers == nil { + step.Request.Headers = make(map[string]string) + } + if step.Request.Cookies == nil { + step.Request.Cookies = make(map[string]string) + } + for k, v := range profile.Headers { + step.Request.Headers[k] = v + } + for k, v := range profile.Cookies { + step.Request.Cookies[k] = v + } + } + return tCase, nil +} + +func convertToPyTest(iCaseConverter ICaseConverter) (string, error) { + // convert to temporary json testcase compatible with python engine style + jsonPath, err := iCaseConverter.ToJSONTemp() + inputType := iCaseConverter.Struct().InputType + if err != nil { + return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String()) + } + defer func() { + if jsonPath != "" { + if err = os.Remove(jsonPath); err != nil { + log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String()) + } + } + }() + + // convert from temporary json testcase to pytest + converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath)) + pyTestPath, err := converterJSON.MakePyTestScript() + if err != nil { + return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ") + } + + // rename resultant pytest + renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest) + err = os.Rename(pyTestPath, renamedPyTestPath) + if err != nil { + log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file") + return pyTestPath, nil + } + return renamedPyTestPath, nil +} diff --git a/hrp/internal/convert/converter_gotest.go b/hrp/internal/convert/converter_gotest.go new file mode 100644 index 00000000..863da231 --- /dev/null +++ b/hrp/internal/convert/converter_gotest.go @@ -0,0 +1,60 @@ +package convert + +import ( + _ "embed" + "os" + + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +func convert2GoTestScripts(paths ...string) error { + log.Warn().Msg("convert to gotest scripts is not supported yet") + os.Exit(1) + + // TODO + var testCasePaths []hrp.ITestCase + for _, path := range paths { + testCasePath := hrp.TestCasePath(path) + testCasePaths = append(testCasePaths, &testCasePath) + } + + testCases, err := hrp.LoadTestCases(testCasePaths...) + if err != nil { + log.Error().Err(err).Msg("failed to load testcases") + return err + } + + var pytestPaths []string + for _, testCase := range testCases { + tc := testCase.ToTCase() + converter := TCaseConverter{ + TCase: tc, + } + pytestPath, err := converter.ToPyTest() + if err != nil { + log.Error().Err(err). + Str("originPath", tc.Config.Path). + Msg("convert to pytest failed") + continue + } + log.Info(). + Str("pytestPath", pytestPath). + Str("originPath", tc.Config.Path). + Msg("convert to pytest success") + pytestPaths = append(pytestPaths, pytestPath) + } + + // format pytest scripts with black + python3, err := builtin.EnsurePython3Venv("black") + if err != nil { + return err + } + args := append([]string{"-m", "black"}, pytestPaths...) + return builtin.ExecCommand(python3, args...) +} + +//go:embed testcase.tmpl +var testcaseTemplate string diff --git a/hrp/internal/convert/converter_har.go b/hrp/internal/convert/converter_har.go new file mode 100644 index 00000000..d34717c9 --- /dev/null +++ b/hrp/internal/convert/converter_har.go @@ -0,0 +1,716 @@ +package convert + +import ( + "encoding/base64" + "fmt" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "net/url" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +// ==================== model definition starts here ==================== + +/* +HTTP Archive (HAR) format +https://w3c.github.io/web-performance/specs/HAR/Overview.html +this file is copied from https://github.com/mrichman/hargo/blob/master/types.go +*/ + +// CaseHar is a container type for deserialization +type CaseHar struct { + Log Log `json:"log"` +} + +// Log represents the root of the exported data. This object MUST be present and its name MUST be "log". +type Log struct { + // The object contains the following name/value pairs: + + // Required. Version number of the format. + Version string `json:"version"` + // Required. An object of type creator that contains the name and version + // information of the log creator application. + Creator Creator `json:"creator"` + // Optional. An object of type browser that contains the name and version + // information of the user agent. + Browser Browser `json:"browser"` + // Optional. An array of objects of type page, each representing one exported + // (tracked) page. Leave out this field if the application does not support + // grouping by pages. + Pages []Page `json:"pages,omitempty"` + // Required. An array of objects of type entry, each representing one + // exported (tracked) HTTP request. + Entries []Entry `json:"entries"` + // Optional. A comment provided by the user or the application. Sorting + // entries by startedDateTime (starting from the oldest) is preferred way how + // to export data since it can make importing faster. However the reader + // application should always make sure the array is sorted (if required for + // the import). + Comment string `json:"comment"` +} + +// Creator contains information about the log creator application +type Creator struct { + // Required. The name of the application that created the log. + Name string `json:"name"` + // Required. The version number of the application that created the log. + Version string `json:"version"` + // Optional. A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// Browser that created the log +type Browser struct { + // Required. The name of the browser that created the log. + Name string `json:"name"` + // Required. The version number of the browser that created the log. + Version string `json:"version"` + // Optional. A comment provided by the user or the browser. + Comment string `json:"comment"` +} + +// Page object for every exported web page and one object for every HTTP request. +// In case when an HTTP trace tool isn't able to group requests by a page, +// the object is empty and individual requests doesn't have a parent page. +type Page struct { + /* There is one object for every exported web page and one + object for every HTTP request. In case when an HTTP trace tool isn't able to + group requests by a page, the object is empty and individual + requests doesn't have a parent page. + */ + + // Date and time stamp for the beginning of the page load + // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00). + StartedDateTime string `json:"startedDateTime"` + // Unique identifier of a page within the . Entries use it to refer the parent page. + ID string `json:"id"` + // Page title. + Title string `json:"title"` + // Detailed timing info about page load. + PageTiming PageTiming `json:"pageTiming"` + // (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// PageTiming describes timings for various events (states) fired during the page load. +// All times are specified in milliseconds. If a time info is not available appropriate field is set to -1. +type PageTiming struct { + // Content of the page loaded. Number of milliseconds since page load started + // (page.startedDateTime). Use -1 if the timing does not apply to the current + // request. + // Depeding on the browser, onContentLoad property represents DOMContentLoad + // event or document.readyState == interactive. + OnContentLoad int `json:"onContentLoad"` + // Page is loaded (onLoad event fired). Number of milliseconds since page + // load started (page.startedDateTime). Use -1 if the timing does not apply + // to the current request. + OnLoad int `json:"onLoad"` + // (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment"` +} + +// Entry is a unique, optional Reference to the parent page. +// Leave out this field if the application does not support grouping by pages. +type Entry struct { + Pageref string `json:"pageref,omitempty"` + // Date and time stamp of the request start + // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD). + StartedDateTime string `json:"startedDateTime"` + // Total elapsed time of the request in milliseconds. This is the sum of all + // timings available in the timings object (i.e. not including -1 values) . + Time float32 `json:"time"` + // Detailed info about the request. + Request Request `json:"request"` + // Detailed info about the response. + Response Response `json:"response"` + // Info about cache usage. + Cache Cache `json:"cache"` + // Detailed timing info about request/response round trip. + PageTimings PageTimings `json:"pageTimings"` + // optional (new in 1.2) IP address of the server that was connected + // (result of DNS resolution). + ServerIPAddress string `json:"serverIPAddress,omitempty"` + // optional (new in 1.2) Unique ID of the parent TCP/IP connection, can be + // the client port number. Note that a port number doesn't have to be unique + // identifier in cases where the port is shared for more connections. If the + // port isn't available for the application, any other unique connection ID + // can be used instead (e.g. connection index). Leave out this field if the + // application doesn't support this info. + Connection string `json:"connection,omitempty"` + // (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// Request contains detailed info about performed request. +type Request struct { + // Request method (GET, POST, ...). + Method string `json:"method"` + // Absolute URL of the request (fragments are not included). + URL string `json:"url"` + // Request HTTP Version. + HTTPVersion string `json:"httpVersion"` + // List of cookie objects. + Cookies []Cookie `json:"cookies"` + // List of header objects. + Headers []NVP `json:"headers"` + // List of query parameter objects. + QueryString []NVP `json:"queryString"` + // Posted data. + PostData PostData `json:"postData"` + // Total number of bytes from the start of the HTTP request message until + // (and including) the double CRLF before the body. Set to -1 if the info + // is not available. + HeaderSize int `json:"headerSize"` + // Size of the request body (POST data payload) in bytes. Set to -1 if the + // info is not available. + BodySize int `json:"bodySize"` + // (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment"` +} + +// Response contains detailed info about the response. +type Response struct { + // Response status. + Status int `json:"status"` + // Response status description. + StatusText string `json:"statusText"` + // Response HTTP Version. + HTTPVersion string `json:"httpVersion"` + // List of cookie objects. + Cookies []Cookie `json:"cookies"` + // List of header objects. + Headers []NVP `json:"headers"` + // Details about the response body. + Content Content `json:"content"` + // Redirection target URL from the Location response header. + RedirectURL string `json:"redirectURL"` + // Total number of bytes from the start of the HTTP response message until + // (and including) the double CRLF before the body. Set to -1 if the info is + // not available. + // The size of received response-headers is computed only from headers that + // are really received from the server. Additional headers appended by the + // browser are not included in this number, but they appear in the list of + // header objects. + HeadersSize int `json:"headersSize"` + // Size of the received response body in bytes. Set to zero in case of + // responses coming from the cache (304). Set to -1 if the info is not + // available. + BodySize int `json:"bodySize"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// Cookie contains list of all cookies (used in and objects). +type Cookie struct { + // The name of the cookie. + Name string `json:"name"` + // The cookie value. + Value string `json:"value"` + // optional The path pertaining to the cookie. + Path string `json:"path,omitempty"` + // optional The host of the cookie. + Domain string `json:"domain,omitempty"` + // optional Cookie expiration time. + // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.123+02:00). + Expires string `json:"expires,omitempty"` + // optional Set to true if the cookie is HTTP only, false otherwise. + HTTPOnly bool `json:"httpOnly,omitempty"` + // optional (new in 1.2) True if the cookie was transmitted over ssl, false + // otherwise. + Secure bool `json:"secure,omitempty"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment bool `json:"comment,omitempty"` +} + +// NVP is simply a name/value pair with a comment +type NVP struct { + Name string `json:"name"` + Value string `json:"value"` + Comment string `json:"comment,omitempty"` +} + +// PostData describes posted data, if any (embedded in object). +type PostData struct { + // Mime type of posted data. + MimeType string `json:"mimeType"` + // List of posted parameters (in case of URL encoded parameters). + Params []PostParam `json:"params"` + // Plain text posted data + Text string `json:"text"` + // optional (new in 1.2) A comment provided by the user or the + // application. + Comment string `json:"comment,omitempty"` +} + +// PostParam is a list of posted parameters, if any (embedded in object). +type PostParam struct { + // name of a posted parameter. + Name string `json:"name"` + // optional value of a posted parameter or content of a posted file. + Value string `json:"value,omitempty"` + // optional name of a posted file. + FileName string `json:"fileName,omitempty"` + // optional content type of a posted file. + ContentType string `json:"contentType,omitempty"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// Content describes details about response content (embedded in object). +type Content struct { + // Length of the returned content in bytes. Should be equal to + // response.bodySize if there is no compression and bigger when the content + // has been compressed. + Size int `json:"size"` + // optional Number of bytes saved. Leave out this field if the information + // is not available. + Compression int `json:"compression,omitempty"` + // MIME type of the response text (value of the Content-Type response + // header). The charset attribute of the MIME type is included (if + // available). + MimeType string `json:"mimeType"` + // optional Response body sent from the server or loaded from the browser + // cache. This field is populated with textual content only. The text field + // is either HTTP decoded text or a encoded (e.g. "base64") representation of + // the response body. Leave out this field if the information is not + // available. + Text string `json:"text,omitempty"` + // optional (new in 1.2) Encoding used for response text field e.g + // "base64". Leave out this field if the text field is HTTP decoded + // (decompressed & unchunked), than trans-coded from its original character + // set into UTF-8. + Encoding string `json:"encoding,omitempty"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// Cache contains info about a request coming from browser cache. +type Cache struct { + // optional State of a cache entry before the request. Leave out this field + // if the information is not available. + BeforeRequest CacheObject `json:"beforeRequest,omitempty"` + // optional State of a cache entry after the request. Leave out this field if + // the information is not available. + AfterRequest CacheObject `json:"afterRequest,omitempty"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// CacheObject is used by both beforeRequest and afterRequest +type CacheObject struct { + // optional - Expiration time of the cache entry. + Expires string `json:"expires,omitempty"` + // The last time the cache entry was opened. + LastAccess string `json:"lastAccess"` + // Etag + ETag string `json:"eTag"` + // The number of times the cache entry has been opened. + HitCount int `json:"hitCount"` + // optional (new in 1.2) A comment provided by the user or the application. + Comment string `json:"comment,omitempty"` +} + +// PageTimings describes various phases within request-response round trip. +// All times are specified in milliseconds. +type PageTimings struct { + Blocked int `json:"blocked,omitempty"` + // optional - Time spent in a queue waiting for a network connection. Use -1 + // if the timing does not apply to the current request. + DNS int `json:"dns,omitempty"` + // optional - DNS resolution time. The time required to resolve a host name. + // Use -1 if the timing does not apply to the current request. + Connect int `json:"connect,omitempty"` + // optional - Time required to create TCP connection. Use -1 if the timing + // does not apply to the current request. + Send int `json:"send"` + // Time required to send HTTP request to the server. + Wait int `json:"wait"` + // Waiting for a response from the server. + Receive int `json:"receive"` + // Time required to read entire response from the server (or cache). + Ssl int `json:"ssl,omitempty"` + // optional (new in 1.2) - Time required for SSL/TLS negotiation. If this + // field is defined then the time is also included in the connect field (to + // ensure backward compatibility with HAR 1.1). Use -1 if the timing does not + // apply to the current request. + Comment string `json:"comment,omitempty"` + // optional (new in 1.2) - A comment provided by the user or the application. +} + +// TestResult contains results for an individual HTTP request +type TestResult struct { + URL string `json:"url"` + Status int `json:"status"` // 200, 500, etc. + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Latency int `json:"latency"` // milliseconds + Method string `json:"method"` + HarFile string `json:"harfile"` +} + +// ==================== model definition ends here ==================== + +func NewConverterHAR(converter *TCaseConverter) *ConverterHAR { + return &ConverterHAR{ + converter: converter, + } +} + +type ConverterHAR struct { + converter *TCaseConverter +} + +func (c *ConverterHAR) Struct() *TCaseConverter { + return c.converter +} + +func (c *ConverterHAR) ToJSON() (string, error) { + tCase, err := c.makeTestCase() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(tCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterHAR) ToJSONTemp() (string, error) { + tCase, err := c.makeTestCaseTemp() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(tCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterHAR) ToYAML() (string, error) { + tCase, err := c.makeTestCase() + if err != nil { + return "", err + } + yamlPath := c.converter.genOutputPath(suffixYAML) + err = builtin.Dump2YAML(tCase, yamlPath) + if err != nil { + return "", err + } + return yamlPath, nil +} + +func (c *ConverterHAR) ToGoTest() (string, error) { + //TODO implement me + return "", errors.New("convert from har to gotest scripts is not supported yet") +} + +func (c *ConverterHAR) ToPyTest() (string, error) { + return convertToPyTest(c) +} + +func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) { + teststeps, err := c.prepareTestSteps() + if err != nil { + return nil, err + } + + tCase := &hrp.TCase{ + Config: c.prepareConfig(), + TestSteps: teststeps, + } + err = tCase.MakeCompat2GoEngine() + if err != nil { + return nil, err + } + return tCase, nil +} + +func (c *ConverterHAR) makeTestCaseTemp() (*hrp.TCase, error) { + teststeps, err := c.prepareTestSteps() + if err != nil { + return nil, err + } + + tCase := &hrp.TCase{ + Config: c.prepareConfig(), + TestSteps: teststeps, + } + err = tCase.MakeCompat2PyEngine() + if err != nil { + return nil, err + } + return tCase, nil +} + +func (c *ConverterHAR) load() (*CaseHar, error) { + har := c.converter.CaseHAR + if har == nil { + return nil, errors.New("empty har case occurs") + } + return har, nil +} + +func (c *ConverterHAR) prepareConfig() *hrp.TConfig { + return hrp.NewConfig("testcase description"). + SetVerifySSL(false) +} + +func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) { + har, err := c.load() + if err != nil { + return nil, err + } + + var steps []*hrp.TStep + for _, entry := range har.Log.Entries { + step, err := c.prepareTestStep(&entry) + if err != nil { + return nil, err + } + steps = append(steps, step) + } + + return steps, nil +} + +func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) { + log.Info(). + Str("method", entry.Request.Method). + Str("url", entry.Request.URL). + Msg("convert teststep") + + step := &stepFromHAR{ + TStep: hrp.TStep{ + Request: &hrp.Request{}, + Validators: make([]interface{}, 0), + }, + profile: c.converter.Profile, + } + if err := step.makeRequestMethod(entry); err != nil { + return nil, err + } + if err := step.makeRequestURL(entry); err != nil { + return nil, err + } + if err := step.makeRequestParams(entry); err != nil { + return nil, err + } + if err := step.makeRequestCookies(entry); err != nil { + return nil, err + } + if err := step.makeRequestHeaders(entry); err != nil { + return nil, err + } + if err := step.makeRequestBody(entry); err != nil { + return nil, err + } + if err := step.makeValidate(entry); err != nil { + return nil, err + } + return &step.TStep, nil +} + +type stepFromHAR struct { + hrp.TStep + profile *Profile +} + +func (s *stepFromHAR) makeRequestMethod(entry *Entry) error { + s.Request.Method = hrp.HTTPMethod(entry.Request.Method) + return nil +} + +func (s *stepFromHAR) makeRequestURL(entry *Entry) error { + u, err := url.Parse(entry.Request.URL) + if err != nil { + log.Error().Err(err).Msg("make request url failed") + return err + } + s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path) + return nil +} + +func (s *stepFromHAR) makeRequestParams(entry *Entry) error { + s.Request.Params = make(map[string]interface{}) + for _, param := range entry.Request.QueryString { + s.Request.Params[param.Name] = param.Value + } + return nil +} + +func (s *stepFromHAR) makeRequestCookies(entry *Entry) error { + // use cookies from har + s.Request.Cookies = make(map[string]string) + for _, cookie := range entry.Request.Cookies { + s.Request.Cookies[cookie.Name] = cookie.Value + } + + if s.profile == nil { + return nil + } + // override all cookies according to the profile + if s.profile.Override { + s.Request.Cookies = make(map[string]string) + } + // create or update the cookies according to the profile + for k, v := range s.profile.Cookies { + s.Request.Cookies[k] = v + } + return nil +} + +func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error { + // use headers from har + s.Request.Headers = make(map[string]string) + for _, header := range entry.Request.Headers { + if strings.EqualFold(header.Name, "cookie") { + continue + } + s.Request.Headers[header.Name] = header.Value + } + + if s.profile == nil { + return nil + } + // override all headers according to the profile + if s.profile.Override { + s.Request.Headers = make(map[string]string) + } + // create or update the headers according to the profile + for k, v := range s.profile.Headers { + s.Request.Headers[k] = v + } + return nil +} + +func (s *stepFromHAR) makeRequestBody(entry *Entry) error { + mimeType := entry.Request.PostData.MimeType + if mimeType == "" { + // GET/HEAD/DELETE without body + return nil + } + + // POST/PUT with body + if strings.HasPrefix(mimeType, "application/json") { + // post json + var body interface{} + if entry.Request.PostData.Text == "" { + body = nil + } else { + err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body) + if err != nil { + log.Error().Err(err).Msg("make request body failed") + return err + } + } + s.Request.Body = body + } else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") { + // post form + var paramsList []string + for _, param := range entry.Request.PostData.Params { + paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value)) + } + s.Request.Body = strings.Join(paramsList, "&") + } else if strings.HasPrefix(mimeType, "text/plain") { + // post raw data + s.Request.Body = entry.Request.PostData.Text + } else { + // TODO + log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType) + } + return nil +} + +func (s *stepFromHAR) makeValidate(entry *Entry) error { + // make validator for response status code + s.Validators = append(s.Validators, hrp.Validator{ + Check: "status_code", + Assert: "equals", + Expect: entry.Response.Status, + Message: "assert response status code", + }) + + // make validators for response headers + for _, header := range entry.Response.Headers { + // assert Content-Type + if strings.EqualFold(header.Name, "Content-Type") { + s.Validators = append(s.Validators, hrp.Validator{ + Check: "headers.\"Content-Type\"", + Assert: "equals", + Expect: header.Value, + Message: "assert response header Content-Type", + }) + } + } + + // make validators for response body + respBody := entry.Response.Content + if respBody.Text == "" { + // response body is empty + return nil + } + if strings.HasPrefix(respBody.MimeType, "application/json") { + var data []byte + var err error + // response body is json + if respBody.Encoding == "base64" { + // decode base64 text + data, err = base64.StdEncoding.DecodeString(respBody.Text) + if err != nil { + return errors.Wrap(err, "decode base64 error") + } + } else if respBody.Encoding == "" { + // no encoding + data = []byte(respBody.Text) + } else { + // other encoding type + return nil + } + // convert to json + var body interface{} + if err = json.Unmarshal(data, &body); err != nil { + return errors.Wrap(err, "json.Unmarshal body error") + } + jsonBody, ok := body.(map[string]interface{}) + if !ok { + return fmt.Errorf("response body is not json, not matched with MimeType") + } + + // response body is json + keys := make([]string, 0, len(jsonBody)) + for k := range jsonBody { + keys = append(keys, k) + } + // sort map keys to keep validators in stable order + sort.Strings(keys) + for _, key := range keys { + value := jsonBody[key] + switch v := value.(type) { + case map[string]interface{}: + continue + case []interface{}: + continue + default: + s.Validators = append(s.Validators, hrp.Validator{ + Check: fmt.Sprintf("body.%s", key), + Assert: "equals", + Expect: v, + Message: fmt.Sprintf("assert response body %s", key), + }) + } + } + } + + return nil +} diff --git a/hrp/internal/convert/converter_har_test.go b/hrp/internal/convert/converter_har_test.go new file mode 100644 index 00000000..0d4daa11 --- /dev/null +++ b/hrp/internal/convert/converter_har_test.go @@ -0,0 +1,373 @@ +package convert + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/httprunner/httprunner/v4/hrp" +) + +var ( + harPath = "../../../examples/data/har/demo.har" + harPath2 = "../../../examples/data/har/postman-echo.har" + harProfileOverridePath = "../../../examples/data/har/profile_override.yml" +) + +var converterHAR = NewConverterHAR(NewTCaseConverter(harPath)) +var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2)) + +func TestHAR2JSON(t *testing.T) { + jsonPath, err := converterHAR.ToJSON() + if !assert.NoError(t, err) { + t.Fatal() + } + if !assert.NotEmpty(t, jsonPath) { + t.Fatal() + } +} + +func TestHAR2YAML(t *testing.T) { + yamlPath, err := converterHAR2.ToYAML() + if !assert.NoError(t, err) { + t.Fatal() + } + if !assert.NotEmpty(t, yamlPath) { + t.Fatal() + } +} + +func TestLoadHAR(t *testing.T) { + h, err := converterHAR.load() + if !assert.NoError(t, err) { + t.Fatal() + } + if !assert.Equal(t, "GET", h.Log.Entries[0].Request.Method) { + t.Fatal() + } + if !assert.Equal(t, "POST", h.Log.Entries[1].Request.Method) { + t.Fatal() + } +} + +func TestLoadHARWithProfile(t *testing.T) { + tCaseConverter := NewTCaseConverter(harPath) + tCaseConverter.SetProfile(harProfileOverridePath) + h := NewConverterHAR(tCaseConverter) + _, err := h.load() + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, + map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + h.converter.Profile.Headers) { + t.Fatal() + } + if !assert.Equal(t, + map[string]string{"UserName": "debugtalk"}, + h.converter.Profile.Cookies) { + t.Fatal() + } +} + +func TestMakeTestCaseFromHAR(t *testing.T) { + tCase, err := converterHAR.makeTestCase() + if !assert.NoError(t, err) { + t.Fatal() + } + + // make request method + if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) { + t.Fatal() + } + if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) { + t.Fatal() + } + + // make request url + if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) { + t.Fatal() + } + if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) { + t.Fatal() + } + + // make request params + if !assert.Equal(t, "HDnY8", tCase.TestSteps[0].Request.Params["foo1"]) { + t.Fatal() + } + + // make request cookies + if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Cookies["sails.sid"]) { + t.Fatal() + } + + // make request headers + if !assert.Equal(t, "HttpRunnerPlus", tCase.TestSteps[0].Request.Headers["User-Agent"]) { + t.Fatal() + } + if !assert.Equal(t, "postman-echo.com", tCase.TestSteps[0].Request.Headers["Host"]) { + t.Fatal() + } + + // make request data + if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) { + t.Fatal() + } + + // make validators + validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator) + if !ok || !assert.Equal(t, "status_code", validator.Check) { + t.Fatal() + } + validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator) + if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) { + t.Fatal() + } + validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator) + if !ok || !assert.Equal(t, "body.url", validator.Check) { + t.Fatal() + } +} + +func TestMakeRequestURL(t *testing.T) { + entry := &Entry{ + Request: Request{ + URL: "http://127.0.0.1:8080/api/login", + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, "http://127.0.0.1:8080/api/login", step.Request.URL) { + t.Fatal() + } +} + +func TestMakeRequestHeaders(t *testing.T) { + entry := &Entry{ + Request: Request{ + Method: "POST", + Headers: []NVP{ + {Name: "Content-Type", Value: "application/json; charset=utf-8"}, + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, map[string]string{ + "Content-Type": "application/json; charset=utf-8", + }, step.Request.Headers) { + t.Fatal() + } +} + +func TestMakeRequestHeadersWithProfileOverride(t *testing.T) { + tCaseConverter := NewTCaseConverter(harPath) + tCaseConverter.SetProfile(harProfileOverridePath) + h := NewConverterHAR(tCaseConverter) + entry := &Entry{ + Request: Request{ + Method: "POST", + Headers: []NVP{ + {Name: "Content-Type", Value: "application/json; charset=utf-8"}, + }, + }, + } + step, err := h.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, step.Request.Headers) { + t.Fatal() + } +} + +func TestMakeRequestCookies(t *testing.T) { + entry := &Entry{ + Request: Request{ + Method: "POST", + Cookies: []Cookie{ + {Name: "abc", Value: "123"}, + {Name: "UserName", Value: "leolee"}, + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, map[string]string{ + "abc": "123", + "UserName": "leolee", + }, step.Request.Cookies) { + t.Fatal() + } +} + +func TestMakeRequestCookiesWithProfileOverride(t *testing.T) { + tCaseConverter := NewTCaseConverter(harPath) + tCaseConverter.SetProfile(harProfileOverridePath) + h := NewConverterHAR(tCaseConverter) + entry := &Entry{ + Request: Request{ + Method: "POST", + Cookies: []Cookie{ + {Name: "abc", Value: "123"}, + {Name: "UserName", Value: "leolee"}, + }, + }, + } + step, err := h.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, map[string]string{ + "UserName": "debugtalk", + }, step.Request.Cookies) { + t.Fatal() + } +} + +func TestMakeRequestDataParams(t *testing.T) { + entry := &Entry{ + Request: Request{ + Method: "POST", + PostData: PostData{ + MimeType: "application/x-www-form-urlencoded; charset=utf-8", + Params: []PostParam{ + {Name: "a", Value: "1"}, + {Name: "b", Value: "2"}, + }, + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, "a=1&b=2", step.Request.Body) { + t.Fatal() + } +} + +func TestMakeRequestDataJSON(t *testing.T) { + entry := &Entry{ + Request: Request{ + Method: "POST", + PostData: PostData{ + MimeType: "application/json; charset=utf-8", + Text: "{\"a\":\"1\",\"b\":\"2\"}", + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, map[string]interface{}{"a": "1", "b": "2"}, step.Request.Body) { + t.Fatal() + } +} + +func TestMakeRequestDataTextEmpty(t *testing.T) { + entry := &Entry{ + Request: Request{ + Method: "POST", + PostData: PostData{ + MimeType: "application/json; charset=utf-8", + Text: "", + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + + if !assert.Equal(t, nil, step.Request.Body) { // TODO + t.Fatal() + } +} + +func TestMakeValidate(t *testing.T) { + entry := &Entry{ + Response: Response{ + Status: 200, + Headers: []NVP{ + {Name: "Content-Type", Value: "application/json; charset=utf-8"}, + }, + Content: Content{ + Size: 71, + MimeType: "application/json; charset=utf-8", + // map[Code:200 IsSuccess:true Message: Value:map[BlnResult:true]] + Text: "eyJJc1N1Y2Nlc3MiOnRydWUsIkNvZGUiOjIwMCwiTWVzc2FnZSI6bnVsbCwiVmFsdWUiOnsiQmxuUmVzdWx0Ijp0cnVlfX0=", + Encoding: "base64", + }, + }, + } + step, err := converterHAR.prepareTestStep(entry) + if !assert.NoError(t, err) { + t.Fatal() + } + validator, ok := step.Validators[0].(hrp.Validator) + if !ok { + t.Fatal() + } + if !assert.Equal(t, validator, + hrp.Validator{ + Check: "status_code", + Expect: 200, + Assert: "equals", + Message: "assert response status code", + }) { + t.Fatal() + } + + validator, ok = step.Validators[1].(hrp.Validator) + if !ok { + t.Fatal() + } + if !assert.Equal(t, validator, + hrp.Validator{ + Check: "headers.\"Content-Type\"", + Expect: "application/json; charset=utf-8", + Assert: "equals", + Message: "assert response header Content-Type", + }) { + t.Fatal() + } + + validator, ok = step.Validators[2].(hrp.Validator) + if !ok { + t.Fatal() + } + if !assert.Equal(t, validator, + hrp.Validator{ + Check: "body.Code", + Expect: float64(200), // TODO + Assert: "equals", + Message: "assert response body Code", + }) { + t.Fatal() + } +} diff --git a/hrp/internal/convert/converter_json.go b/hrp/internal/convert/converter_json.go new file mode 100644 index 00000000..5aa0b69a --- /dev/null +++ b/hrp/internal/convert/converter_json.go @@ -0,0 +1,111 @@ +package convert + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/version" +) + +func NewConverterJSON(converter *TCaseConverter) *ConverterJSON { + return &ConverterJSON{ + converter: converter, + } +} + +type ConverterJSON struct { + converter *TCaseConverter +} + +func (c *ConverterJSON) Struct() *TCaseConverter { + return c.converter +} + +func (c *ConverterJSON) ToJSON() (string, error) { + testCase, err := c.makeTestCase() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(testCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterJSON) ToJSONTemp() (string, error) { + testCase, err := c.makeTestCaseTemp() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(testCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterJSON) ToYAML() (string, error) { + testCase, err := c.makeTestCase() + if err != nil { + return "", err + } + yamlPath := c.converter.genOutputPath(suffixYAML) + err = builtin.Dump2YAML(testCase, yamlPath) + if err != nil { + return "", err + } + return yamlPath, nil +} + +func (c *ConverterJSON) ToGoTest() (string, error) { + //TODO implement me + return "", errors.New("convert from json testcase to gotest scripts is not supported yet") +} + +func (c *ConverterJSON) ToPyTest() (string, error) { + return convertToPyTest(c) +} + +func (c *ConverterJSON) MakePyTestScript() (string, error) { + httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion) + python3, err := builtin.EnsurePython3Venv(httprunner) + if err != nil { + return "", err + } + args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath) + err = builtin.ExecCommand(python3, args...) + if err != nil { + return "", err + } + return c.converter.genOutputPath(suffixPyTest), nil +} + +func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) { + tCase, err := makeTestCaseFromJSONYAML(c) + if err != nil { + return nil, err + } + err = tCase.MakeCompat2GoEngine() + if err != nil { + return nil, err + } + return tCase, nil +} + +func (c *ConverterJSON) makeTestCaseTemp() (*hrp.TCase, error) { + tCase, err := makeTestCaseFromJSONYAML(c) + if err != nil { + return nil, err + } + err = tCase.MakeCompat2PyEngine() + if err != nil { + return nil, err + } + return tCase, nil +} diff --git a/hrp/internal/convert/postman2case/core.go b/hrp/internal/convert/converter_postman.go similarity index 52% rename from hrp/internal/convert/postman2case/core.go rename to hrp/internal/convert/converter_postman.go index 1f15cbf5..d373c23a 100644 --- a/hrp/internal/convert/postman2case/core.go +++ b/hrp/internal/convert/converter_postman.go @@ -1,4 +1,4 @@ -package postman2case +package convert import ( "bytes" @@ -19,6 +19,83 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/json" ) +// ==================== model definition starts here ==================== + +/* +Postman Collection format reference: +https://schema.postman.com/json/collection/v2.0.0/collection.json +https://schema.postman.com/json/collection/v2.1.0/collection.json +*/ + +// CasePostman represents the postman exported file +type CasePostman struct { + Info TInfo `json:"info"` + Items []TItem `json:"item"` +} + +// TInfo gives information about the collection +type TInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Schema string `json:"schema"` +} + +// TItem contains the detail information of request and expected responses +// item could be defined recursively +type TItem struct { + Items []TItem `json:"item"` + Name string `json:"name"` + Request TRequest `json:"request"` + Responses []TResponse `json:"response"` +} + +type TRequest struct { + Method string `json:"method"` + Headers []TField `json:"header"` + Body TBody `json:"body"` + URL TUrl `json:"url"` + Description string `json:"description"` +} + +type TResponse struct { + Name string `json:"name"` + OriginalRequest TRequest `json:"originalRequest"` + Status string `json:"status"` + Code int `json:"code"` + Headers []TField `json:"headers"` + Body string `json:"body"` +} + +type TUrl struct { + Raw string `json:"raw"` + Protocol string `json:"protocol"` + Path []string `json:"path"` + Description string `json:"description"` + Query []TField `json:"query"` + Variable []TField `json:"variable"` +} + +type TField struct { + Key string `json:"key"` + Value string `json:"value"` + Src string `json:"src"` + Description string `json:"description"` + Type string `json:"type"` + Disabled bool `json:"disabled"` + Enable bool `json:"enable"` +} + +type TBody struct { + Mode string `json:"mode"` + FormData []TField `json:"formdata"` + URLEncoded []TField `json:"urlencoded"` + Raw string `json:"raw"` + Disabled bool `json:"disabled"` + Options interface{} `json:"options"` +} + +// ==================== model definition ends here ==================== + const ( enumBodyRaw = "raw" enumBodyUrlEncoded = "urlencoded" @@ -32,19 +109,6 @@ const ( enumFieldTypeFile = "file" ) -const ( - suffixName = ".converted" // distinguish the converted json(testcase) from the origin json(collection) - extensionJSON = ".json" - extensionYAML = ".yaml" -) - -const ( - configProfile = "profile" - configPatch = "patch" - keyHeaders = "headers" - keyCookies = "cookies" -) - var contentTypeMap = map[string]string{ "text": "text/plain", "javascript": "application/javascript", @@ -53,119 +117,131 @@ var contentTypeMap = map[string]string{ "xml": "application/xml", } -func NewCollection(path string) *collection { - return &collection{ - path: path, +func NewConverterPostman(converter *TCaseConverter) *ConverterPostman { + return &ConverterPostman{ + converter: converter, } } -type collection struct { - path string - profile map[string]interface{} - patch map[string]interface{} - outputDir string +type ConverterPostman struct { + converter *TCaseConverter } -func (c *collection) SetProfile(path string) { - log.Info().Str("path", path).Msg("set profile") - c.profile = make(map[string]interface{}) - err := builtin.LoadFile(path, c.profile) - if err != nil { - log.Warn().Str("path", path). - Msg("invalid profile format, ignore!") - } +func (c *ConverterPostman) Struct() *TCaseConverter { + return c.converter } -func (c *collection) SetPatch(path string) { - log.Info().Str("path", path).Msg("set patch") - c.patch = make(map[string]interface{}) - err := builtin.LoadFile(path, c.patch) - if err != nil { - log.Warn().Str("path", path). - Msg("invalid patch format, ignore!") - } -} - -func (c *collection) SetOutputDir(dir string) { - log.Info().Str("dir", dir).Msg("set output directory") - c.outputDir = dir -} - -func (c *collection) GenJSON() (jsonPath string, err error) { +func (c *ConverterPostman) ToJSON() (string, error) { testCase, err := c.makeTestCase() if err != nil { return "", err } - jsonPath = c.genOutputPath(extensionJSON) + jsonPath := c.converter.genOutputPath(suffixJSON) err = builtin.Dump2JSON(testCase, jsonPath) - return + if err != nil { + return "", err + } + return jsonPath, nil } -func (c *collection) GenYAML() (yamlPath string, err error) { +func (c *ConverterPostman) ToJSONTemp() (string, error) { + testCase, err := c.makeTestCaseTemp() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(testCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterPostman) ToYAML() (string, error) { testCase, err := c.makeTestCase() if err != nil { return "", err } - yamlPath = c.genOutputPath(extensionYAML) + yamlPath := c.converter.genOutputPath(suffixYAML) err = builtin.Dump2YAML(testCase, yamlPath) - return -} - -func (c *collection) genOutputPath(suffix string) string { - file := getFilenameWithoutExtension(c.path) + suffix - if c.outputDir != "" { - return filepath.Join(c.outputDir, file) - } else { - return filepath.Join(filepath.Dir(c.path), file) + if err != nil { + return "", err } + return yamlPath, nil } -func getFilenameWithoutExtension(path string) string { - base := filepath.Base(path) - ext := filepath.Ext(base) - return base[0:len(base)-len(ext)] + suffixName +func (c *ConverterPostman) ToGoTest() (string, error) { + //TODO implement me + return "", errors.New("convert from postman to gotest scripts is not supported yet") } -func (c *collection) makeTestCase() (*hrp.TCase, error) { - tCollection, err := c.load() +func (c *ConverterPostman) ToPyTest() (string, error) { + return convertToPyTest(c) +} + +func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) { + casePostman, err := c.load() if err != nil { return nil, err } - teststeps, err := c.prepareTestSteps(tCollection) + teststeps, err := c.prepareTestSteps(casePostman) if err != nil { return nil, err } tCase := &hrp.TCase{ - Config: c.prepareConfig(tCollection), + Config: c.prepareConfig(casePostman), TestSteps: teststeps, } + err = tCase.MakeCompat2GoEngine() + if err != nil { + return nil, err + } return tCase, nil } -func (c *collection) load() (*TCollection, error) { - collection := &TCollection{} - err := builtin.LoadFile(c.path, collection) +func (c *ConverterPostman) makeTestCaseTemp() (*hrp.TCase, error) { + casePostman, err := c.load() if err != nil { - return nil, errors.Wrap(err, "load postman collection failed") + return nil, err } - return collection, nil + teststeps, err := c.prepareTestSteps(casePostman) + if err != nil { + return nil, err + } + tCase := &hrp.TCase{ + Config: c.prepareConfig(casePostman), + TestSteps: teststeps, + } + err = tCase.MakeCompat2PyEngine() + if err != nil { + return nil, err + } + return tCase, nil } -func (c *collection) prepareConfig(tCollection *TCollection) *hrp.TConfig { - return hrp.NewConfig(tCollection.Info.Name). +func (c *ConverterPostman) load() (*CasePostman, error) { + casePostman := c.converter.CasePostman + if casePostman == nil { + return nil, errors.New("empty postman case occurs") + } + return casePostman, nil +} + +func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig { + return hrp.NewConfig(casePostman.Info.Name). SetVerifySSL(false) } -func (c *collection) prepareTestSteps(tCollection *TCollection) ([]*hrp.TStep, error) { +func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) { // recursively convert collection items into a list var itemList []TItem - for _, item := range tCollection.Items { + for _, item := range casePostman.Items { extractItemList(item, &itemList) } var steps []*hrp.TStep for _, item := range itemList { - step, err := c.prepareTestStep(&item) + step, err := c.prepareTestStep(&item, steps) if err != nil { return nil, err } @@ -191,19 +267,18 @@ func extractItemList(item TItem, itemList *[]TItem) { } } -func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) { +func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) { log.Info(). Str("method", item.Request.Method). Str("url", item.Request.URL.Raw). Msg("convert teststep") - step := &tStep{ + step := &stepFromPostman{ TStep: hrp.TStep{ Request: &hrp.Request{}, Validators: make([]interface{}, 0), }, - profile: c.profile, - patch: c.patch, + profile: c.converter.Profile, } if err := step.makeRequestName(item); err != nil { return nil, err @@ -223,30 +298,29 @@ func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) { if err := step.makeRequestCookies(item); err != nil { return nil, err } - if err := step.makeRequestBody(item); err != nil { + if err := step.makeRequestBody(item, steps); err != nil { return nil, err } return &step.TStep, nil } -type tStep struct { +type stepFromPostman struct { hrp.TStep - profile map[string]interface{} - patch map[string]interface{} + profile *Profile } // makeRequestName indicates the step name the same as item name -func (s *tStep) makeRequestName(item *TItem) error { +func (s *stepFromPostman) makeRequestName(item *TItem) error { s.Name = item.Name return nil } -func (s *tStep) makeRequestMethod(item *TItem) error { +func (s *stepFromPostman) makeRequestMethod(item *TItem) error { s.Request.Method = hrp.HTTPMethod(item.Request.Method) return nil } -func (s *tStep) makeRequestURL(item *TItem) error { +func (s *stepFromPostman) makeRequestURL(item *TItem) error { rawUrl := item.Request.URL.Raw // parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2 for _, field := range item.Request.URL.Variable { @@ -261,7 +335,7 @@ func (s *tStep) makeRequestURL(item *TItem) error { return nil } -func (s *tStep) makeRequestParams(item *TItem) error { +func (s *stepFromPostman) makeRequestParams(item *TItem) error { s.Request.Params = make(map[string]interface{}) for _, field := range item.Request.URL.Query { if field.Disabled { @@ -272,44 +346,9 @@ func (s *tStep) makeRequestParams(item *TItem) error { return nil } -func (s *tStep) updateRequestInfo(config string, key string) bool { - var m map[string]interface{} - switch config { - case configProfile: - m = s.profile - case configPatch: - m = s.patch - default: - return false - } - iRequestMap, existed := m[key] - if existed { - requestMap, ok := iRequestMap.(map[string]interface{}) - if ok { - for k, v := range requestMap { - switch key { - case keyHeaders: - s.Request.Headers[k] = fmt.Sprintf("%v", v) - case keyCookies: - s.Request.Cookies[k] = fmt.Sprintf("%v", v) - } - } - return true - } - log.Warn().Interface(key, iRequestMap).Msgf("%v from %v is not a map, ignore!", key, config) - } - return false -} - -func (s *tStep) makeRequestHeaders(item *TItem) error { - s.Request.Headers = make(map[string]string) - - // override all headers according to the profile - if s.updateRequestInfo(configProfile, keyHeaders) { - return nil - } - +func (s *stepFromPostman) makeRequestHeaders(item *TItem) error { // headers defined in postman collection + s.Request.Headers = make(map[string]string) for _, field := range item.Request.Headers { if field.Disabled || strings.EqualFold(field.Key, "cookie") { continue @@ -317,20 +356,23 @@ func (s *tStep) makeRequestHeaders(item *TItem) error { s.Request.Headers[field.Key] = field.Value } - // create or update the headers indicated in the patch - s.updateRequestInfo(configPatch, keyHeaders) + if s.profile == nil { + return nil + } + // override all headers according to the profile + if s.profile.Override { + s.Request.Headers = make(map[string]string) + } + // create or update the headers according to the profile + for k, v := range s.profile.Headers { + s.Request.Headers[k] = v + } return nil } -func (s *tStep) makeRequestCookies(item *TItem) error { - s.Request.Cookies = make(map[string]string) - - // override all cookies according to the profile - if s.updateRequestInfo(configProfile, keyCookies) { - return nil - } - +func (s *stepFromPostman) makeRequestCookies(item *TItem) error { // cookies defined in postman collection + s.Request.Cookies = make(map[string]string) for _, field := range item.Request.Headers { if field.Disabled || !strings.EqualFold(field.Key, "cookie") { continue @@ -338,12 +380,21 @@ func (s *tStep) makeRequestCookies(item *TItem) error { s.parseRequestCookiesMap(field.Value) } - // create or update the cookies indicated in the patch - s.updateRequestInfo(configPatch, keyCookies) + if s.profile == nil { + return nil + } + // override all cookies according to the profile + if s.profile.Override { + s.Request.Cookies = make(map[string]string) + } + // create or update the cookies according to the profile + for k, v := range s.profile.Cookies { + s.Request.Cookies[k] = v + } return nil } -func (s *tStep) parseRequestCookiesMap(cookies string) { +func (s *stepFromPostman) parseRequestCookiesMap(cookies string) { for _, cookie := range strings.Split(cookies, ";") { cookie = strings.TrimSpace(cookie) index := strings.Index(cookie, "=") @@ -351,11 +402,11 @@ func (s *tStep) parseRequestCookiesMap(cookies string) { log.Warn().Str("cookie", cookie).Msg("cookie format invalid") continue } - s.Request.Cookies[cookie[0:index]] = cookie[index+1:] + s.Request.Cookies[cookie[:index]] = cookie[index+1:] } } -func (s *tStep) makeRequestBody(item *TItem) error { +func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error { mode := item.Request.Body.Mode if mode == "" { return nil @@ -364,7 +415,7 @@ func (s *tStep) makeRequestBody(item *TItem) error { case enumBodyRaw: return s.makeRequestBodyRaw(item) case enumBodyFormData: - return s.makeRequestBodyFormData(item) + return s.makeRequestBodyFormData(item, steps) case enumBodyUrlEncoded: return s.makeRequestBodyUrlEncoded(item) case enumBodyFile, enumBodyGraphQL: @@ -373,7 +424,7 @@ func (s *tStep) makeRequestBody(item *TItem) error { return nil } -func (s *tStep) makeRequestBodyRaw(item *TItem) (err error) { +func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("make request body raw failed: %v", p) @@ -401,7 +452,7 @@ func (s *tStep) makeRequestBodyRaw(item *TItem) (err error) { return } -func (s *tStep) makeRequestBodyFormData(item *TItem) (err error) { +func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) { defer func() { if err != nil { err = errors.Wrap(err, "make request body form-data failed") @@ -446,7 +497,7 @@ func writeFormDataFile(writer *multipart.Writer, field *TField) error { return err } -func (s *tStep) makeRequestBodyUrlEncoded(item *TItem) error { +func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error { payloadMap := make(map[string]string) for _, field := range item.Request.Body.URLEncoded { if field.Disabled { @@ -460,6 +511,6 @@ func (s *tStep) makeRequestBodyUrlEncoded(item *TItem) error { } // TODO makeValidate from example response -func (s *tStep) makeValidate(item *TItem) error { +func (s *stepFromPostman) makeValidate(item *TItem) error { return nil } diff --git a/hrp/internal/convert/postman2case/core_test.go b/hrp/internal/convert/converter_postman_test.go similarity index 73% rename from hrp/internal/convert/postman2case/core_test.go rename to hrp/internal/convert/converter_postman_test.go index a102e136..72994794 100644 --- a/hrp/internal/convert/postman2case/core_test.go +++ b/hrp/internal/convert/converter_postman_test.go @@ -1,4 +1,4 @@ -package postman2case +package convert import ( "testing" @@ -7,13 +7,15 @@ import ( ) var ( - collectionPath = "../../../../examples/data/postman2case/demo.json" - profilePath = "../../../../examples/data/postman2case/profile.yml" - patchPath = "../../../../examples/data/postman2case/patch.yml" + collectionPath = "../../../examples/data/postman2case/demo.json" + collectionProfileOverridePath = "../../../examples/data/postman2case/profile_override.yml" + collectionProfilePath = "../../../examples/data/postman2case/profile.yml" ) -func TestGenJSON(t *testing.T) { - jsonPath, err := NewCollection(collectionPath).GenJSON() +var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath)) + +func TestPostman2JSON(t *testing.T) { + jsonPath, err := converterPostman.ToJSON() if !assert.NoError(t, err) { t.Fatal() } @@ -22,8 +24,8 @@ func TestGenJSON(t *testing.T) { } } -func TestGenYAML(t *testing.T) { - yamlPath, err := NewCollection(collectionPath).GenYAML() +func TestPostman2YAML(t *testing.T) { + yamlPath, err := converterPostman.ToYAML() if !assert.NoError(t, err) { t.Fatal() } @@ -33,17 +35,17 @@ func TestGenYAML(t *testing.T) { } func TestLoadCollection(t *testing.T) { - tCollection, err := NewCollection(collectionPath).load() + casePostman, err := converterPostman.load() if !assert.NoError(t, err) { t.Fatal(err) } - if !assert.Equal(t, "postman collection demo", tCollection.Info.Name) { + if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) { t.Fatal() } } -func TestMakeTestCase(t *testing.T) { - tCase, err := NewCollection(collectionPath).makeTestCase() +func TestMakeTestCaseFromCollection(t *testing.T) { + tCase, err := converterPostman.makeTestCase() if !assert.NoError(t, err) { t.Fatal() } @@ -107,9 +109,10 @@ func TestMakeTestCase(t *testing.T) { } } -func TestMakeTestCaseWithProfile(t *testing.T) { - c := NewCollection(collectionPath) - c.SetProfile(profilePath) +func TestMakeTestCaseWithProfileOverride(t *testing.T) { + tCaseConverter := NewTCaseConverter(collectionPath) + tCaseConverter.SetProfile(collectionProfileOverridePath) + c := NewConverterPostman(tCaseConverter) tCase, err := c.makeTestCase() if !assert.NoError(t, err) { t.Fatal() @@ -133,22 +136,23 @@ func TestMakeTestCaseWithProfile(t *testing.T) { } } -func TestMakeTestCaseWithPatch(t *testing.T) { - c := NewCollection(collectionPath) - c.SetPatch(patchPath) +func TestMakeTestCaseWithProfile(t *testing.T) { + tCaseConverter := NewTCaseConverter(collectionPath) + tCaseConverter.SetProfile(collectionProfilePath) + c := NewConverterPostman(tCaseConverter) tCase, err := c.makeTestCase() if !assert.NoError(t, err) { t.Fatal() } - // create cookies Cookie1 indicated in patch + // create cookies Cookie1 indicated in profile if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) { t.Fatal() } - // update header User-Agent indicated in patch + // update header User-Agent indicated in profile if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) { t.Fatal() } - // pass header Connection which is not indicated in patch + // pass header Connection which is not indicated in profile if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) { t.Fatal() } diff --git a/hrp/internal/convert/converter_pytest.go b/hrp/internal/convert/converter_pytest.go new file mode 100644 index 00000000..8c094900 --- /dev/null +++ b/hrp/internal/convert/converter_pytest.go @@ -0,0 +1,19 @@ +package convert + +import ( + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/version" +) + +func convert2PyTestScripts(paths ...string) error { + httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion) + python3, err := builtin.EnsurePython3Venv(httprunner) + if err != nil { + return err + } + + args := append([]string{"-m", "httprunner", "make"}, paths...) + return builtin.ExecCommand(python3, args...) +} diff --git a/hrp/internal/convert/converter_yaml.go b/hrp/internal/convert/converter_yaml.go new file mode 100644 index 00000000..81d1b1a9 --- /dev/null +++ b/hrp/internal/convert/converter_yaml.go @@ -0,0 +1,94 @@ +package convert + +import ( + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +func NewConverterYAML(converter *TCaseConverter) *ConverterYAML { + return &ConverterYAML{ + converter: converter, + } +} + +type ConverterYAML struct { + converter *TCaseConverter +} + +func (c *ConverterYAML) Struct() *TCaseConverter { + return c.converter +} + +func (c *ConverterYAML) ToJSON() (string, error) { + testCase, err := c.makeTestCase() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(testCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterYAML) ToJSONTemp() (string, error) { + testCase, err := c.makeTestCaseTemp() + if err != nil { + return "", err + } + jsonPath := c.converter.genOutputPath(suffixJSON) + err = builtin.Dump2JSON(testCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} + +func (c *ConverterYAML) ToYAML() (string, error) { + testCase, err := c.makeTestCase() + if err != nil { + return "", err + } + yamlPath := c.converter.genOutputPath(suffixYAML) + err = builtin.Dump2YAML(testCase, yamlPath) + if err != nil { + return "", err + } + return yamlPath, nil +} + +func (c *ConverterYAML) ToGoTest() (string, error) { + //TODO implement me + return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet") +} + +func (c *ConverterYAML) ToPyTest() (string, error) { + return convertToPyTest(c) +} + +func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) { + tCase, err := makeTestCaseFromJSONYAML(c) + if err != nil { + return nil, err + } + err = tCase.MakeCompat2GoEngine() + if err != nil { + return nil, err + } + return tCase, nil +} + +func (c *ConverterYAML) makeTestCaseTemp() (*hrp.TCase, error) { + tCase, err := makeTestCaseFromJSONYAML(c) + if err != nil { + return nil, err + } + err = tCase.MakeCompat2PyEngine() + if err != nil { + return nil, err + } + return tCase, nil +} diff --git a/hrp/internal/convert/har2case/core.go b/hrp/internal/convert/har2case/core.go index 0e96a96d..25824855 100644 --- a/hrp/internal/convert/har2case/core.go +++ b/hrp/internal/convert/har2case/core.go @@ -22,13 +22,6 @@ const ( suffixYAML = ".yaml" ) -const ( - configProfile = "profile" - configPatch = "patch" - keyHeaders = "headers" - keyCookies = "cookies" -) - func NewHAR(path string) *har { return &har{ path: path, @@ -40,7 +33,6 @@ type har struct { filterStr string excludeStr string profile map[string]interface{} - patch map[string]interface{} outputDir string } @@ -54,16 +46,6 @@ func (h *har) SetProfile(path string) { } } -func (h *har) SetPatch(path string) { - log.Info().Str("path", path).Msg("set patch") - h.patch = make(map[string]interface{}) - err := builtin.LoadFile(path, h.patch) - if err != nil { - log.Warn().Str("path", path). - Msg("invalid patch format, ignore!") - } -} - func (h *har) SetOutputDir(dir string) { log.Info().Str("dir", dir).Msg("set output directory") h.outputDir = dir @@ -164,7 +146,6 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { Validators: make([]interface{}, 0), }, profile: h.profile, - patch: h.patch, } if err := step.makeRequestMethod(entry); err != nil { return nil, err @@ -193,7 +174,6 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { type tStep struct { hrp.TStep profile map[string]interface{} - patch map[string]interface{} } func (s *tStep) makeRequestMethod(entry *Entry) error { @@ -219,59 +199,43 @@ func (s *tStep) makeRequestParams(entry *Entry) error { return nil } -func (s *tStep) updateRequestInfo(config string, key string) bool { - var m map[string]interface{} - switch config { - case configProfile: - m = s.profile - case configPatch: - m = s.patch - default: - return false - } - iRequestMap, existed := m[key] - if existed { - requestMap, ok := iRequestMap.(map[string]interface{}) - if ok { - for k, v := range requestMap { - switch key { - case keyHeaders: - s.Request.Headers[k] = fmt.Sprintf("%v", v) - case keyCookies: - s.Request.Cookies[k] = fmt.Sprintf("%v", v) - } - } - return true - } - log.Warn().Interface(key, iRequestMap).Msgf("%v from %v is not a map, ignore!", key, config) - } - return false -} - func (s *tStep) makeRequestCookies(entry *Entry) error { s.Request.Cookies = make(map[string]string) - - // override all cookies according to the profile - if s.updateRequestInfo(configProfile, keyCookies) { - return nil + cookies, ok := s.profile["cookies"] + if ok { + // use cookies from profile + cookies, ok := cookies.(map[string]interface{}) + if ok { + for k, v := range cookies { + s.Request.Cookies[k] = fmt.Sprintf("%v", v) + } + return nil + } + log.Warn().Interface("cookies", cookies). + Msg("cookies from profile is not a map, ignore!") } // use cookies from har for _, cookie := range entry.Request.Cookies { s.Request.Cookies[cookie.Name] = cookie.Value } - - // create or update the cookies indicated in the patch - s.updateRequestInfo(configPatch, keyCookies) return nil } func (s *tStep) makeRequestHeaders(entry *Entry) error { s.Request.Headers = make(map[string]string) - - // override all headers according to the profile - if s.updateRequestInfo(configProfile, keyHeaders) { - return nil + headers, ok := s.profile["headers"] + if ok { + // use headers from profile + cookies, ok := headers.(map[string]interface{}) + if ok { + for k, v := range cookies { + s.Request.Headers[k] = fmt.Sprintf("%v", v) + } + return nil + } + log.Warn().Interface("headers", headers). + Msg("headers from profile is not a map, ignore!") } // use headers from har @@ -281,9 +245,6 @@ func (s *tStep) makeRequestHeaders(entry *Entry) error { } s.Request.Headers[header.Name] = header.Value } - - // create or update the headers indicated in the patch - s.updateRequestInfo(configPatch, keyHeaders) return nil } diff --git a/hrp/internal/convert/har2case/core_test.go b/hrp/internal/convert/har2case/core_test.go index ce6466fe..0fc6a3cb 100644 --- a/hrp/internal/convert/har2case/core_test.go +++ b/hrp/internal/convert/har2case/core_test.go @@ -1,7 +1,6 @@ package har2case import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -12,7 +11,7 @@ import ( var ( harPath = "../../../../examples/data/har/demo.har" harPath2 = "../../../../examples/data/har/postman-echo.har" - profilePath = "../../../../examples/data/har/profile.yml" + profilePath = "../../../../examples/data/har/profile_override.yml" ) func TestGenJSON(t *testing.T) { @@ -382,32 +381,3 @@ func TestMakeValidate(t *testing.T) { t.Fatal() } } - -func Test_tStep_makeRequestCookies(t *testing.T) { - type fields struct { - TStep hrp.TStep - profile map[string]interface{} - patch map[string]interface{} - } - type args struct { - entry *Entry - } - tests := []struct { - name string - fields fields - args args - wantErr assert.ErrorAssertionFunc - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &tStep{ - TStep: tt.fields.TStep, - profile: tt.fields.profile, - patch: tt.fields.patch, - } - tt.wantErr(t, s.makeRequestCookies(tt.args.entry), fmt.Sprintf("makeRequestCookies(%v)", tt.args.entry)) - }) - } -} diff --git a/hrp/internal/convert/postman2case/collection.go b/hrp/internal/convert/postman2case/collection.go deleted file mode 100644 index ddabee21..00000000 --- a/hrp/internal/convert/postman2case/collection.go +++ /dev/null @@ -1,74 +0,0 @@ -package postman2case - -/* -Postman Collection format reference: -https://schema.postman.com/json/collection/v2.0.0/collection.json -https://schema.postman.com/json/collection/v2.1.0/collection.json -*/ - -// TCollection represents the postman exported file -type TCollection struct { - Info TInfo `json:"info"` - Items []TItem `json:"item"` -} - -// TInfo gives information about the collection -type TInfo struct { - Name string `json:"name"` - Description string `json:"description"` - Schema string `json:"schema"` -} - -// TItem contains the detail information of request and expected responses -// item could be defined recursively -type TItem struct { - Items []TItem `json:"item"` - Name string `json:"name"` - Request TRequest `json:"request"` - Responses []TResponse `json:"response"` -} - -type TRequest struct { - Method string `json:"method"` - Headers []TField `json:"header"` - Body TBody `json:"body"` - URL TUrl `json:"url"` - Description string `json:"description"` -} - -type TResponse struct { - Name string `json:"name"` - OriginalRequest TRequest `json:"originalRequest"` - Status string `json:"status"` - Code int `json:"code"` - Headers []TField `json:"headers"` - Body string `json:"body"` -} - -type TUrl struct { - Raw string `json:"raw"` - Protocol string `json:"protocol"` - Path []string `json:"path"` - Description string `json:"description"` - Query []TField `json:"query"` - Variable []TField `json:"variable"` -} - -type TField struct { - Key string `json:"key"` - Value string `json:"value"` - Src string `json:"src"` - Description string `json:"description"` - Type string `json:"type"` - Disabled bool `json:"disabled"` - Enable bool `json:"enable"` -} - -type TBody struct { - Mode string `json:"mode"` - FormData []TField `json:"formdata"` - URLEncoded []TField `json:"urlencoded"` - Raw string `json:"raw"` - Disabled bool `json:"disabled"` - Options interface{} `json:"options"` -} diff --git a/hrp/internal/convert/case2script/testcase.tmpl b/hrp/internal/convert/testcase.tmpl similarity index 100% rename from hrp/internal/convert/case2script/testcase.tmpl rename to hrp/internal/convert/testcase.tmpl diff --git a/hrp/step_api.go b/hrp/step_api.go index 1c9992ba..7b57d896 100644 --- a/hrp/step_api.go +++ b/hrp/step_api.go @@ -47,7 +47,7 @@ func (path *APIPath) ToAPI() (*API, error) { if err != nil { return nil, err } - err = convertCompatValidator(api.Validators) + err = convertValidatorCompat2GoEngine(api.Validators) convertExtract(api.Extract) return api, err } diff --git a/hrp/testcase.go b/hrp/testcase.go index 05c2b051..d2c2bd80 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -64,7 +64,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) { return nil, errors.New("incorrect testcase file format, expected config in file") } - err = tc.makeCompat() + err = tc.MakeCompat2GoEngine() if err != nil { return nil, err } @@ -154,27 +154,28 @@ type TCase struct { TestSteps []*TStep `json:"teststeps" yaml:"teststeps"` } -// makeCompat converts TCase to compatible testcase -func (tc *TCase) makeCompat() error { - var err error +// MakeCompat2GoEngine converts TCase compatible with Golang engine style +func (tc *TCase) MakeCompat2GoEngine() (err error) { defer func() { if p := recover(); p != nil { - err = fmt.Errorf("convert compat testcase error: %v", p) + err = fmt.Errorf("[MakeCompat2GoEngine] convert compat testcase error: %v", p) } }() for _, step := range tc.TestSteps { - // 1. deal with request body compatible with HttpRunner + // 1. deal with request body compatibility if step.Request != nil && step.Request.Body == nil { if step.Request.Json != nil { step.Request.Headers["Content-Type"] = "application/json; charset=utf-8" step.Request.Body = step.Request.Json + step.Request.Json = nil } else if step.Request.Data != nil { step.Request.Body = step.Request.Data + step.Request.Data = nil } } - // 2. deal with validators compatible with HttpRunner - err = convertCompatValidator(step.Validators) + // 2. deal with validators compatibility + err = convertValidatorCompat2GoEngine(step.Validators) if err != nil { return err } @@ -185,16 +186,19 @@ func (tc *TCase) makeCompat() error { return nil } -func convertCompatValidator(Validators []interface{}) (err error) { +func convertValidatorCompat2GoEngine(Validators []interface{}) (err error) { for i, iValidator := range Validators { + if _, ok := iValidator.(Validator); ok { + continue + } validatorMap := iValidator.(map[string]interface{}) validator := Validator{} _, checkExisted := validatorMap["check"] _, assertExisted := validatorMap["assert"] _, expectExisted := validatorMap["expect"] - // check priority: HRP > HttpRunner + // validator check priority: Golang > Python engine style if checkExisted && assertExisted && expectExisted { - // HRP validator format + // Golang engine style validator.Check = validatorMap["check"].(string) validator.Assert = validatorMap["assert"].(string) validator.Expect = validatorMap["expect"] @@ -203,8 +207,10 @@ func convertCompatValidator(Validators []interface{}) (err error) { } validator.Check = convertCheckExpr(validator.Check) Validators[i] = validator - } else if len(validatorMap) == 1 { - // HttpRunner validator format + continue + } + if len(validatorMap) == 1 { + // Python engine style for assertMethod, iValidatorContent := range validatorMap { checkAndExpect := iValidatorContent.([]interface{}) if len(checkAndExpect) != 2 { @@ -216,9 +222,9 @@ func convertCompatValidator(Validators []interface{}) (err error) { } validator.Check = convertCheckExpr(validator.Check) Validators[i] = validator - } else { - return fmt.Errorf("unexpected validator format: %v", validatorMap) + continue } + return fmt.Errorf("unexpected validator format: %v", validatorMap) } return nil } @@ -244,6 +250,74 @@ func convertCheckExpr(checkExpr string) string { return strings.Join(checkItems, ".") } +// MakeCompat2PyEngine converts TCase compatible with Python engine style +func (tc *TCase) MakeCompat2PyEngine() (err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("[MakeCompat2PyEngine] convert compat testcase error: %v", p) + } + }() + for _, step := range tc.TestSteps { + // 1. deal with request body compatibility + if step.Request != nil && step.Request.Body != nil { + if strings.HasPrefix(step.Request.Headers["Content-Type"], "application/json") { + step.Request.Json = step.Request.Body + step.Request.Body = nil + continue + } + step.Request.Data = step.Request.Body + step.Request.Body = nil + } + + // 2. deal with validators compatibility + err = convertValidatorCompat2PyEngine(step.Validators) + if err != nil { + return err + } + } + return +} + +func convertValidatorCompat2PyEngine(Validators []interface{}) (err error) { + for i, iValidator := range Validators { + if v, ok := iValidator.(Validator); ok { + var iValidatorContent []interface{} + iValidatorContent = append(iValidatorContent, v.Check) + iValidatorContent = append(iValidatorContent, v.Expect) + newValidatorMap := make(map[string]interface{}) + newValidatorMap[v.Assert] = iValidatorContent + Validators[i] = newValidatorMap + continue + } + validatorMap := iValidator.(map[string]interface{}) + // validator check priority: Python > Golang engine style + if len(validatorMap) == 1 { + // Python engine style + for _, iValidatorContent := range validatorMap { + checkAndExpect := iValidatorContent.([]interface{}) + if len(checkAndExpect) != 2 { + return fmt.Errorf("unexpected validator format: %v", validatorMap) + } + } + continue + } + _, checkExisted := validatorMap["check"] + _, assertExisted := validatorMap["assert"] + _, expectExisted := validatorMap["expect"] + if checkExisted && assertExisted && expectExisted { + // Golang engine style + var iValidatorContent []interface{} + iValidatorContent = append(iValidatorContent, validatorMap["check"]) + iValidatorContent = append(iValidatorContent, validatorMap["expect"]) + newValidatorMap := make(map[string]interface{}) + newValidatorMap[validatorMap["assert"].(string)] = iValidatorContent + Validators[i] = newValidatorMap + continue + } + return fmt.Errorf("unexpected validator format: %v", validatorMap) + } + return +} func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { testCases := make([]*TestCase, 0)