diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b37e9b..1b54764 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,11 +16,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: '1.15.0' # The Go version to download (if necessary) and use. + go-version: '1.21.8' # The Go version to download (if necessary) and use. # - name: Install dependencies # run: | # go version @@ -29,7 +29,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.31 + version: v1.57.2 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true # Run build of the application @@ -39,22 +39,3 @@ jobs: # Run testing on the code - name: Run testing run: make test -# -# -# # The "deploy" workflow -# deploy: -# # The type of runner that the job will run on -# runs-on: ubuntu-latest -# needs: [build] # Only run this workflow when "build" workflow succeeds -# if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'push' }} # Only run this workflow if it is master branch on push event -# steps: -# - uses: actions/checkout@v2 -# -# # Deploy to Docker registry -# - name: Deploy to Docker registry -# uses: docker/build-push-action@v1 -# with: -# username: ${{ secrets.DOCKER_USERNAME }} -# password: ${{ secrets.DOCKER_PASSWORD }} -# repository: wilsontanwm/gosimple -# tag_with_ref: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a6449c8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +# .github/workflows/release.yml +name: goreleaser + +on: + push: + # run only against tags + tags: + - "*" + +permissions: + contents: write + # packages: write + # issues: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + # More assembly might be required: Docker logins, GPG, etc. + # It all depends on your needs. + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + # 'latest', 'nightly', or a semver + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index a7518af..b5cd21c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,7 +18,7 @@ linters-settings: lll: line-length: 140 goimports: - local-prefixes: github.com/avarabyeu/goRP/v5 + local-prefixes: github.com/reportportal/goRP/v5 gocritic: enabled-tags: - performance @@ -41,6 +41,16 @@ linters: - testpackage - nlreturn - stylecheck + - exhaustivestruct + - exhaustruct + - varnamelen + - tagliatelle + - wrapcheck + - gomnd + - gci + - revive + - testableexamples + - depguard #run: # skip-dirs: @@ -53,9 +63,3 @@ issues: linters: - gosec -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.15.x # use the fixed version to not introduce new linters unexpectedly -# prepare: -# - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/.goreleaser.yml b/.goreleaser.yml index 543289c..f4770f4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,17 +1,25 @@ builds: - - env: + - id: gorp + main: ./ + binary: gorp + env: - CGO_ENABLED=0 goos: - linux - windows - darwin + goarch: + - amd64 + - arm + - arm64 archives: - - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 + - name_template: >- + {{- .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end -}} checksum: name_template: 'checksums.txt' snapshot: diff --git a/.travis.yml b/.travis.yml index d82a79f..38b83ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,4 +13,4 @@ script: - make checkstyle build after_success: - - curl --request POST "https://goreportcard.com/checks" --data "repo=github.com/avarabyeu/goRP" + - curl --request POST "https://goreportcard.com/checks" --data "repo=github.com/reportportal/goRP" diff --git a/Dockerfile b/Dockerfile index 19336fa..c55250a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ ARG dockerTag RUN echo $dockerTag | awk -F"v" '{ print $2 }' > version RUN cat ./version -RUN version=$(cat version) && curl -L >gorp.tar.gz https://github.com/avarabyeu/goRP/releases/download/$dockerTag/goRP_${version}_linux_amd64.tar.gz \ +RUN version=$(cat version) && curl -L >gorp.tar.gz https://github.com/reportportal/goRP/releases/download/$dockerTag/goRP_${version}_linux_amd64.tar.gz \ && tar -xzvf gorp.tar.gz -C /usr/bin \ && rm gorp.tar.gz diff --git a/Makefile b/Makefile index 2e4ec25..00bb397 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,5 @@ .DEFAULT_GOAL := build - BUILD_DATE = `date +%FT%T%z` - GO = go BINARY_DIR=bin @@ -17,11 +15,7 @@ help: @echo "checkstyle - gofmt+golint+misspell" init-deps: - # installs gometalinter -# curl -L https://git.io/vp6lP | sh -# gometalinter --install - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.31.0 - + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.57.2 #vendor: # dep ensure --vendor-only @@ -30,19 +24,18 @@ test: $(GO) test -cover ${GODIRS_NOVENDOR} lint: - golangci-lint run --enable-all --deadline 10m ./... + bin/golangci-lint run ./... fmt: - gofumpt -extra -l -w -s ${GOFILES_NOVENDOR} - gofumports -local -l -w ${GOFILES_NOVENDOR} - gci -local github.com/avarabyeu/goRP/v5 -w ${GOFILES_NOVENDOR} + gofumpt -extra -l -w ${GOFILES_NOVENDOR} + gci write --section Standard --section Default --section "Prefix(github.com/reportportal/goRP/v5)" ${GOFILES_NOVENDOR} #build: checkstyle test build: $(GO) build ${BUILD_INFO_LDFLAGS} -o ${BINARY_DIR}/gorp ./ cross-build: - gox ${BUILD_INFO_LDFLAGS} -arch="amd64 386" -os="linux windows darwin" -output="dist/{{.Dir}}_{{.OS}}_{{.Arch}}" + gox ${BUILD_INFO_LDFLAGS} -arch="amd64 arm64" -os="linux windows darwin" -output="dist/{{.Dir}}_{{.OS}}_{{.Arch}}" ./cmd/gorp clean: if [ -d ${BINARY_DIR} ] ; then rm -r ${BINARY_DIR} ; fi diff --git a/README.md b/README.md index 41f7432..6053b86 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,105 @@ -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/avarabyeu/goRP/Build) -[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/eBay/fabio/master/LICENSE) -[![Go Report Card](https://goreportcard.com/badge/github.com/avarabyeu/goRP)](https://goreportcard.com/report/github.com/avarabyeu/goRP) +![Build Status](https://github.com/reportportal/goRP/workflows/Build/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/reportportal/goRP)](https://goreportcard.com/report/github.com/reportportal/goRP) +[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/reportportal/goRP/master/LICENSE) +[![Release](https://img.shields.io/github/release/reportportal/goRP.svg)](https://github.com/reportportal/goRP/releases/latest) +[![GitHub Releases Stats of goRP](https://img.shields.io/github/downloads/reportportal/goRP/total.svg?logo=github)](https://somsubhra.github.io/github-release-stats/?username=reportportal&repository=gorP) # goRP + Golang Client and CLI Utility for [ReportPortal](https://reportportal.io) ## Installation +- Via Go Install +```sh +go install github.com/reportportal/goRP@latest +``` +- Via cURL (passing version and arch) +```sh +curl -sL https://github.com/avarabyeu/goRP/releases/download/v5.0.2/goRP_5.0.2_darwin_amd64.tar.gz | tar zx -C . +``` +- Via cURL (latest one) +```sh +curl -s https://api.github.com/repos/reportportal/goRP/releases/latest | \ + jq -r '.assets[] | select(.name | contains ("tar.gz")) | .browser_download_url' | \ + grep "$(uname)_$(arch)" | \ + xargs curl -sL | tar zx -C . +``` ## Usage + ``` gorp [global options] command [command options] [arguments...] COMMANDS: - launch Operations over launches - help, h Shows a list of commands or help for one command + launch Operations over launches + report Reports input to report portal + init Initializes configuration cache + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - -u value, --uuid value Access Token [$GORP_UUID] - -p value, --project value ReportPortal Project Name [$GORP_PROJECT] + --uuid value, -u value Access Token [$GORP_UUID] + --project value, -p value ReportPortal Project Name [$GORP_PROJECT] --host value ReportPortal Server Name - --help, -h show help - --version, -v print the version + --help, -h show help (default: false) + --version, -v print the version (default: false) ``` +### Init command + + NAME: + gorp init - Initializes configuration cache + USAGE: + gorp init [command options] [arguments...] + OPTIONS: + --help, -h show help (default: false) + ### Launch command + ``` USAGE: goRP launch command [command options] [arguments...] COMMANDS: - list List launches + list List launches + merge Merge Launches + help, h Shows a list of commands or help for one command ``` #### List Launches + ``` USAGE: goRP launch list [command options] [arguments...] OPTIONS: - --fn value, --filter-name value Filter Name [$FILTER_NAME] - -f value, --filter value Filter [$Filter] + --filter-name value, --fn value Filter Name [$FILTER_NAME] + --filter value, -f value Filter [$Filter] + --help, -h show help (default: false) +``` + +### Report command + + NAME: + goRP report - Reports input to report portal + USAGE: + goRP report command [command options] [arguments...] + COMMANDS: + test2json Input format: test2json + help, h Shows a list of commands or help for one command + OPTIONS: + --help, -h show help (default: false) + + +## Using as Golang Test Results Agent +Run tests with JSON output +``` +go test -json ./... > results.txt +``` +Report The results +``` +gorp report test2json -f results.txt +``` +Report directly from go test output ``` +go test -json ./... | gorp report test2json +``` \ No newline at end of file diff --git a/cli/report.go b/cli/report.go deleted file mode 100644 index 4044d9a..0000000 --- a/cli/report.go +++ /dev/null @@ -1,324 +0,0 @@ -package cli - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - - "github.com/avarabyeu/goRP/gorp" -) - -const logsBatchSize = 10 - -var ( - reportCommand = &cli.Command{ - Name: "report", - Usage: "Reports input to report portal", - Subcommands: cli.Commands{reportTest2JsonCommand}, - } - - reportTest2JsonCommand = &cli.Command{ - Name: "test2json", - Usage: "Input format: test2json", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: "File Name", - EnvVars: []string{"FILE"}, - }, - &cli.StringFlag{ - Name: "launchName", - Aliases: []string{"ln"}, - Usage: "Launch Name", - EnvVars: []string{"LAUNCH_NAME"}, - Value: "gorp launch", - }, - }, - Action: reportTest2json, - } -) - -func reportTest2json(c *cli.Context) error { - rpClient, err := buildClient(c) - if err != nil { - return err - } - input := make(chan *testEvent) - - // run in separate goroutine - launchNameArg := c.String("launchName") - rep := newReporter(rpClient, launchNameArg, input) - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - rep.receive() - }() - defer wg.Wait() - - defer close(input) - - var reader io.Reader - if fileName := c.String("file"); fileName != "" { - f, fErr := os.Open(filepath.Clean(fileName)) - if fErr != nil { - return fErr - } - defer func() { - if cErr := f.Close(); cErr != nil { - logrus.Error(cErr) - } - }() - reader = f - } else { - reader = os.Stdin - } - - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - data := scanner.Text() - - var ev testEvent - if err := json.Unmarshal([]byte(data), &ev); err != nil { - logrus.Error(err) - return err - } - input <- &ev - } - return nil -} - -type testEvent struct { - Time time.Time // encodes as an RFC3339-format string - Action string - Package string - Test string - Elapsed float64 // seconds - Output string -} - -type reporter struct { - input <-chan *testEvent - client *gorp.Client - launchName string - launchID string - launchOnce sync.Once - tests map[string]string - suites map[string]string - logs []*gorp.SaveLogRQ - logsBatchSize int - waitQueue sync.WaitGroup -} - -func newReporter(client *gorp.Client, launchName string, input <-chan *testEvent) *reporter { - return &reporter{ - input: input, - launchName: launchName, - client: client, - launchOnce: sync.Once{}, - tests: map[string]string{}, - suites: map[string]string{}, - logs: []*gorp.SaveLogRQ{}, - logsBatchSize: logsBatchSize, - } -} - -func (r *reporter) receive() { - for ev := range r.input { - var err error - r.launchOnce.Do(func() { - if err = r.startLaunch(); err != nil { - logrus.Error(err) - } - }) - - switch ev.Action { - case "run": - _, err = r.startTest(ev) - case "output": - r.log(ev) - case "pass": - err = r.finish(ev, gorp.Statuses.Passed) - case "fail": - err = r.finish(ev, gorp.Statuses.Failed) - } - if err != nil { - logrus.Fatal(err) - } - } - // make sure we flush all logs that are left - r.flushLogs(true) - // wait for requests to get sent - r.waitQueue.Wait() - - if r.launchID != "" { - if err := r.finishLaunch(gorp.Statuses.Passed); err != nil { - logrus.Fatal(err) - } - } -} - -func (r *reporter) startSuite(ev *testEvent) (string, error) { - rs, err := r.client.StartTest(&gorp.StartTestRQ{ - StartRQ: gorp.StartRQ{ - Name: ev.Package, - StartTime: gorp.NewTimestamp(time.Now()), - }, - LaunchID: r.launchID, - HasStats: false, - Type: gorp.TestItemTypes.Suite, - Retry: false, - }) - if err != nil { - return "", err - } - r.suites[ev.Package] = rs.ID - return rs.ID, nil -} - -func (r *reporter) startTest(ev *testEvent) (string, error) { - testID := r.getTestName(ev) - parentID, found := r.suites[ev.Package] - if !found { - var err error - parentID, err = r.startSuite(ev) - if err != nil { - return "", err - } - } - rs, err := r.client.StartChildTest(parentID, &gorp.StartTestRQ{ - StartRQ: gorp.StartRQ{ - Name: ev.Test, - StartTime: gorp.NewTimestamp(time.Now()), - }, - LaunchID: r.launchID, - HasStats: true, - UniqueID: testID, - CodeRef: testID, - TestCaseID: testID, - Type: gorp.TestItemTypes.Test, - Retry: false, - }) - if err != nil { - return "", err - } - r.tests[testID] = rs.ID - return rs.ID, nil -} - -func (r *reporter) log(ev *testEvent) { - if ev.Output == "" { - return - } - testName := r.getTestName(ev) - testID := r.tests[testName] - - // if output starts from tab - if strings.HasPrefix(strings.TrimLeft(ev.Output, " "), "\t") && len(r.logs) > 0 { - lastLog := r.logs[len(r.logs)-1] - lastLog.Message = lastLog.Message + "\n" + ev.Output - lastLog.Level = gorp.LogLevelError - return - } - - rq := &gorp.SaveLogRQ{ - ItemID: testID, - LaunchUUID: r.launchID, - Level: gorp.LogLevelInfo, - LogTime: gorp.NewTimestamp(time.Now()), - Message: ev.Output, - } - r.logs = append(r.logs, rq) - r.flushLogs(false) -} - -func (r *reporter) flushLogs(force bool) { - if force || (len(r.logs) >= r.logsBatchSize) { - batch := r.logs - r.waitQueue.Add(1) - go func(logs []*gorp.SaveLogRQ) { - defer r.waitQueue.Done() - - if _, err := r.client.SaveLogs(logs...); err != nil { - logrus.Errorf("unable to report logs: %v. Batch len: %d", err, len(logs)) - } - }(batch) - r.logs = []*gorp.SaveLogRQ{} - } -} - -func (r *reporter) getTestName(ev *testEvent) string { - return fmt.Sprintf("%s/%s", ev.Package, ev.Test) -} - -func (r *reporter) startLaunch() error { - var launch *gorp.EntryCreatedRS - launch, err := r.client.StartLaunch(&gorp.StartLaunchRQ{ - StartRQ: gorp.StartRQ{ - Name: r.launchName, - StartTime: gorp.NewTimestamp(time.Now()), - }, - Mode: gorp.LaunchModes.Default, - }) - if err != nil { - return err - } - r.launchID = launch.ID - return err -} - -func (r *reporter) finishLaunch(status gorp.Status) error { - _, err := r.client.FinishLaunch(r.launchID, &gorp.FinishExecutionRQ{ - Status: status, - EndTime: gorp.NewTimestamp(time.Now()), - }) - return err -} - -func (r *reporter) finishTest(ev *testEvent, status gorp.Status) error { - testName := r.getTestName(ev) - testID := r.tests[testName] - - _, err := r.client.FinishTest(testID, &gorp.FinishTestRQ{ - FinishExecutionRQ: gorp.FinishExecutionRQ{ - EndTime: gorp.NewTimestamp(time.Now()), - Status: status, - }, - LaunchUUID: r.launchID, - }) - return err -} - -func (r *reporter) finish(ev *testEvent, status gorp.Status) error { - var err error - if ev.Test == "" { - err = r.finishSuite(ev, status) - } else { - err = r.finishTest(ev, status) - } - return err -} - -func (r *reporter) finishSuite(ev *testEvent, status gorp.Status) error { - suiteID := r.suites[ev.Package] - - _, err := r.client.FinishTest(suiteID, &gorp.FinishTestRQ{ - FinishExecutionRQ: gorp.FinishExecutionRQ{ - EndTime: gorp.NewTimestamp(time.Now()), - Status: status, - }, - LaunchUUID: r.launchID, - }) - return err -} diff --git a/go.mod b/go.mod index dec4bf3..9f53993 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,23 @@ -module github.com/avarabyeu/goRP/v5 +module github.com/reportportal/goRP/v5 -go 1.13 +go 1.21 require ( - github.com/go-resty/resty/v2 v2.7.0 - github.com/google/uuid v1.3.0 + github.com/go-resty/resty/v2 v2.12.0 + github.com/google/uuid v1.6.0 github.com/manifoldco/promptui v0.9.0 - github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.0 - github.com/urfave/cli/v2 v2.3.0 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.1 +) + +require ( + github.com/chzyer/readline v1.5.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4d3bcea..d4acd3d 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,82 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= -github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= -github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/commands.go b/internal/commands/commands.go similarity index 90% rename from cli/commands.go rename to internal/commands/commands.go index cbe1995..29cade3 100644 --- a/cli/commands.go +++ b/internal/commands/commands.go @@ -1,22 +1,22 @@ -package cli +package commands import ( "encoding/json" "fmt" + "log/slog" "net/url" "os" "github.com/manifoldco/promptui" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "github.com/avarabyeu/goRP/v5/gorp" + "github.com/reportportal/goRP/v5/pkg/gorp" ) type config struct { - UUID string - Project string - Host string + UUID string `json:"uuid"` + Project string `json:"project"` + Host string `json:"host"` } var ( @@ -48,13 +48,14 @@ func initConfiguration(c *cli.Context) error { return nil } } + f, err := os.OpenFile(getConfigFile(), os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return cli.Exit(fmt.Sprintf("Cannot open config file, %s", err), 1) } defer func() { if closeErr := f.Close(); closeErr != nil { - logrus.Error(closeErr) + slog.Default().Error(closeErr.Error()) } }() @@ -96,6 +97,7 @@ func initConfiguration(c *cli.Context) error { return cli.Exit(fmt.Sprintf("Cannot read config file. %s", err), 1) } + //nolint:forbidigo //expected output fmt.Println("Configuration has been successfully saved!") return nil diff --git a/cli/launch.go b/internal/commands/launch.go similarity index 87% rename from cli/launch.go rename to internal/commands/launch.go index 1bf86da..adab109 100644 --- a/cli/launch.go +++ b/internal/commands/launch.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "errors" @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v2" - "github.com/avarabyeu/goRP/v5/gorp" + gorp2 "github.com/reportportal/goRP/v5/pkg/gorp" ) var ( @@ -88,15 +88,17 @@ func mergeLaunches(c *cli.Context) error { if err != nil { return err } - rq := &gorp.MergeLaunchesRQ{ + rq := &gorp2.MergeLaunchesRQ{ Name: c.String("name"), - MergeType: gorp.MergeType(c.String("type")), + MergeType: gorp2.MergeType(c.String("type")), Launches: ids, } launchResource, err := rpClient.MergeLaunches(rq) if err != nil { return fmt.Errorf("unable to merge launches: %w", err) } + + //nolint:forbidigo //expected output fmt.Println(launchResource.ID) return nil @@ -108,7 +110,7 @@ func listLaunches(c *cli.Context) error { return err } - var launches *gorp.LaunchPage + var launches *gorp2.LaunchPage if filters := c.StringSlice("filter"); len(filters) > 0 { filter := strings.Join(filters, "&") @@ -122,6 +124,7 @@ func listLaunches(c *cli.Context) error { return err } + //nolint:forbidigo //expected output for _, launch := range launches.Content { fmt.Printf("%d #%d \"%s\"\n", launch.ID, launch.Number, launch.Name) } @@ -129,12 +132,12 @@ func listLaunches(c *cli.Context) error { return nil } -func getMergeIDs(c *cli.Context, rpClient *gorp.Client) ([]int, error) { +func getMergeIDs(c *cli.Context, rpClient *gorp2.Client) ([]int, error) { if ids := c.IntSlice("ids"); len(ids) > 0 { return ids, nil } - var launches *gorp.LaunchPage + var launches *gorp2.LaunchPage var err error filter := c.String("filter") @@ -148,7 +151,7 @@ func getMergeIDs(c *cli.Context, rpClient *gorp.Client) ([]int, error) { return nil, errors.New("no either IDs or filter provided") } if err != nil { - return nil, fmt.Errorf("unable to find launches by filter: %s", err.Error()) + return nil, fmt.Errorf("unable to find launches by filter: %w", err) } ids := make([]int, len(launches.Content)) diff --git a/internal/commands/report.go b/internal/commands/report.go new file mode 100644 index 0000000..961a53d --- /dev/null +++ b/internal/commands/report.go @@ -0,0 +1,373 @@ +package commands + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/urfave/cli/v2" + + gorp2 "github.com/reportportal/goRP/v5/pkg/gorp" +) + +const logsBatchSize = 10 + +var ( + reportCommand = &cli.Command{ + Name: "report", + Usage: "Reports input to report portal", + Subcommands: cli.Commands{reportTest2JsonCommand}, + } + + reportTest2JsonCommand = &cli.Command{ + Name: "test2json", + Usage: "Input format: test2json", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "File Name", + EnvVars: []string{"FILE"}, + }, + &cli.StringFlag{ + Name: "launchName", + Aliases: []string{"ln"}, + Usage: "Launch Name", + EnvVars: []string{"LAUNCH_NAME"}, + Value: "gorp launch", + }, + &cli.StringSliceFlag{ + Name: "attr", + Aliases: []string{"a"}, + Usage: "Launch attribute with format 'key:value'. Omitting a ':' separator will tag the launch with the value.", + }, + }, + Action: reportTest2json, + } +) + +//nolint:nonamedreturns // for readability +func reportTest2json(c *cli.Context) (err error) { + rpClient, err := buildClient(c) + if err != nil { + return err + } + input := make(chan *testEvent) + + // run in separate goroutine + launchNameArg := c.String("launchName") + attrArgs := c.StringSlice("attr") + rep := newReporter(rpClient, launchNameArg, input, attrArgs...) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err = rep.receive() + }() + // wait for report to complete + defer wg.Wait() + + defer close(input) + + var reader io.Reader + if fileName := c.String("file"); fileName != "" { + f, fErr := os.Open(filepath.Clean(fileName)) + if fErr != nil { + return fErr + } + defer func() { + if cErr := f.Close(); cErr != nil { + slog.Error(cErr.Error()) + } + }() + reader = f + } else { + reader = os.Stdin + } + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + data := scanner.Text() + + var ev testEvent + if err := json.Unmarshal([]byte(data), &ev); err != nil { + slog.Default().Error(err.Error()) + return err + } + input <- &ev + } + return nil +} + +type testEvent struct { + Time time.Time `json:"time"` // encodes as an RFC3339-format string + Action string `json:"action"` + Package string `json:"package"` + Test string `json:"test"` + Elapsed float64 `json:"elapsed"` // seconds + Output string `json:"output"` +} + +type reporter struct { + input <-chan *testEvent + client *gorp2.Client + launchName string + launchID string + launchOnce sync.Once + launchAttributes []*gorp2.Attribute + tests map[string]string + suites map[string]string + logs []*gorp2.SaveLogRQ + logsBatchSize int + waitQueue sync.WaitGroup +} + +func newReporter(client *gorp2.Client, launchName string, input <-chan *testEvent, launchAttrArgs ...string) *reporter { + launchAttributes := make([]*gorp2.Attribute, 0, len(launchAttrArgs)) + for _, attr := range launchAttrArgs { + // Separate the key:value pair. If `:` is not present, the entire string is considered the value and an empty key is used + var p gorp2.Parameter + if key, value, ok := strings.Cut(attr, ":"); ok { + p.Key = key + p.Value = value + } else { + p.Value = attr + } + launchAttributes = append(launchAttributes, &gorp2.Attribute{ + Parameter: p, + System: false, + }) + } + + return &reporter{ + input: input, + launchName: launchName, + launchAttributes: launchAttributes, + client: client, + launchOnce: sync.Once{}, + tests: map[string]string{}, + suites: map[string]string{}, + logs: []*gorp2.SaveLogRQ{}, + logsBatchSize: logsBatchSize, + } +} + +func (r *reporter) reportEvent(ev *testEvent) error { + var err error + switch ev.Action { + case "run": + _, err = r.startTest(ev) + case "output": + r.log(ev) + case "pass": + err = r.finish(ev, gorp2.Statuses.Passed) + case "fail": + err = r.finish(ev, gorp2.Statuses.Failed) + } + return err +} + +func (r *reporter) receive() error { + prevEventTime := time.Now() + for ev := range r.input { + var err error + startTime := ev.Time + + // start launch once + // when first event comes + r.launchOnce.Do(func() { + if err = r.startLaunch(startTime); err != nil { + slog.Error(err.Error()) + } + }) + if err != nil { + return err + } + + // report event to ReportPortal + err = r.reportEvent(ev) + if err != nil { + return err + } + + // remember last's event time + // for RP's finishLaunch + prevEventTime = ev.Time + } + + // make sure we flush all logs that are left + r.flushLogs(true) + // wait for requests to get sent + r.waitQueue.Wait() + + // finish launch of started + if r.launchID != "" { + if err := r.finishLaunch(gorp2.Statuses.Passed, prevEventTime); err != nil { + return err + } + } + return nil +} + +func (r *reporter) startSuite(ev *testEvent) (string, error) { + rs, err := r.client.StartTest(&gorp2.StartTestRQ{ + StartRQ: gorp2.StartRQ{ + Name: ev.Package, + StartTime: gorp2.NewTimestamp(ev.Time), + }, + LaunchID: r.launchID, + HasStats: false, + Type: gorp2.TestItemTypes.Suite, + Retry: false, + }) + if err != nil { + return "", err + } + r.suites[ev.Package] = rs.ID + return rs.ID, nil +} + +func (r *reporter) startTest(ev *testEvent) (string, error) { + testID := r.getTestName(ev) + parentID, found := r.suites[ev.Package] + if !found { + var err error + parentID, err = r.startSuite(ev) + if err != nil { + return "", err + } + } + rs, err := r.client.StartChildTest(parentID, &gorp2.StartTestRQ{ + StartRQ: gorp2.StartRQ{ + Name: ev.Test, + StartTime: gorp2.NewTimestamp(ev.Time), + }, + LaunchID: r.launchID, + HasStats: true, + UniqueID: testID, + CodeRef: testID, + TestCaseID: testID, + Type: gorp2.TestItemTypes.Test, + Retry: false, + }) + if err != nil { + return "", err + } + r.tests[testID] = rs.ID + return rs.ID, nil +} + +func (r *reporter) log(ev *testEvent) { + if ev.Output == "" { + return + } + testName := r.getTestName(ev) + testID := r.tests[testName] + + // if output starts from tab + if strings.HasPrefix(strings.TrimLeft(ev.Output, " "), "\t") && len(r.logs) > 0 { + lastLog := r.logs[len(r.logs)-1] + lastLog.Message = lastLog.Message + "\n" + ev.Output + lastLog.Level = gorp2.LogLevelError + return + } + + rq := &gorp2.SaveLogRQ{ + ItemID: testID, + LaunchUUID: r.launchID, + Level: gorp2.LogLevelInfo, + LogTime: gorp2.NewTimestamp(ev.Time), + Message: ev.Output, + } + r.logs = append(r.logs, rq) + r.flushLogs(false) +} + +func (r *reporter) flushLogs(force bool) { + if force || (len(r.logs) >= r.logsBatchSize) { + batch := r.logs + r.waitQueue.Add(1) + go func(logs []*gorp2.SaveLogRQ) { + defer r.waitQueue.Done() + + if _, err := r.client.SaveLogs(logs...); err != nil { + slog.Error("unable to report logs", "error", err, "batch_length", len(logs)) + } + }(batch) + r.logs = []*gorp2.SaveLogRQ{} + } +} + +func (r *reporter) getTestName(ev *testEvent) string { + return fmt.Sprintf("%s/%s", ev.Package, ev.Test) +} + +func (r *reporter) startLaunch(startTime time.Time) error { + var launch *gorp2.EntryCreatedRS + launch, err := r.client.StartLaunch(&gorp2.StartLaunchRQ{ + StartRQ: gorp2.StartRQ{ + Name: r.launchName, + StartTime: gorp2.NewTimestamp(startTime), + Attributes: r.launchAttributes, + }, + Mode: gorp2.LaunchModes.Default, + }) + if err != nil { + return err + } + r.launchID = launch.ID + return err +} + +func (r *reporter) finishLaunch(status gorp2.Status, endTime time.Time) error { + _, err := r.client.FinishLaunch(r.launchID, &gorp2.FinishExecutionRQ{ + Status: status, + EndTime: gorp2.NewTimestamp(endTime), + }) + return err +} + +func (r *reporter) finishTest(ev *testEvent, status gorp2.Status) error { + testName := r.getTestName(ev) + testID := r.tests[testName] + + _, err := r.client.FinishTest(testID, &gorp2.FinishTestRQ{ + FinishExecutionRQ: gorp2.FinishExecutionRQ{ + EndTime: gorp2.NewTimestamp(ev.Time), + Status: status, + }, + LaunchUUID: r.launchID, + }) + return err +} + +func (r *reporter) finish(ev *testEvent, status gorp2.Status) error { + var err error + if ev.Test == "" { + err = r.finishSuite(ev, status) + } else { + err = r.finishTest(ev, status) + } + return err +} + +func (r *reporter) finishSuite(ev *testEvent, status gorp2.Status) error { + suiteID := r.suites[ev.Package] + + _, err := r.client.FinishTest(suiteID, &gorp2.FinishTestRQ{ + FinishExecutionRQ: gorp2.FinishExecutionRQ{ + EndTime: gorp2.NewTimestamp(ev.Time), + Status: status, + }, + LaunchUUID: r.launchID, + }) + return err +} diff --git a/cli/util.go b/internal/commands/util.go similarity index 98% rename from cli/util.go rename to internal/commands/util.go index 89624af..ef880af 100644 --- a/cli/util.go +++ b/internal/commands/util.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "errors" diff --git a/cli/util_test.go b/internal/commands/util_test.go similarity index 53% rename from cli/util_test.go rename to internal/commands/util_test.go index 0d75286..947f97a 100644 --- a/cli/util_test.go +++ b/internal/commands/util_test.go @@ -1,4 +1,4 @@ -package cli +package commands import ( "testing" @@ -7,17 +7,21 @@ import ( ) func TestUnderstandsYes(t *testing.T) { - assert.Equal(t, true, answerYes("yes")) + t.Parallel() + assert.True(t, answerYes("yes")) } func TestUnderstandsYesUpper(t *testing.T) { - assert.Equal(t, true, answerYes("YES")) + t.Parallel() + assert.True(t, answerYes("YES")) } func TestEmptyAnswer(t *testing.T) { - assert.Equal(t, false, answerYes("")) + t.Parallel() + assert.False(t, answerYes("")) } func TestUnderstandsNo(t *testing.T) { - assert.Equal(t, false, answerYes("no")) + t.Parallel() + assert.False(t, answerYes("no")) } diff --git a/main.go b/main.go index c065215..43fb1e7 100644 --- a/main.go +++ b/main.go @@ -3,26 +3,31 @@ package main import ( "fmt" "log" + "log/slog" "os" "github.com/urfave/cli/v2" - rp "github.com/avarabyeu/goRP/v5/cli" + rp "github.com/reportportal/goRP/v5/internal/commands" ) var ( - version = "" - buildDate = "" + version = "dev" + date = "unknown" ) func main() { - log.SetFlags(0) - log.SetOutput(os.Stdout) + slog.SetDefault(slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelError, + AddSource: true, + }), + )) app := cli.NewApp() app.Name = "goRP" app.Usage = "ReportPortal CLI Client" - app.Version = fmt.Sprintf("%s (%s)", version, buildDate) + app.Version = fmt.Sprintf("%s (%s)", version, date) app.Authors = []*cli.Author{{ Name: "Andrei Varabyeu", Email: "andrei.varabyeu@gmail.com", diff --git a/gorp/client.go b/pkg/gorp/client.go similarity index 85% rename from gorp/client.go rename to pkg/gorp/client.go index db94afa..2cb6423 100644 --- a/gorp/client.go +++ b/pkg/gorp/client.go @@ -3,8 +3,8 @@ package gorp import ( "bytes" "encoding/json" + "errors" "fmt" - "os" "time" "github.com/go-resty/resty/v2" @@ -19,14 +19,14 @@ type Client struct { // NewClient creates new instance of Client // host - server hostname // project - name of the project -// uuid - User Token (see user profile page) -func NewClient(host, project, uuid string) *Client { +// apiKey - User Token (see user profile page) +func NewClient(host, project, apiKey string) *Client { http := resty.New(). // SetDebug(true). SetBaseURL(host). - SetAuthToken(uuid). + SetAuthToken(apiKey). OnAfterResponse(func(client *resty.Client, rs *resty.Response) error { - // nolint:gomnd // 4xx errors + //nolint:gomnd // 4xx errors if (rs.StatusCode() / 100) >= 4 { return fmt.Errorf("status code error: %d\n%s", rs.StatusCode(), rs.String()) } @@ -179,7 +179,7 @@ func (c *Client) finishTest(id string, body interface{}) (*MsgRS, error) { // SaveLog attaches log in RP func (c *Client) SaveLog(log *SaveLogRQ) (*EntryCreatedRS, error) { var rs EntryCreatedRS - _, err := c.http.SetDebug(true).R(). + _, err := c.http.R(). SetPathParams(map[string]string{ "project": c.project, }). @@ -194,8 +194,30 @@ func (c *Client) SaveLogs(logs ...*SaveLogRQ) (*EntryCreatedRS, error) { return c.SaveLogMultipart(logs, nil) } -// SaveLogMultipart attaches log in RP -func (c *Client) SaveLogMultipart(log []*SaveLogRQ, files map[string]*os.File) (*EntryCreatedRS, error) { +// SaveLogMultipart saves a batch of logs in RP, along with any associated files (if any). +// +// Example usage: +// +// f, _ := os.Open("someFile.txt") +// +// logs := []*SaveLogRQ{{ +// File: FileAttachment{ +// // note that this value must present in 'files' map as key (see below) +// Name: "fileAttachment.txt", +// }, +// LaunchUUID: launchID, +// ItemID: itemID, +// Level: gorp.LogLevelError, +// LogTime: NewTimestamp(time.Now()), +// Message: "Important message!", +// }} +// +// files := map[string]*os.File{ +// "fileAttachment.txt": f, // key must match the FileAttachment.Name field +// } +// +// resp, err := client.SaveLogMultipart(log, files) +func (c *Client) SaveLogMultipart(log []*SaveLogRQ, files []Multipart) (*EntryCreatedRS, error) { var bodyBuf bytes.Buffer err := json.NewEncoder(&bodyBuf).Encode(log) if err != nil { @@ -211,14 +233,16 @@ func (c *Client) SaveLogMultipart(log []*SaveLogRQ, files map[string]*os.File) ( rq.SetMultipartField("json_request_part", "", "application/json", &bodyBuf) // BINARY PART - for k, v := range files { - if v == nil { - return nil, fmt.Errorf("no file for [%s] is provided", k) + for _, v := range files { + fileName, contentType, reader, lErr := v.Load() + if lErr != nil { + return nil, fmt.Errorf("unable to read multipart: %w", lErr) } - if _, sErr := os.Stat(v.Name()); os.IsNotExist(sErr) { - return nil, fmt.Errorf("file %s does not exist", v.Name()) + if fileName == "" { + return nil, errors.New("no file name is provided") } - rq.SetMultipartField(k, k, "", v) + + rq.SetMultipartField("file", fileName, contentType, reader) } var rs EntryCreatedRS diff --git a/gorp/client_test.go b/pkg/gorp/client_test.go similarity index 95% rename from gorp/client_test.go rename to pkg/gorp/client_test.go index 1bd6a17..ba6c31b 100644 --- a/gorp/client_test.go +++ b/pkg/gorp/client_test.go @@ -9,6 +9,7 @@ import ( ) func TestCreateRPClient(t *testing.T) { + t.Parallel() client := NewClient("http://host.com", "prj", "uuid") assert.Equal(t, "prj", client.project) @@ -17,6 +18,7 @@ func TestCreateRPClient(t *testing.T) { } func TestHandleErrors(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) diff --git a/gorp/example_test.go b/pkg/gorp/example_test.go similarity index 63% rename from gorp/example_test.go rename to pkg/gorp/example_test.go index 6b50818..7e3cb33 100644 --- a/gorp/example_test.go +++ b/pkg/gorp/example_test.go @@ -9,7 +9,8 @@ import ( ) func ExampleClient() { - client := NewClient("xxx", "xxx", "xxx") + client := NewClient("https://reportportal.epam.com", + "andrei_varabyeu_personal", "gorp-test_MCQZajD8S_ClBSGo-Q6X7KtzBKjFGiw0PPf6oB0-nexU1MSRMMcO_4_Sn4YGTBtk") launchUUID := uuid.New() launch, err := client.StartLaunch(&StartLaunchRQ{ @@ -47,13 +48,34 @@ func ExampleClient() { }) checkErr(err, "unable to save log") - file, _ := os.Open("../go.mod") - _, err = client.SaveLogMultipart([]*SaveLogRQ{{ - LaunchUUID: launchUUID.String(), - ItemID: testUUID.String(), - Level: LogLevelInfo, - Message: "Log with binary", - }}, map[string]*os.File{"go.mod": file}) + file1, err := os.Open("../../go.mod") + checkErr(err, "unable to read file") + file2, err := os.Open("../../go.sum") + checkErr(err, "unable to read file") + + _, err = client.SaveLogMultipart([]*SaveLogRQ{ + { + LaunchUUID: launchUUID.String(), + ItemID: testUUID.String(), + Level: LogLevelInfo, + Message: "Log with binary one", + Attachment: Attachment{ + Name: "go.mod", + }, + }, + { + LaunchUUID: launchUUID.String(), + ItemID: testUUID.String(), + Level: LogLevelInfo, + Message: "Log with binary two", + Attachment: Attachment{ + Name: "go.sum", + }, + }, + }, []Multipart{ + &FileMultipart{File: file1}, + &ReaderMultipart{ContentType: "text/plain", FileName: file2.Name(), Reader: file2}, + }) checkErr(err, "unable to save log multipart") @@ -71,6 +93,8 @@ func ExampleClient() { EndTime: Timestamp{time.Now()}, }) checkErr(err, "unable to finish launch") + + // Output: } func checkErr(err error, msg string) { diff --git a/gorp/model_api.go b/pkg/gorp/model_api.go similarity index 98% rename from gorp/model_api.go rename to pkg/gorp/model_api.go index 62004e4..267c7ff 100644 --- a/gorp/model_api.go +++ b/pkg/gorp/model_api.go @@ -37,7 +37,7 @@ type ( ApproximateDuration float32 `json:"approximateDuration,omitempty"` HasRetries bool `json:"hasRetries,omitempty"` Statistics *Statistics `json:"statistics,omitempty"` - Analyzers []string `json:"analysing,omitempty"` // nolint:misspell // defined as described on server end + Analyzers []string `json:"analysing,omitempty"` //nolint:misspell // defined as described on server end } // FilterResource - GET Filter response model diff --git a/gorp/model_api_test.go b/pkg/gorp/model_api_test.go similarity index 91% rename from gorp/model_api_test.go rename to pkg/gorp/model_api_test.go index 51f8520..3d6d250 100644 --- a/gorp/model_api_test.go +++ b/pkg/gorp/model_api_test.go @@ -6,15 +6,17 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUnixTimeDeserialization(t *testing.T) { + t.Parallel() const jsonStr = `"1512114178671"` const expTime = "2017-12-01T07:42:59+00:00" var unitTime Timestamp err := json.Unmarshal([]byte(jsonStr), &unitTime) - assert.NoError(t, err) + require.NoError(t, err) unitTime = Timestamp{unitTime.Truncate(1 * time.Minute)} @@ -25,16 +27,18 @@ func TestUnixTimeDeserialization(t *testing.T) { } func TestUnixTimeSerialization(t *testing.T) { + t.Parallel() const jsonStr = `1512114179000` const expTime = "2017-12-01T07:42:59+00:00" d, _ := time.Parse(time.RFC3339, expTime) bytes, err := json.Marshal(&Timestamp{d}) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, jsonStr, string(bytes)) } func TestErrOnIncorrectTime(t *testing.T) { + t.Parallel() const jsonStr = `"hello-world"` var unitTime Timestamp @@ -43,11 +47,13 @@ func TestErrOnIncorrectTime(t *testing.T) { } func TestDirectionConverter(t *testing.T) { + t.Parallel() assert.Equal(t, "ASC", directionToStr(true)) assert.Equal(t, "DESC", directionToStr(false)) } func TestFiltersConverter(t *testing.T) { + t.Parallel() fp := ConvertToFilterParams(&FilterResource{ Entities: []*FilterEntity{ { diff --git a/gorp/model_enums.go b/pkg/gorp/model_enums.go similarity index 96% rename from gorp/model_enums.go rename to pkg/gorp/model_enums.go index 45c31fa..480570c 100644 --- a/gorp/model_enums.go +++ b/pkg/gorp/model_enums.go @@ -70,7 +70,7 @@ var Statuses = statusValuesType{ Stopped: "STOPPED", Skipped: "SKIPPED", Interrupted: "INTERRUPTED", - Canceled: "CANCELLED", // nolint:misspell // defined as described on server end + Canceled: "CANCELLED", //nolint:misspell // defined as described on server end Info: "INFO", Warn: "WARN", } diff --git a/gorp/model_reporting.go b/pkg/gorp/model_reporting.go similarity index 87% rename from gorp/model_reporting.go rename to pkg/gorp/model_reporting.go index 71c1453..f64f25b 100644 --- a/gorp/model_reporting.go +++ b/pkg/gorp/model_reporting.go @@ -26,13 +26,14 @@ type ( RetryOf string `json:"retryOf,omitempty"` } - // SaveLogRQ payload representation. Without attaches. + // SaveLogRQ payload representation. SaveLogRQ struct { - LaunchUUID string `json:"launchUuid,omitempty"` - ItemID string `json:"itemUuid,omitempty"` - LogTime Timestamp `json:"time,omitempty"` - Message string `json:"message,omitempty"` - Level string `json:"level,omitempty"` + LaunchUUID string `json:"launchUuid,omitempty"` + ItemID string `json:"itemUuid,omitempty"` + LogTime Timestamp `json:"time,omitempty"` + Message string `json:"message,omitempty"` + Level string `json:"level,omitempty"` + Attachment Attachment `json:"file,omitempty"` } // StartTestRQ payload representation @@ -102,6 +103,11 @@ type ( Timestamp struct { time.Time } + + // Attachment represents file attachment in log entries + Attachment struct { + Name string `json:"name,omitempty"` + } ) // UnmarshalJSON converts Epoch milliseconds (timestamp) to appropriate object diff --git a/pkg/gorp/multipart.go b/pkg/gorp/multipart.go new file mode 100644 index 0000000..1c37fcf --- /dev/null +++ b/pkg/gorp/multipart.go @@ -0,0 +1,52 @@ +package gorp + +import ( + "errors" + "fmt" + "io" + "mime" + "os" + "path/filepath" +) + +// Multipart is an interface that allows to pass over different types +// of multipart data sources +type Multipart interface { + // Load loads multipart data + Load() (fileName, contentType string, reader io.Reader, err error) +} + +// FileMultipart is a multipart content in form of file +type FileMultipart struct { + *os.File +} + +//nolint:nonamedreturns // for readability +func (fm *FileMultipart) Load() (fileName, contentType string, reader io.Reader, err error) { + if fm.File == nil { + return "", "", nil, errors.New("file shouldn't be nil") + } + fName := fm.File.Name() + if _, sErr := os.Stat(fName); os.IsNotExist(sErr) { + return "", "", nil, fmt.Errorf("file %s does not exist", fName) + } + contentType = mime.TypeByExtension(filepath.Ext(fName)) + if contentType == "" { + contentType = "application/octet-stream" + } + return filepath.Base(fName), contentType, fm.File, nil +} + +// ReaderMultipart is a multipart content in form of io.Reader +type ReaderMultipart struct { + FileName, ContentType string + io.Reader +} + +//nolint:nonamedreturns // for readability +func (fm *ReaderMultipart) Load() (fileName, contentType string, reader io.Reader, err error) { + if fm.FileName == "" { + return "", "", nil, errors.New("multipart filename shouldn't be nil") + } + return fm.FileName, fm.ContentType, fm, nil +} diff --git a/gorp/util.go b/pkg/gorp/util.go similarity index 100% rename from gorp/util.go rename to pkg/gorp/util.go diff --git a/util/util.go b/util/util.go deleted file mode 100644 index 3ab4ad1..0000000 --- a/util/util.go +++ /dev/null @@ -1,26 +0,0 @@ -package util - -import ( - "fmt" - "time" - - log "github.com/sirupsen/logrus" -) - -// Retry executes callback func until it executes successfully -func Retry(attempts int, timeout time.Duration, callback func() (interface{}, error)) (interface{}, error) { - var err error - for i := 0; i < attempts; i++ { - var res interface{} - res, err = callback() - if err == nil { - return res, nil - } - log.Warnf("Retry failed with the following error: %v", err) - - <-time.After(timeout) - log.Infof("Retrying... Attempt: %d. Left: %d", i+1, attempts-1-i) - } - - return nil, fmt.Errorf("after %d attempts, last error: %s", attempts, err) -}