From 133ec860de2875b592a50413ef173932bb14a260 Mon Sep 17 00:00:00 2001 From: "Christian G. Warden" Date: Fri, 27 Oct 2017 14:43:18 -0700 Subject: [PATCH] Add Support for Running Specific Tests Update `force test` to allow specific test methods within a class to be run. --- command/test.go | 5 +- lib/partner.go | 44 ----------------- lib/test.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++ lib/test_test.go | 40 ++++++++++++++++ 4 files changed, 164 insertions(+), 46 deletions(-) create mode 100644 lib/test.go create mode 100644 lib/test_test.go diff --git a/command/test.go b/command/test.go index 13b37b73..802db71c 100644 --- a/command/test.go +++ b/command/test.go @@ -10,7 +10,7 @@ import ( ) var cmdTest = &Command{ - Usage: "test (all | classname...)", + Usage: "test (all | classname... | classname.method...)", Short: "Run apex tests", Long: ` Run apex tests @@ -23,7 +23,8 @@ Examples: force test all force test Test1 Test2 Test3 - force test -namespace=ns Test4 + force test Test1.method1 Test1.method2 + force test -namespace=ns Test4 force test -v Test1 `, } diff --git a/lib/partner.go b/lib/partner.go index 443c080e..40e51869 100644 --- a/lib/partner.go +++ b/lib/partner.go @@ -11,25 +11,6 @@ type ForcePartner struct { Force *Force } -type TestRunner interface { - RunTests(tests []string, namespace string) (output TestCoverage, err error) -} - -type TestCoverage struct { - Log string `xml:"Header>DebuggingInfo>debugLog"` - NumberRun int `xml:"Body>runTestsResponse>result>numTestsRun"` - NumberFailures int `xml:"Body>runTestsResponse>result>numFailures"` - NumberLocations []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocations"` - NumberLocationsNotCovered []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocationsNotCovered"` - Name []string `xml:"Body>runTestsResponse>result>codeCoverage>name"` - SMethodNames []string `xml:"Body>runTestsResponse>result>successes>methodName"` - SClassNames []string `xml:"Body>runTestsResponse>result>successes>name"` - FMethodNames []string `xml:"Body>runTestsResponse>result>failures>methodName"` - FClassNames []string `xml:"Body>runTestsResponse>result>failures>name"` - FMessage []string `xml:"Body>runTestsResponse>result>failures>message"` - FStackTrace []string `xml:"Body>runTestsResponse>result>failures>stackTrace"` -} - func NewForcePartner(force *Force) (partner *ForcePartner) { partner = &ForcePartner{Force: force} return @@ -131,31 +112,6 @@ func (partner *ForcePartner) SoapExecuteCore(action, query string) (response []b return } -func (partner *ForcePartner) RunTests(tests []string, namespace string) (output TestCoverage, err error) { - soap := "\n" - if strings.EqualFold(tests[0], "all") { - soap += "True\n" - } else { - for _, element := range tests { - soap += "" + element + "\n" - } - } - if namespace != "" { - soap += "" + namespace + "\n" - } - soap += "" - body, err := partner.soapExecute("runTests", soap) - if err != nil { - return - } - var result TestCoverage - if err = xml.Unmarshal(body, &result); err != nil { - return - } - output = result - return -} - func (partner *ForcePartner) soapExecute(action, query string) (response []byte, err error) { url := fmt.Sprintf("%s/services/Soap/s/%s/%s", partner.Force.Credentials.InstanceUrl, partner.Force.Credentials.SessionOptions.ApiVersion, partner.Force.Credentials.UserInfo.OrgId) soap := NewSoap(url, "http://soap.sforce.com/2006/08/apex", partner.Force.Credentials.AccessToken) diff --git a/lib/test.go b/lib/test.go new file mode 100644 index 00000000..a40eebd2 --- /dev/null +++ b/lib/test.go @@ -0,0 +1,121 @@ +package lib + +import ( + "encoding/xml" + "errors" + "strings" +) + +type TestRunner interface { + RunTests(tests []string, namespace string) (output TestCoverage, err error) +} + +type TestCoverage struct { + Log string `xml:"Header>DebuggingInfo>debugLog"` + NumberRun int `xml:"Body>runTestsResponse>result>numTestsRun"` + NumberFailures int `xml:"Body>runTestsResponse>result>numFailures"` + NumberLocations []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocations"` + NumberLocationsNotCovered []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocationsNotCovered"` + Name []string `xml:"Body>runTestsResponse>result>codeCoverage>name"` + SMethodNames []string `xml:"Body>runTestsResponse>result>successes>methodName"` + SClassNames []string `xml:"Body>runTestsResponse>result>successes>name"` + FMethodNames []string `xml:"Body>runTestsResponse>result>failures>methodName"` + FClassNames []string `xml:"Body>runTestsResponse>result>failures>name"` + FMessage []string `xml:"Body>runTestsResponse>result>failures>message"` + FStackTrace []string `xml:"Body>runTestsResponse>result>failures>stackTrace"` +} + +type TestNode struct { + ClassName string `xml:"className"` + TestMethods []string `xml:"testMethods"` +} + +type RunTestsRequest struct { + AllTests bool `xml:"allTests"` + Classes []string `xml:"classes"` + Namespace string `xml:"namespace"` + MaxFailedTests int `xml:"maxFailedTests"` + Tests []TestNode `xml:"tests,omitEmpty"` +} + +func containsMethods(tests []string) (result bool, err error) { + containsMethods := make(map[bool]int) + classNames := make(map[string]int) + for _, v := range tests { + class, method := splitClassMethod(v) + containsMethods[len(method) > 0]++ + classNames[class]++ + } + if len(classNames) > 1 && (len(containsMethods) > 1 || containsMethods[true] > 0) { + err = errors.New("Tests must all be either class names or methods within the same class") + return + } + _, result = containsMethods[true] + return +} + +func splitClassMethod(s string) (string, string) { + if len(s) == 0 { + return s, s + } + slice := strings.SplitN(s, ".", 2) + if len(slice) == 1 { + return slice[0], "" + } + return slice[0], slice[1] +} + +func NewRunTestsRequest(tests []string, namespace string) (request RunTestsRequest, err error) { + request = RunTestsRequest{ + MaxFailedTests: -1, + Namespace: namespace, + } + if len(tests) == 0 || (len(tests) == 1 && strings.EqualFold(tests[0], "all")) { + request.AllTests = true + return + } + + containsMethods, err := containsMethods(tests) + if err != nil { + return + } + if !containsMethods { + request.Classes = tests + } else { + var methods []string + var class string + for _, v := range tests { + var method string + class, method = splitClassMethod(v) + methods = append(methods, method) + } + // Per the docs, the list of TestNodes can only contain one element + request.Tests = make([]TestNode, 1, 1) + request.Tests[0] = TestNode{ + ClassName: class, + TestMethods: methods, + } + } + return +} + +func (partner *ForcePartner) RunTests(tests []string, namespace string) (output TestCoverage, err error) { + request, err := NewRunTestsRequest(tests, namespace) + if err != nil { + return + } + soap, err := xml.MarshalIndent(request, " ", " ") + if err != nil { + return + } + body, err := partner.soapExecute("runTests", string(soap)) + if err != nil { + return + } + var result TestCoverage + if err = xml.Unmarshal(body, &result); err != nil { + return + } + output = result + return +} diff --git a/lib/test_test.go b/lib/test_test.go new file mode 100644 index 00000000..3121ddce --- /dev/null +++ b/lib/test_test.go @@ -0,0 +1,40 @@ +package lib_test + +import ( + . "github.com/heroku/force/lib" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test", func() { + Describe("NewRunTestsRequest", func() { + It("should support individual test methods", func() { + request, _ := NewRunTestsRequest([]string{"MyClass.method1"}, "") + Expect(len(request.Tests)).To(Equal(1)) + Expect(request.Tests[0].ClassName).To(Equal("MyClass")) + Expect(request.Tests[0].TestMethods[0]).To(Equal("method1")) + }) + It("should support multiple test methods", func() { + request, _ := NewRunTestsRequest([]string{"MyClass.method1", "MyClass.method2"}, "") + Expect(len(request.Tests)).To(Equal(1)) + Expect(request.Tests[0].ClassName).To(Equal("MyClass")) + Expect(request.Tests[0].TestMethods[0]).To(Equal("method1")) + Expect(request.Tests[0].TestMethods[1]).To(Equal("method2")) + }) + It("should support multiple classes", func() { + request, _ := NewRunTestsRequest([]string{"MyClass", "MyOtherClass"}, "") + Expect(len(request.Tests)).To(Equal(0)) + Expect(request.Classes[0]).To(Equal("MyClass")) + Expect(request.Classes[1]).To(Equal("MyOtherClass")) + }) + It("should fail if only some classes specify methods", func() { + _, err := NewRunTestsRequest([]string{"MyClass", "MyOtherClass.method2"}, "") + Expect(err).To(HaveOccurred()) + }) + It("should fail with multiple methods from different classes", func() { + _, err := NewRunTestsRequest([]string{"MyClass.method1", "MyOtherClass.method2"}, "") + Expect(err).To(HaveOccurred()) + }) + }) +})