blob: 6ae1392350ff788aea74645c5d6d4f9705bd9482 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package presubmit
import (
"fmt"
"os"
"strings"
"fuchsia.googlesource.com/jiri/gerrit"
)
type CLNumber int
type Patchset int
// A Workflow handles the interaction with the Continuous Integrations system.
type Workflow interface {
// RemoveOutdatedBuilds should halt and remove all ongoing builds that are older
// than the given valid ones.
RemoveOutdatedBuilds(validCLs map[CLNumber]Patchset) []error
// AddPresubmitTestBuild should start the presubmit tests with the given CLs.
AddPresubmitTestBuild(cls gerrit.CLList) error
// CheckPresubmitBuildConfig returns an error if the presubmit build is not configured
// properly, or if it fails to fetch the status of the last build.
CheckPresubmitBuildConfig() error
// PostResults should publish message for the given changes. The score indicates
// what `verified` score to assign. Select from: Verified{Fail,Neutral,Pass}.
PostResults(message string, changes gerrit.CLList, score VerifiedScore) error
}
// CLsSender handles the workflow and business logic of sending groups of related CLs
// to presubmit testing. The interaction with the CI system is mocked out for testing
// and (in theory) modularity WRT adopting new CI systems.
type CLsSender struct {
CLLists []gerrit.CLList
CLsSent int
Worker Workflow
}
// SendCLstoPresubmitTest sends the set of CLLists for presubmit testing.
func (s *CLsSender) SendCLsToPresubmitTest() error {
for _, curCLList := range s.CLLists {
multiPartCl := combineCLList(curCLList)
if len(multiPartCl.clMap) == 0 {
fmt.Println("Skipping empty CL set")
continue
}
// Don't send the CLs to presubmit-test if at least one of them have PresubmitTest: none.
if multiPartCl.skipPresubmitTest {
fmt.Printf("Skipping %s because presubmit=none\n", multiPartCl.clString)
if err := s.Worker.PostResults(
"Presubmit tests skipped.\n", multiPartCl.changes, VerifiedPass); err != nil {
return err
}
continue
}
// Only test code submitted by trusted contributors.
if !multiPartCl.hasTrustedOwner {
fmt.Printf("Skipping %s because the owner is an external contributor\n", multiPartCl.clString)
if err := s.Worker.PostResults(
"Tell Freenode#fuchsia to kick the presubmit tests.\n", multiPartCl.changes, VerifiedFail); err != nil {
return err
}
continue
}
// Cancel any previous tests from old patch sets that may still be running.
for _, err := range s.Worker.RemoveOutdatedBuilds(multiPartCl.clMap) {
if err != nil {
fmt.Fprintln(os.Stderr, err) // Not fatal; just log errors.
}
}
// Finally send the CLs to presubmit-test.
fmt.Printf("Sending %s to presubmit test\n", multiPartCl.clString)
if err := s.Worker.AddPresubmitTestBuild(curCLList); err != nil {
fmt.Fprintf(os.Stderr, "addPresubmitTestBuild failed: %v\n", err)
} else {
s.CLsSent += len(curCLList)
}
// Notify the author that their change(s) have been sent for presubmit testing.
if err := s.Worker.PostResults(
"Change was sent for presubmit testing. Please stand by.\n", multiPartCl.changes, VerifiedNeutral); err != nil {
return err
}
}
return nil
}
// multiPartCLInfo collects all the data about a list of CLs that we run at the same time.
// Because a single logical change may be broken up into multiple individual CLs, we have to
// run tests on many CLs at once. Colloquially this is referred to as a "multi part" CL.
type multiPartCLInfo struct {
clMap map[CLNumber]Patchset
clString string
skipPresubmitTest bool
hasTrustedOwner bool
changes gerrit.CLList
}
// combineCLList combines the given individual CLs into a single multiPartCLInfo.
func combineCLList(curCLList gerrit.CLList) multiPartCLInfo {
result := multiPartCLInfo{}
result.hasTrustedOwner = true
result.clMap = map[CLNumber]Patchset{}
clStrings := []string{}
for _, curCL := range curCLList {
// If we have a malformed ref string, we can't recover. Must abort.
cl, ps, err := gerrit.ParseRefString(curCL.Reference())
if err != nil {
fmt.Fprintln(os.Stderr, err)
return multiPartCLInfo{}
}
// Check if the author has indicated this change should avoid presubmit tests.
if curCL.PresubmitTest == gerrit.PresubmitTestTypeNone {
result.skipPresubmitTest = true
}
// If any of the CLs aren't trusted, mark the whole list as untrusted.
if !isTrustedContributor(curCL.OwnerEmail()) {
result.hasTrustedOwner = false
}
clStrings = append(clStrings, formatCLString(cl, ps))
result.clMap[CLNumber(cl)] = Patchset(ps)
result.changes = append(result.changes, curCL)
}
result.clString = strings.Join(clStrings, ", ")
return result
}
// isTrustedContributor returns whether this owner is a "trusted" contributor. Being trusted
// controls whether we automatically run your code through tests. Currently this function
// just checks if you're submitting from a google.com address. In the future, it could use an
// ACL or something.
func isTrustedContributor(emailAddress string) bool {
return strings.HasSuffix(emailAddress, "@google.com")
}
// formatCLString formats the given cl and patch numbers into a user-readable description. V23
// does this as a URL like http://go/vcl/xxxx/yy. We could do something similar, but don't!
func formatCLString(clNumber int, patchsetNumber int) string {
return fmt.Sprintf("%d/%d", clNumber, patchsetNumber)
}