From 9d5c1d4e8630a28a5d0d4a23fd29f15b02d45d75 Mon Sep 17 00:00:00 2001 From: lszucs <44788749+lszucs@users.noreply.github.com> Date: Fri, 30 Aug 2019 15:12:10 +0200 Subject: [PATCH] Tool-253 collect unmappable test results to other tab (#18) * refactor: better funcion name * collect results to 'other' dir if variant unknown * fix: forgot to rename reference * export non android test results to 'other' folder * typo fixes * report and results inputs clarification * added new test case * default report dir pattern update * logging updates, unique Others dir, debug log fix * log fixes * other dir name update * step.yml update, removed unused var * PR updates --- bitrise.yml | 16 +++++-- main.go | 119 ++++++++++++++++++++++++++++------------------ main_test.go | 73 ++++++++++++++++++++++++++++ step.yml | 49 ++++++++++++++++--- testaddon.go | 92 +++++++++++++++++++++++++---------- testaddon_test.go | 65 +++++++++++++++++++------ 6 files changed, 318 insertions(+), 96 deletions(-) create mode 100644 main_test.go diff --git a/bitrise.yml b/bitrise.yml index da0fd47..477d9b9 100755 --- a/bitrise.yml +++ b/bitrise.yml @@ -43,6 +43,8 @@ workflows: - gradlew_path: ./gradlew - path::./: title: Android Unit Test (monorepo projects in source dir) + inputs: + - is_debug: "true" after-run: - check-artifacts @@ -72,6 +74,7 @@ workflows: inputs: - module: app - variant: Debug + - is_debug: "true" after-run: - check-artifacts @@ -95,6 +98,8 @@ workflows: - gradlew_path: ./gradlew - path::./: title: Android Unit Test (android project & mono repo projects in /tmp dir) + inputs: + - is_debug: "true" after-run: - check-artifacts @@ -121,6 +126,8 @@ workflows: - gradlew_path: ./gradlew - path::./: title: Android Unit Test (android project with build.gradle.kts) + inputs: + - is_debug: "true" after-run: - check-artifacts @@ -178,7 +185,7 @@ workflows: - is_create_path: true - script: inputs: - - content: git clone -b no-failures $SAMPLE_REPO_GIT_CLONE_URL . + - content: git clone -b no-failures $SAMPLE_REPO_GIT_CLONE_URL -b code-coverage . - install-missing-android-tools: inputs: - gradlew_path: ./gradlew @@ -195,21 +202,22 @@ workflows: inputs: - module: app - variant: Debug + - is_debug: "true" - script: - title: check exported arftifacts + title: check exported artifacts is_always_run: true inputs: - content: |- #!/usr/bin/env bash set -ex - # check directory and test-info.json existense + # check directory and test-info.json existence if [ $(find ${BITRISE_TEST_DEPLOY_DIR} -regex ".*/app-debug/test-info.json" | grep -c .) -eq 0 ]; then echo "ERROR: ${BITRISE_TEST_DEPLOY_DIR} does not contain app-debug/test-info.json." exit 1 fi - # check result xml existense + # check result xml existence if [ ! $(find ${BITRISE_TEST_DEPLOY_DIR} -regex ".*/app-debug/TEST.*.xml" | grep -c .) -eq 2 ]; then echo "ERROR: ${BITRISE_TEST_DEPLOY_DIR} does not contain all test result XMLs." exit 1 diff --git a/main.go b/main.go index 3b99188..d567b09 100755 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "time" @@ -17,18 +18,19 @@ import ( shellquote "github.com/kballard/go-shellquote" ) -const resultArtifactPathPattern = "*TEST*.xml" - // Configs ... type Configs struct { - ProjectLocation string `env:"project_location,dir"` - ReportPathPattern string `env:"report_path_pattern"` - ResultPathPattern string `env:"result_path_pattern"` - Variant string `env:"variant"` - Module string `env:"module"` - Arguments string `env:"arguments"` - CacheLevel string `env:"cache_level,opt[none,only_deps,all]"` - IsDebug bool `env:"is_debug,opt[true,false]"` + ProjectLocation string `env:"project_location,dir"` + HTMLResultDirPattern string `env:"report_path_pattern"` + XMLResultDirPattern string `env:"result_path_pattern"` + Variant string `env:"variant"` + Module string `env:"module"` + Arguments string `env:"arguments"` + CacheLevel string `env:"cache_level,opt[none,only_deps,all]"` + IsDebug bool `env:"is_debug,opt[true,false]"` + + DeployDir string `env:"BITRISE_DEPLOY_DIR"` + TestResultDir string `env:"BITRISE_TEST_RESULT_DIR"` } func failf(f string, args ...interface{}) { @@ -60,6 +62,14 @@ func getArtifacts(gradleProject gradle.Project, started time.Time, pattern strin return } +func workDirRel(pth string) (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + return filepath.Rel(wd, pth) +} + func exportArtifacts(deployDir string, artifacts []gradle.Artifact) error { for _, artifact := range artifacts { artifact.Name += ".zip" @@ -73,7 +83,12 @@ func exportArtifacts(deployDir string, artifacts []gradle.Artifact) error { artifact.Name = fmt.Sprintf("%s-%s%s", strings.TrimSuffix(artifact.Name, ".zip"), timestamp, ".zip") } - log.Printf(" Export [ %s => $BITRISE_DEPLOY_DIR/%s ]", filepath.Base(artifact.Path), artifact.Name) + src := filepath.Base(artifact.Path) + if rel, err := workDirRel(artifact.Path); err == nil { + src = "./" + rel + } + + log.Printf(" Export [ %s => $BITRISE_DEPLOY_DIR/%s ]", src, artifact.Name) if err := artifact.ExportZIP(deployDir); err != nil { log.Warnf("failed to export artifact (%s), error: %v", artifact.Path, err) @@ -110,25 +125,42 @@ func filterVariants(module, variant string, variantsMap gradle.Variants) (gradle return filteredVariants, nil } -func main() { - var config Configs +func tryExportTestAddonArtifact(artifactPth, outputDir string, lastOtherDirIdx int) int { + dir := getExportDir(artifactPth) - if config.IsDebug { - log.SetEnableDebugLog(true) - log.Debugf("Debug mode enabled") + if dir == OtherDirName { + // start indexing other dir name, to avoid overrideing it + // e.g.: other, other-1, other-2 + lastOtherDirIdx++ + if lastOtherDirIdx > 0 { + dir = dir + "-" + strconv.Itoa(lastOtherDirIdx) + } } + if err := testaddon.ExportArtifact(artifactPth, outputDir, dir); err != nil { + log.Warnf("Failed to export test results for test addon: %s", err) + } else { + src := artifactPth + if rel, err := workDirRel(artifactPth); err == nil { + src = "./" + rel + } + log.Printf(" Export [%s => %s]", src, filepath.Join("$BITRISE_TEST_RESULT_DIR", dir, filepath.Base(artifactPth))) + } + return lastOtherDirIdx +} + +func main() { + var config Configs + if err := stepconf.Parse(&config); err != nil { failf("Couldn't create step config: %v\n", err) } stepconf.Print(config) - - deployDir := os.Getenv("BITRISE_DEPLOY_DIR") - - log.Printf("- Deploy dir: %s", deployDir) fmt.Println() + log.SetEnableDebugLog(config.IsDebug) + gradleProject, err := gradle.NewProject(config.ProjectLocation) if err != nil { failf("Failed to open project, error: %s", err) @@ -182,53 +214,50 @@ func main() { log.Errorf("Test task failed, error: %v", testErr) } fmt.Println() - - log.Infof("Export reports:") + log.Infof("Export HTML results:") fmt.Println() - reports, err := getArtifacts(gradleProject, started, config.ReportPathPattern, true, true) + reports, err := getArtifacts(gradleProject, started, config.HTMLResultDirPattern, true, true) if err != nil { failf("Failed to find reports, error: %v", err) } - if err := exportArtifacts(deployDir, reports); err != nil { + if err := exportArtifacts(config.DeployDir, reports); err != nil { failf("Failed to export reports, error: %v", err) } fmt.Println() - - log.Infof("Export results:") + log.Infof("Export XML results:") fmt.Println() - results, err := getArtifacts(gradleProject, started, config.ResultPathPattern, true, true) + results, err := getArtifacts(gradleProject, started, config.XMLResultDirPattern, true, true) if err != nil { failf("Failed to find results, error: %v", err) } - if err := exportArtifacts(deployDir, results); err != nil { + if err := exportArtifacts(config.DeployDir, results); err != nil { failf("Failed to export results, error: %v", err) } - log.Infof("Export test results for test addon:") - fmt.Println() + if config.TestResultDir != "" { + // Test Addon is turned on + fmt.Println() + log.Infof("Export XML results for test addon:") + fmt.Println() - resultXMLs, err := getArtifacts(gradleProject, started, resultArtifactPathPattern, false, false) - if err != nil { - log.Warnf("Failed to find test result XMLs, error: %s", err) - } else { - if baseDir := os.Getenv("BITRISE_TEST_RESULT_DIR"); baseDir != "" { + xmlResultFilePattern := config.XMLResultDirPattern + if !strings.HasSuffix(xmlResultFilePattern, "*.xml") { + xmlResultFilePattern += "*.xml" + } + + resultXMLs, err := getArtifacts(gradleProject, started, xmlResultFilePattern, false, false) + if err != nil { + log.Warnf("Failed to find test XML test results, error: %s", err) + } else { + lastOtherDirIdx := -1 for _, artifact := range resultXMLs { - uniqueDir, err := getUniqueDir(artifact.Path) - if err != nil { - log.Warnf("Failed to export test results for test addon: cannot get export directory for artifact (%s): %s", err) - continue - } - - if err := testaddon.ExportArtifact(artifact.Path, baseDir, uniqueDir); err != nil { - log.Warnf("Failed to export test results for test addon: %s", err) - } + lastOtherDirIdx = tryExportTestAddonArtifact(artifact.Path, config.TestResultDir, lastOtherDirIdx) } - log.Printf(" Exporting test results to test addon successful [ %s ] ", baseDir) } } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..fa85651 --- /dev/null +++ b/main_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/pathutil" +) + +func Test_tryExportTestAddonArtifact(t *testing.T) { + tmpDir, err := pathutil.NormalizedOSTempDirPath("") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + artifactPth string + outputDir string + lastOtherDirIdx int + + wantIdx int + wantOutputPth string + }{ + { + name: "Exports Local Unit Test result XML file", + artifactPth: filepath.Join(tmpDir, "./app/build/test-results/testDebugUnitTest/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml"), + outputDir: filepath.Join(tmpDir, "1"), + lastOtherDirIdx: 0, + wantIdx: 0, + wantOutputPth: filepath.Join(tmpDir, "1", "app-debug", "TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml"), + }, + { + name: "Exports Jacoco result XML file", + artifactPth: filepath.Join(tmpDir, "./app/build/test-results/jacocoTestReleaseUnitTestReport/jacocoTestReleaseUnitTestReport.xml"), + outputDir: filepath.Join(tmpDir, "2"), + lastOtherDirIdx: 0, + wantIdx: 1, + wantOutputPth: filepath.Join(tmpDir, "2", "other-1", "jacocoTestReleaseUnitTestReport.xml"), + }, + { + name: "Exports Other XML file", + artifactPth: filepath.Join(tmpDir, "./app/build/test-results/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml"), + outputDir: filepath.Join(tmpDir, "3"), + lastOtherDirIdx: 0, + wantIdx: 1, + wantOutputPth: filepath.Join(tmpDir, "3", "other-1", "TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml"), + }, + } + for _, tt := range tests { + dir := filepath.Dir(tt.artifactPth) + if err := os.MkdirAll(dir, 0700); err != nil { + t.Error(err) + continue + } + if _, err := os.Create(tt.artifactPth); err != nil { + t.Error(err) + continue + } + + t.Run(tt.name, func(t *testing.T) { + if got := tryExportTestAddonArtifact(tt.artifactPth, tt.outputDir, tt.lastOtherDirIdx); got != tt.wantIdx { + t.Errorf("tryExportTestAddonArtifact() = %v, want %v", got, tt.wantIdx) + } + if exist, err := pathutil.IsPathExists(tt.wantOutputPth); err != nil { + t.Error(err) + } else if !exist { + t.Errorf("expected output file (%s) does not exist", tt.wantOutputPth) + } + }) + } +} diff --git a/step.yml b/step.yml index ba32f67..a1f90a0 100755 --- a/step.yml +++ b/step.yml @@ -53,19 +53,54 @@ inputs: summary: Extra arguments passed to the gradle task description: Extra arguments passed to the gradle task is_required: false - - report_path_pattern: "*build/reports" + - report_path_pattern: "*build/reports/tests" opts: category: Options - title: Report location pattern - summary: Will find the report dir with the given pattern. - description: Will find the report dir with the given pattern. + title: Local unit test HTML result directory pattern + description: |- + The step will use this pattern to export __Local unit test HTML results__. + The whole HTML results directory will be zipped and moved to the `$BITRISE_DEPLOY_DIR`. + + You need to override this input if you have custom output dir set for Local unit test HTML results. + The pattern needs to be relative to the selected module's directory. + + Example 1: app module and debug variant is selected and the HTML report is generated at: + + - `/app/build/reports/tests/testDebugUnitTest` + + this case use: `*build/reports/tests/testDebugUnitTest` pattern. + + Example 2: app module and NO variant is selected and the HTML reports are generated at: + + - `/app/build/reports/tests/testDebugUnitTest` + - `/app/build/reports/tests/testReleaseUnitTest` + + to export every variant's reports use: `*build/reports/tests` pattern. is_required: true - result_path_pattern: "*build/test-results" opts: category: Options - title: Test results location pattern - summary: Will find the test-results dir with the given pattern. - description: Will find test-results dir with the given pattern. + title: Local unit test XML result directory pattern + description: |- + The step will use this pattern to export __Local unit test XML results__. + The whole XML results directory will be zipped and moved to the `$BITRISE_DEPLOY_DIR` + and the result files will be deployed to the Ship Addon. + + You need to override this input if you have custom output dir set for Local unit test XML results. + The pattern needs to be relative to the selected module's directory. + + Example 1: app module and debug variant is selected and the XML report is generated at: + + - `/app/build/test-results/testDebugUnitTest` + + this case use: `*build/test-results/testDebugUnitTest` pattern. + + Example 2: app module and NO variant is selected and the XML reports are generated at: + + - `/app/build/test-results/testDebugUnitTest` + - `/app/build/test-results/testReleaseUnitTest` + + to export every variant's reports use: `*build/test-results` pattern. is_required: true - cache_level: "only_deps" opts: diff --git a/testaddon.go b/testaddon.go index c62708d..f38c3ee 100644 --- a/testaddon.go +++ b/testaddon.go @@ -2,47 +2,87 @@ package main import ( "fmt" + "path/filepath" "strings" "unicode" - - "github.com/bitrise-io/go-utils/log" ) -// getUniqueDir returns the unique subdirectory inside the test addon export diroctory for a given artifact. -func getUniqueDir(path string) (string, error) { - log.Debugf("getUniqueDir(%s)", path) - parts := strings.Split(path, "/") - i := len(parts) - 1 - for i > 0 && parts[i] != "test-results" { - i-- +// OtherDirName is a directory name of non Android Unit test results +const OtherDirName = "other" + +func getExportDir(artifactPath string) string { + dir, err := getVariantDir(artifactPath) + if err != nil { + return OtherDirName } - if i == 0 { - return "", fmt.Errorf("path (%s) does not contain 'test-results' folder", path) + return dir +} + +// indexOfTestResultsDirName finds the index of "test-results" in the given path parts, othervise returns -1 +func indexOfTestResultsDirName(pthParts []string) int { + // example: ./app/build/test-results/testDebugUnitTest/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml + for i, part := range pthParts { + if part == "test-results" { + return i + } + } + return -1 +} + +func lowercaseFirstLetter(str string) string { + for i, v := range str { + return string(unicode.ToLower(v)) + str[i+1:] + } + return "" +} + +func parseVariantName(pthParts []string, testResultsPartIdx int) (string, error) { + // example: ./app/build/test-results/testDebugUnitTest/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml + if testResultsPartIdx+1 > len(pthParts) { + return "", fmt.Errorf("unknown path (%s): Local Unit Test task output dir should follow the test-results part", filepath.Join(pthParts...)) } - if i+1 > len(parts) { - return "", fmt.Errorf("get variant name: out of index error") + taskOutputDir := pthParts[testResultsPartIdx+1] + if !strings.HasPrefix(taskOutputDir, "test") || !strings.HasSuffix(taskOutputDir, "UnitTest") { + return "", fmt.Errorf("unknown path (%s): Local Unit Test task output dir should match test*UnitTest pattern", filepath.Join(pthParts...)) } - variant := parts[i+1] - variant = strings.TrimPrefix(variant, "test") + variant := strings.TrimPrefix(taskOutputDir, "test") variant = strings.TrimSuffix(variant, "UnitTest") - runes := []rune(variant) + if len(variant) == 0 { + return "", fmt.Errorf("unknown path (%s): Local Unit Test task output dir should match testUnitTest pattern", filepath.Join(pthParts...)) + } + + return lowercaseFirstLetter(variant), nil +} + +func parseModuleName(pthParts []string, testResultsPartIdx int) (string, error) { + if testResultsPartIdx < 2 { + return "", fmt.Errorf(`unknown path (%s): Local Unit Test task output dir should match /build/test-results`, filepath.Join(pthParts...)) + } + return pthParts[testResultsPartIdx-2], nil +} + +// getVariantDir returns the unique subdirectory inside the test addon export directory for a given artifact. +func getVariantDir(path string) (string, error) { + parts := strings.Split(path, "/") + + i := indexOfTestResultsDirName(parts) + if i == -1 { + return "", fmt.Errorf("path (%s) does not contain 'test-results' folder", path) + } - if len(runes) == 0 { - return "", fmt.Errorf("get variant name from task name: empty string after trimming") + variant, err := parseVariantName(parts, i) + if err != nil { + return "", fmt.Errorf("failed to parse variant name: %s", err) } - runes[0] = unicode.ToLower(runes[0]) - variant = string(runes) - if i < 2 { - return "", fmt.Errorf("get module name: out of index error") + module, err := parseModuleName(parts, i) + if err != nil { + return "", fmt.Errorf("failed to parse module name: %s", err) } - module := parts[i-2] - ret := module + "-" + variant - log.Debugf("getUniqueDir(%s): (%s,%v)", path, ret, nil) - return ret, nil + return module + "-" + variant, nil } diff --git a/testaddon_test.go b/testaddon_test.go index 46ec469..04b16c0 100644 --- a/testaddon_test.go +++ b/testaddon_test.go @@ -4,32 +4,69 @@ import ( "testing" ) -func TestGetUniqueDir(t *testing.T) { - tc := []struct{ - title string - path string +func TestGetVariantDir(t *testing.T) { + tc := []struct { + title string + path string wantStr string - isErr bool + isErr bool }{ { - title: "should return error on empty string", - path: "", + title: "should return variant dir", + path: "./app/build/test-results/testDebugUnitTest/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml", + wantStr: "app-debug", + isErr: false, + }, + { + title: "should return error on empty string", + path: "", + wantStr: "", + isErr: true, + }, + { + title: "should return error for non default Local Android Unit result XML path", + path: "/path/to/test-results/", wantStr: "", - isErr: true, + isErr: true, }, { - title: "should return error if artifact path ends in test results folder with trailing slash", - path: "/path/to/test-results/", + title: "should return error for non default Local Android Unit result XML path", + path: "./app/build/test-results/jacocoTestReleaseUnitTestReport/jacocoTestReleaseUnitTestReport.xml", wantStr: "", - isErr: true, + isErr: true, }, } - + for _, tt := range tc { - str, err := getUniqueDir(tt.path) + str, err := getVariantDir(tt.path) if str != tt.wantStr || (err != nil) != tt.isErr { t.Fatalf("%s: got (%s, %s)", tt.title, str, err) } } -} \ No newline at end of file +} + +func TestGetExportDir(t *testing.T) { + tc := []struct { + title string + artifactPath string + want string + }{ + { + title: "should return 'other' for non mappable result path", + artifactPath: "./app/build/test-results/jacocoTestReleaseUnitTestReport/jacocoTestReleaseUnitTestReport.xml", + want: "other", + }, + { + title: "should return string in - for android result path", + artifactPath: "./app/build/test-results/testDemoDebugUnitTest/TEST-sample.results.test.multiple.bitrise.com.multipletestresultssample.UnitTest0.xml", + want: "app-demoDebug", + }, + } + + for _, tt := range tc { + if got := getExportDir(tt.artifactPath); got != tt.want { + t.Fatalf("%s: got '%s' want '%s'", tt.title, got, tt.want) + } + } +}