test/run: process build tags like go/build

R=bradfitz, dave, rsc, r
CC=golang-dev
https://golang.org/cl/10001045
diff --git a/test/run.go b/test/run.go
index 5e167d6..3535532 100644
--- a/test/run.go
+++ b/test/run.go
@@ -27,6 +27,7 @@
 	"sort"
 	"strconv"
 	"strings"
+	"unicode"
 )
 
 var (
@@ -299,14 +300,17 @@
 	return pkgs, nil
 }
 
+type context struct {
+	GOOS   string
+	GOARCH string
+}
+
 // shouldTest looks for build tags in a source file and returns
 // whether the file should be used according to the tags.
 func shouldTest(src string, goos, goarch string) (ok bool, whyNot string) {
 	if idx := strings.Index(src, "\npackage"); idx >= 0 {
 		src = src[:idx]
 	}
-	notgoos := "!" + goos
-	notgoarch := "!" + goarch
 	for _, line := range strings.Split(src, "\n") {
 		line = strings.TrimSpace(line)
 		if strings.HasPrefix(line, "//") {
@@ -318,29 +322,59 @@
 		if len(line) == 0 || line[0] != '+' {
 			continue
 		}
+		ctxt := &context{
+			GOOS:   goos,
+			GOARCH: goarch,
+		}
 		words := strings.Fields(line)
 		if words[0] == "+build" {
-			for _, word := range words {
-				switch word {
-				case goos, goarch:
-					return true, ""
-				case notgoos, notgoarch:
-					continue
-				default:
-					if word[0] == '!' {
-						// NOT something-else
-						return true, ""
-					}
+			ok := false
+			for _, word := range words[1:] {
+				if ctxt.match(word) {
+					ok = true
+					break
 				}
 			}
-			// no matching tag found.
-			return false, line
+			if !ok {
+				// no matching tag found.
+				return false, line
+			}
 		}
 	}
-	// no build tags.
+	// no build tags
 	return true, ""
 }
 
+func (ctxt *context) match(name string) bool {
+	if name == "" {
+		return false
+	}
+	if i := strings.Index(name, ","); i >= 0 {
+		// comma-separated list
+		return ctxt.match(name[:i]) && ctxt.match(name[i+1:])
+	}
+	if strings.HasPrefix(name, "!!") { // bad syntax, reject always
+		return false
+	}
+	if strings.HasPrefix(name, "!") { // negation
+		return len(name) > 1 && !ctxt.match(name[1:])
+	}
+
+	// Tags must be letters, digits, underscores or dots.
+	// Unlike in Go identifiers, all digits are fine (e.g., "386").
+	for _, c := range name {
+		if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
+			return false
+		}
+	}
+
+	if name == ctxt.GOOS || name == ctxt.GOARCH {
+		return true
+	}
+
+	return false
+}
+
 func init() { checkShouldTest() }
 
 // run runs a test.
@@ -815,7 +849,7 @@
 	return cpu
 }
 
-// checkShouldTest runs canity checks on the shouldTest function.
+// checkShouldTest runs sanity checks on the shouldTest function.
 func checkShouldTest() {
 	assert := func(ok bool, _ string) {
 		if !ok {
@@ -823,11 +857,28 @@
 		}
 	}
 	assertNot := func(ok bool, _ string) { assert(!ok, "") }
+
+	// Simple tests.
 	assert(shouldTest("// +build linux", "linux", "arm"))
 	assert(shouldTest("// +build !windows", "linux", "arm"))
 	assertNot(shouldTest("// +build !windows", "windows", "amd64"))
-	assertNot(shouldTest("// +build arm 386", "linux", "amd64"))
+
+	// A file with no build tags will always be tested.
 	assert(shouldTest("// This is a test.", "os", "arch"))
+
+	// Build tags separated by a space are OR-ed together.
+	assertNot(shouldTest("// +build arm 386", "linux", "amd64"))
+
+	// Build tags seperated by a comma are AND-ed together.
+	assertNot(shouldTest("// +build !windows,!plan9", "windows", "amd64"))
+	assertNot(shouldTest("// +build !windows,!plan9", "plan9", "386"))
+
+	// Build tags on multiple lines are AND-ed together.
+	assert(shouldTest("// +build !windows\n// +build amd64", "linux", "amd64"))
+	assertNot(shouldTest("// +build !windows\n// +build amd64", "windows", "amd64"))
+
+	// Test that (!a OR !b) matches anything.
+	assert(shouldTest("// +build !windows !plan9", "windows", "amd64"))
 }
 
 // envForDir returns a copy of the environment