cts: fix generated test names in get-test-plan tool

testnames: sectionX_ruleY
fix init value

Bug: tint:1159 tint:1158
Change-Id: Icc92668ee141b2631d9705f41a5155d6483f9713
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/64041
Reviewed-by: Sarah Mashayekhi <sarahmashay@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Sarah Mashayekhi <sarahmashay@google.com>
diff --git a/tools/src/cmd/get-test-plan/main.go b/tools/src/cmd/get-test-plan/main.go
index 4d401e4..6b2f691 100644
--- a/tools/src/cmd/get-test-plan/main.go
+++ b/tools/src/cmd/get-test-plan/main.go
@@ -46,26 +46,25 @@
 )
 
 const (
-	toolName                             = "get-test-plan"
-	specPath                             = "https://www.w3.org/TR/WGSL/"
-	specVersionUsedtoDevelopmentThisTool = "https://www.w3.org/TR/2021/WD-WGSL-20210831"
+	toolName        = "get-test-plan"
+	specPath        = "https://www.w3.org/TR/WGSL/"
+	specVersionUsed = "https://www.w3.org/TR/2021/WD-WGSL-20210910/"
 )
 
 var (
-	errInvalidArg      = errors.New("invalid arguments")
-	finalSpecURL       = ""
-	markedNodesSet     = make(map[*html.Node]bool)
-	testNamesSet       = make(map[string]bool)
-	visitedBuiltinsSet = make(map[string]int)
-	sha1sSet           = make(map[string]bool)
-	keywords           = []string{
+	errInvalidArg  = errors.New("invalid arguments")
+	headURL        = specVersionUsed
+	markedNodesSet = make(map[*html.Node]bool)
+	testNamesSet   = make(map[string]bool)
+	sha1sSet       = make(map[string]bool)
+	keywords       = []string{
 		"MUST ", "MUST NOT ", "REQUIRED ", "SHALL ",
 		"SHALL NOT ", "SHOULD ", "SHOULD NOT ",
 		"RECOMMENDED ", "MAY ", "OPTIONAL ",
 	}
-	globalSection     = ""
-	globalPrevSection = ""
-	globalCounter     = 0
+	globalSection      = ""
+	globalPrevSectionX = -1
+	globalRuleCounter  = 0
 )
 
 // Holds all the information about a wgsl rule
@@ -92,7 +91,7 @@
 		fmt.Fprintf(out, "spec is an optional local file or a URL to the WGSL specification.\n")
 		fmt.Fprintf(out, "If spec is omitted then the specification is fetched from %v\n\n", specPath)
 
-		fmt.Fprintf(out, "this tools is developed based on: %v\n", specVersionUsedtoDevelopmentThisTool)
+		fmt.Fprintf(out, "this tools is developed based on: %v\n", specVersionUsed)
 		fmt.Fprintf(out, "flags may be any combination of:\n")
 		flag.PrintDefaults()
 	}
@@ -152,8 +151,8 @@
 	if *ctsDir != "" {
 		getUnimplementedTestPlan(*parser, *ctsDir)
 	}
-	txt, tsv := concatRules(rules)
 
+	txt, tsv := concatRules(rules)
 	// if no output then write rules to stdout
 	if *output == "" {
 		fmt.Println(txt)
@@ -179,8 +178,8 @@
 // example: section = [x, y, z] ie. x.y.z(.w)* it returns (start = min(w),end = max(w))
 // if there are no rules extracted from x.y.z it returns (-1, -1)
 func getSectionRange(rules []rule, s []int) (start, end int, err error) {
-	start = 1
-	end = 1
+	start = -1
+	end = -1
 	for _, r := range rules {
 		sectionDims, err := parseSection(r.SubSection)
 		if err != nil {
@@ -250,10 +249,10 @@
 			"Rule Number " + strconv.Itoa(r.Number) + ":",
 			"Unique Id: " + r.Sha,
 			"Section: " + r.SubSection,
-			"URL: " + r.URL,
 			"Keyword: " + r.Keyword,
 			"testName: " + r.TestName,
-			"Description: " + r.Description,
+			"URL: " + r.URL,
+			r.Description,
 			"---------------------------------------------------"}, "\n"))
 
 		tsvLines = append(tsvLines, strings.Join([]string{
@@ -282,31 +281,6 @@
 	return nil
 }
 
-// getBetween returns all the substrings of 'in' that are between 'begins[i]' and 'end'
-// in other words for input of ".*[begin][middle][end].*"
-// output will be []"cleanUpString([begin][middle][end])"
-// example:
-// in: `T is f32 or vecN<f32>
-//    clamp(e1: T ,e2: T ,e3: T) -> T
-//    Returns min(max(e1,e2),e3). Component-wise when T is a vector.
-//    (GLSLstd450NClamp)`
-// begins: []string{"\n", ""}
-// end: "("
-// middles: "clamp(", "Returns min(max(", "(", "clamp(", "Returns min(max(", "("
-func getBetween(in string, begins []string, end string) []string {
-	middles := []string{}
-	for _, right := range begins {
-		re := regexp.MustCompile(regexp.QuoteMeta(right) +
-			`.*` +
-			regexp.QuoteMeta(end) + `|$^`)
-
-		for _, m := range re.FindAllString(in, -1) {
-			middles = append(middles, cleanUpString(m))
-		}
-	}
-	return middles
-}
-
 // parseSpec reads the spec from a local file, or the URL to WGSL spec
 func parseSpec(args []string) (*html.Node, error) {
 	// Check for explicit WGSL spec path
@@ -380,7 +354,6 @@
 		return nil, fmt.Errorf("unsupported URL scheme: %v", specURL.Scheme)
 	}
 	defer specContent.Close()
-	finalSpecURL = specURL.String()
 
 	// Parse spec
 	spec, err := html.Parse(specContent)
@@ -486,19 +459,14 @@
 	}
 
 	id := getID(node)
-	url := finalSpecURL + "#" + id
 	desc := cleanUpString(getNodeData(node))
 
-	title := ""
-	if index := strings.Index(desc, "."); index > -1 {
-		title = desc[0:index]
-	}
-	t, _, err := testName(id, desc, title, subSection)
+	t, _, err := testName(id, desc, subSection)
 	if err != nil {
 		return err
 	}
 
-	sha, err := getSha1(desc, subSection)
+	sha, err := getSha1(desc, id)
 	if err != nil {
 		return err
 	}
@@ -508,7 +476,7 @@
 		Number:      len(p.rules) + 1,
 		Section:     section,
 		SubSection:  subSection,
-		URL:         url,
+		URL:         headURL + "#" + id,
 		Description: desc,
 		TestName:    t,
 		Keyword:     keyword,
@@ -546,14 +514,14 @@
 	sb := strings.Builder{}
 	printNodeText(node, &sb)
 	title := cleanUpStartEnd(getNodeAttrValue(node, "data-algorithm"))
-	desc := title + ":\n" + cleanUpStartEnd(sb.String())
+	desc := title + ":\n" + cleanUpString(sb.String())
 	id := getID(node)
-	testName, builtinName, err := testName(id, desc, title, subSection)
+	testName, _, err := testName(id, desc, subSection)
 	if err != nil {
 		return err
 	}
 
-	sha, err := getSha1(desc, "")
+	sha, err := getSha1(desc, id)
 	if err != nil {
 		return err
 	}
@@ -563,27 +531,11 @@
 		Number:      len(p.rules) + 1,
 		Section:     section,
 		SubSection:  subSection,
-		URL:         finalSpecURL + "#" + id,
+		URL:         headURL + "#" + id,
 		Description: desc,
 		TestName:    testName,
 		Keyword:     "ALGORITHM",
 	}
-	if strings.Contains(id, "builtin-functions") {
-		prevRuleIndex, builtinExist := visitedBuiltinsSet[builtinName]
-		if builtinExist {
-			// TODO(sarahM0): https://bugs.c/tint/1159
-			// Overloads of a builtin function is merged to the previous rule
-			// depending on what we decide to choose as key sha1, this might change
-			(p.rules)[prevRuleIndex].Description += ("\nNext overload:" +
-				"\nURL:" + r.URL + "\nDescription: " + r.Description)
-			r.Desc = append(r.Desc, desc)
-			return nil
-		} else {
-			visitedBuiltinsSet[builtinName] = len(p.rules)
-
-		}
-	}
-
 	p.rules = append(p.rules, r)
 	return nil
 }
@@ -603,13 +555,12 @@
 	desc := cleanUpStartEnd(getNodeData(node))
 	id := getID(node)
 
-	url := finalSpecURL + "#" + id
-	t, _, err := testName(id, desc, "", subSection)
+	t, _, err := testName(id, desc, subSection)
 	if err != nil {
 		return err
 	}
 
-	sha, err := getSha1(desc, subSection)
+	sha, err := getSha1(desc, id)
 	if err != nil {
 		return err
 	}
@@ -619,7 +570,7 @@
 		Number:      len(p.rules) + 1,
 		SubSection:  subSection,
 		Section:     section,
-		URL:         url,
+		URL:         headURL + "#" + id,
 		Description: desc,
 		TestName:    t,
 		Keyword:     "Nowrap",
@@ -701,19 +652,32 @@
 }
 
 var (
-	reCleanUpString = regexp.MustCompile(`\s+|\t|\n`)
-	// regex for starts with spaces
+	reCleanUpString       = regexp.MustCompile(`\n(\n|\s|\t)+|(\s|\t)+\n`)
+	reSpacePlusTwo        = regexp.MustCompile(`\t|\s{2,}`)
 	reBeginOrEndWithSpace = regexp.MustCompile(`^\s|\s$`)
+	reIrregularWhiteSpace = regexp.MustCompile(`§.`)
 )
 
 // cleanUpString creates a string by removing  all extra spaces, newlines and tabs
 // form input string 'in' and returns it
+// This is done so that the uniqueID does not change because of a change in white spaces
+//
+// example in:
+// ` float abs:
+// T is f32 or vecN<f32>
+// 		abs(e: T ) -> 	T
+// 	  Returns the absolute value of e (e.g. e  with a positive sign bit). Component-wise when T is a vector.
+//   (GLSLstd450Fabs)`
+//
+// example out:
+// `float abs:
+// T is f32 or vecN<f32> abs(e: T ) -> T Returns the absolute value of e (e.g. e with a positive sign bit). Component-wise when T is a vector. (GLSLstd450Fabs)`
 func cleanUpString(in string) string {
-	// regex for more than one space, newlines or a tabs
-	out := reCleanUpString.ReplaceAllString(in, " ")
+	out := reCleanUpString.ReplaceAllString(in, "\n")
+	out = reSpacePlusTwo.ReplaceAllString(out, " ")
 	//`§.` is not a valid character for a cts description
 	// ie. this is invalid: g.test().desc(`§.`)
-	out = strings.ReplaceAll(out, "§.", "section ")
+	out = reIrregularWhiteSpace.ReplaceAllString(out, "section ")
 	out = reBeginOrEndWithSpace.ReplaceAllString(out, "")
 	return out
 }
@@ -736,73 +700,73 @@
 
 var (
 	name         = "^[a-zA-Z0-9_]+$"
-	reName       = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
-	reUnderScore = regexp.MustCompile(`[-]+|\s+`)
-	reDoNotBegin = regexp.MustCompile(`^[0-9_]+|[_]+$`)
+	reName       = regexp.MustCompile(`[^a-zA-Z0-9_]`)
+	reUnderScore = regexp.MustCompile(`[_]+`)
+	reDoNotBegin = regexp.MustCompile(`^[0-9_]+|[_]$`)
 )
 
-// testName proposes a test name for the CTS implementation of this rule
-// TODO(crbug.com/tint/1158): update this function to just use section x.y then a number:w
-func testName(id string, desc string, title string, section string) (fileName, builtinName string, err error) {
+// testName creates a test name given a rule id (ie. section name), description and section
+// returns for a builtin rule:
+// 		testName: ${builtin name} + "_" + ${section name}
+// 		builtinName: ${builtin name}
+//		err: nil
+// returns for a other rules:
+//		testName: ${section name} + "_rule_ + " + ${string(counter)}
+//		builtinName: ""
+//		err: nil
+// if it cannot create a unique name it returns "", "", err.
+func testName(id string, desc string, section string) (testName, builtinName string, err error) {
 	// regex for every thing other than letters and numbers
-	if len(desc) == 0 {
-		return "", "", fmt.Errorf("unable to generate a test name for empty description")
+	if desc == "" || section == "" || id == "" {
+		return "", "", fmt.Errorf("cannot generate test name")
 	}
-	fileName = ""
-
-	title = reName.ReplaceAllString(title, "_")
+	// avoid any characters other than letters, numbers and underscore
 	id = reName.ReplaceAllString(id, "_")
-
 	// avoid underscore repeats
-	title = reUnderScore.ReplaceAllString(title, "_")
 	id = reUnderScore.ReplaceAllString(id, "_")
-
 	// test name must not start with underscore or a number
 	// nor end with and underscore
-	title = reDoNotBegin.ReplaceAllString(title, "")
 	id = reDoNotBegin.ReplaceAllString(id, "")
 
+	sectionX, err := parseSection(section)
+	if err != nil {
+		return "", "", err
+	}
+
 	builtinName = ""
-	if strings.Contains(id, "builtin_functions") {
-		n := getBetween(desc, []string{"\n", "\t", " ", ""}, "(")
-		if len(n) > 0 {
-			builtinName = n[0][:len(n[0])-1]
-			match, _ := regexp.MatchString(name, builtinName)
-			if match {
-				fileName = builtinName
-				// no need to check, it's an overload
-				testNamesSet[fileName] = true
-				return fileName, builtinName, nil
+	index := strings.Index(desc, ":")
+	if strings.Contains(id, "builtin_functions") && index > -1 {
+		builtinName = reName.ReplaceAllString(desc[:index], "_")
+		builtinName = reDoNotBegin.ReplaceAllString(builtinName, "")
+		builtinName = reUnderScore.ReplaceAllString(builtinName, "_")
+		match, _ := regexp.MatchString(name, builtinName)
+		if match {
+			testName = builtinName + "," + id
+			for i := 1; testNamesSet[testName]; i++ {
+				testName = builtinName + "_" + id + "_" + strconv.Itoa(i)
 			}
+			testNamesSet[testName] = true
+			return testName, builtinName, nil
 		}
-	}
-
-	if title != "" {
-		fileName = id + "," + title
-		if !testNamesSet[fileName] && len(fileName) < 32 {
-			testNamesSet[fileName] = true
-			return fileName, "", nil
-		}
-	}
-
-	if section != "" {
-		section = reName.ReplaceAllString(section, "_")
-		if section == globalPrevSection {
-			globalCounter++
-		} else {
-			globalCounter = 0
-			globalPrevSection = section
-		}
-		fileName = "section_" + section + "_rule_" + strconv.Itoa(globalCounter)
-		return fileName, "", nil
 
 	}
-	fileName = "error-unable-to-generate-unique-file-name"
-	return fileName, "", fmt.Errorf("unable to generate unique test name\n" + desc)
+
+	if sectionX[0] == globalPrevSectionX {
+		globalRuleCounter++
+	} else {
+		globalRuleCounter = 0
+		globalPrevSectionX = sectionX[0]
+	}
+	testName = "section" + strconv.Itoa(sectionX[0]) + "_rule" + strconv.Itoa(globalRuleCounter)
+	if testNamesSet[testName] {
+		testName = "error-unable-to-generate-unique-file-name"
+		return testName, "", fmt.Errorf("unable to generate unique test name\n" + desc)
+	}
+	testNamesSet[testName] = true
+	return testName, "", nil
 }
 
-// printNodeText traverses node and its children, writing the Data of all
-// TextNodes to sb.
+// printNodeText traverses node and its children, writing the Data of all TextNodes to sb.
 func printNodeText(node *html.Node, sb *strings.Builder) {
 	// mark this node as seen
 	markedNodesSet[node] = true
@@ -992,13 +956,15 @@
 }
 
 func isBuiltinFunctionRule(r rule) bool {
-	_, builtinName, _ := testName(r.URL, r.Description, "", r.SubSection)
-	return builtinName != ""
+	_, builtinName, _ := testName(r.URL, r.Description, r.SubSection)
+	return builtinName != "" || strings.Contains(r.URL, "builtin-functions")
 }
 
 func testPlan(r rule) string {
 	sb := strings.Builder{}
-	sb.WriteString(fmt.Sprintf(unImplementedTestTemplate, r.TestName, r.Sha, "`\n"+r.Description+"\n`"))
+	sb.WriteString(fmt.Sprintf(unImplementedTestTemplate, r.TestName,
+		r.Sha, "`\n"+r.URL+"\n"+r.Description+"\n`"))
+
 	return sb.String()
 }