blob: 4966912523eebb5c4ccf430bd154885220a21bcd [file] [log] [blame]
// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package main
import (
"fmt"
"os"
"strings"
"testing"
"dawn.googlesource.com/dawn/tools/src/oswrapper"
"github.com/stretchr/testify/require"
"golang.org/x/net/html"
)
// NOTE: The code under test relies on global state (several variables prefixed
// with "global"), so these tests cannot be run in parallel.
// Helper to create HTML node from string
func createHTMLNode(t *testing.T, data string) *html.Node {
node, err := html.Parse(strings.NewReader(data))
require.NoError(t, err)
return node
}
type mockOSWrapper struct {
oswrapper.OSWrapper
writeFileErr error
}
func (m *mockOSWrapper) WriteFile(name string, data []byte, perm os.FileMode) error {
if m.writeFileErr != nil {
return m.writeFileErr
}
return m.OSWrapper.WriteFile(name, data, perm)
}
func resetGlobals() {
markedNodesSet = make(map[*html.Node]bool)
testNamesSet = make(map[string]bool)
sha1sSet = make(map[string]bool)
globalSection = ""
globalPrevSectionX = -1
globalRuleCounter = 0
}
func TestGetSectionRange(t *testing.T) {
rules := []rule{
{SubSection: "1.0"},
{SubSection: "1.1"},
{SubSection: "2.0"},
{SubSection: "1.1.1"},
{SubSection: "1.2"},
}
start, end, err := getSectionRange(rules, []int{1})
require.NoError(t, err)
require.Equal(t, 0, start)
require.Equal(t, 2, end)
start, end, err = getSectionRange(rules, []int{1, 1})
require.NoError(t, err)
require.Equal(t, 0, start)
require.Equal(t, 1, end)
start, end, err = getSectionRange(rules, []int{3})
require.ErrorContains(t, err, "cannot determine section range")
require.Equal(t, -1, start)
require.Equal(t, -1, end)
start, end, err = getSectionRange([]rule{{SubSection: "invalid"}}, []int{1})
require.ErrorContains(t, err, "cannot parse sections string")
require.Equal(t, -1, start)
require.Equal(t, -1, end)
}
func TestParseSection(t *testing.T) {
res, err := parseSection("1.2.3")
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, res)
_, err = parseSection("1.a")
require.ErrorContains(t, err, "cannot parse sections string")
}
func TestConcatRules(t *testing.T) {
rules := []rule{
{
Number: 1,
Sha: "123",
SubSection: "1.0",
URL: "url",
Description: "desc",
TestName: "test",
Keyword: "MUST",
},
}
// Single.
txt, tsv := concatRules(rules, "")
expectedTxt := `Rule Number 1:
Unique Id: 123
Section: 1.0
Keyword: MUST
testName: test
URL: url
desc
---------------------------------------------------`
expectedTsv := "Number\tUniqueId\tSection\tURL\tDescription\tProposed Test Name\tkeyword\n" +
"1\t123\t1.0\turl\tdesc\tMUST\ttest"
require.Equal(t, expectedTxt, txt)
require.Equal(t, tsv, expectedTsv)
// No matching rule.
txt, tsv = concatRules(rules, "notfound")
require.Equal(t, "", txt)
require.Equal(t, "Number\tUniqueId\tSection\tURL\tDescription\tProposed Test Name\tkeyword", tsv)
// Multiple rules with a filter that matches some.
rulesWithFilter := []rule{
{
Number: 10,
Sha: "sha10",
SubSection: "10.0",
URL: "url10",
Description: "first matching rule",
TestName: "matching-test",
Keyword: "MUST",
},
{
Number: 11,
Sha: "sha11",
SubSection: "10.1",
URL: "url11",
Description: "non-matching rule",
TestName: "other-test",
Keyword: "SHOULD",
},
{
Number: 12,
Sha: "sha12",
SubSection: "10.2",
URL: "url12",
Description: "second matching rule",
TestName: "matching-test-2",
Keyword: "MAY",
},
}
txt, tsv = concatRules(rulesWithFilter, "matching-test")
expectedTxt = `Rule Number 10:
Unique Id: sha10
Section: 10.0
Keyword: MUST
testName: matching-test
URL: url10
first matching rule
---------------------------------------------------
Rule Number 12:
Unique Id: sha12
Section: 10.2
Keyword: MAY
testName: matching-test-2
URL: url12
second matching rule
---------------------------------------------------`
expectedTsv = "Number\tUniqueId\tSection\tURL\tDescription\tProposed Test Name\tkeyword\n" +
"10\tsha10\t10.0\turl10\tfirst matching rule\tMUST\tmatching-test\n" +
"12\tsha12\t10.2\turl12\tsecond matching rule\tMAY\tmatching-test-2"
require.Equal(t, txt, expectedTxt)
require.NotContains(t, txt, "non-matching rule")
require.Equal(t, tsv, expectedTsv)
require.NotContains(t, tsv, "non-matching rule")
}
func TestWriteFile(t *testing.T) {
w := oswrapper.CreateFSTestOSWrapper()
path := "/dir/file.txt"
content := "hello"
err := writeFile(path, content, w)
require.NoError(t, err)
readContent, err := w.ReadFile(path)
require.NoError(t, err)
require.Equal(t, content, string(readContent))
// Test mkdir failure case (by making a file at dir location)
err = w.WriteFile("/dir2", []byte("file"), 0666)
require.NoError(t, err)
err = writeFile("/dir2/file.txt", content, w)
require.ErrorContains(t, err, "failed to create directory for")
// Test WriteFile failure
mockW := &mockOSWrapper{
OSWrapper: oswrapper.CreateFSTestOSWrapper(),
writeFileErr: fmt.Errorf("write error"),
}
err = writeFile("/dir/fail.txt", content, mockW)
require.ErrorContains(t, err, "failed to write file")
require.ErrorContains(t, err, "write error")
}
func TestContainsKeyword(t *testing.T) {
list := []string{"foo", "bar"}
found, kw := containsKeyword("something FOO", list)
require.True(t, found)
require.Equal(t, "foo", kw)
found, _ = containsKeyword("nothing", list)
require.False(t, found)
}
func TestParseAndGetRules(t *testing.T) {
resetGlobals()
htmlStr := `
<div id="section3"></div>
<section data-level="3.0">
<p>MUST do something</p>
<p class="algorithm" data-algorithm="algo1">algo description</p>
<table><tr><td class="nowrap">nowrap desc</td></tr></table>
</section>
`
node := createHTMLNode(t, htmlStr)
parser, err := Parse(node)
require.NoError(t, err)
require.Len(t, parser.rules, 3)
var mustRule, algoRule, nowrapRule rule
foundMust := false
foundAlgo := false
foundNowrap := false
for _, r := range parser.rules {
if strings.TrimSpace(r.Keyword) == "MUST" {
mustRule = r
foundMust = true
} else if r.Keyword == "ALGORITHM" {
algoRule = r
foundAlgo = true
} else if r.Keyword == "Nowrap" {
nowrapRule = r
foundNowrap = true
}
}
require.True(t, foundMust, "MUST rule not found")
require.True(t, foundAlgo, "ALGORITHM rule not found")
require.True(t, foundNowrap, "Nowrap rule not found")
require.Equal(t, "3.0", mustRule.SubSection)
require.Equal(t, "algo1:\nalgo description", algoRule.Description)
require.Equal(t, "nowrap desc", nowrapRule.Description)
}
func TestGetSectionInfo(t *testing.T) {
resetGlobals()
// Case 1: Node has data-level
node1 := &html.Node{
Type: html.ElementNode,
Attr: []html.Attribute{{Key: "data-level", Val: "1.2"}},
}
sect, sub, err := getSectionInfo(node1)
require.NoError(t, err)
require.Equal(t, 1, sect)
require.Equal(t, "1.2", sub)
// Case 2: Parent has data-level
node2 := &html.Node{Type: html.ElementNode}
node1.AppendChild(node2)
sect, sub, err = getSectionInfo(node2)
require.NoError(t, err)
require.Equal(t, 1, sect)
require.Equal(t, "1.2", sub)
// Case 3: Sibling has data-level
parent := &html.Node{Type: html.ElementNode}
sibling := &html.Node{
Type: html.ElementNode,
Attr: []html.Attribute{{Key: "data-level", Val: "2.0"}},
}
target := &html.Node{Type: html.ElementNode}
parent.AppendChild(sibling)
parent.AppendChild(target)
sect, sub, err = getSectionInfo(target)
require.NoError(t, err)
require.Equal(t, 2, sect)
require.Equal(t, "2.0", sub)
// Case 4: Global fallback
// globalSection is set by previous calls if they find something.
// Reset it first.
globalSection = "5.5"
orphan := &html.Node{Type: html.ElementNode}
sect, sub, err = getSectionInfo(orphan)
require.NoError(t, err)
require.Equal(t, 5, sect)
require.Equal(t, "5.5", sub)
}
func TestGetID(t *testing.T) {
resetGlobals()
// Case 1: Node has ID
idNode := &html.Node{
Type: html.ElementNode,
Attr: []html.Attribute{{Key: "id", Val: "my-id"}},
}
require.Equal(t, "my-id", getID(idNode))
// Case 2: Node has previous sibling with ID
prevSibling := &html.Node{
Type: html.ElementNode,
Attr: []html.Attribute{{Key: "id", Val: "sibling-id"}},
}
targetNode := &html.Node{Type: html.ElementNode}
parentNode := &html.Node{Type: html.ElementNode}
parentNode.AppendChild(prevSibling)
parentNode.AppendChild(targetNode)
require.Equal(t, "sibling-id", getID(targetNode))
// Case 3: Ancestor has previous sibling with ID
container := &html.Node{Type: html.ElementNode}
ancSibling := &html.Node{
Type: html.ElementNode,
Attr: []html.Attribute{{Key: "id", Val: "ancestor-sibling-id"}},
}
child := &html.Node{Type: html.ElementNode}
grandParent := &html.Node{Type: html.ElementNode}
grandParent.AppendChild(ancSibling)
grandParent.AppendChild(container)
container.AppendChild(child)
require.Equal(t, "ancestor-sibling-id", getID(child))
}
func TestCleanUpString(t *testing.T) {
in := " foo \n bar "
out := cleanUpString(in)
require.Equal(t, "foo bar", out)
require.Equal(t, "section 1", cleanUpString("ยง.1"))
}
func TestCleanUpStartEnd(t *testing.T) {
require.Equal(t, "foo", cleanUpStartEnd(" foo "))
require.Equal(t, "foo", cleanUpStartEnd("\tfoo\n"))
}
func TestTestName(t *testing.T) {
resetGlobals()
name, builtin, err := testName("id", "desc", "1.0")
require.NoError(t, err)
require.Equal(t, "id,rule0", name)
require.Equal(t, "", builtin)
// Test builtin
name, builtin, err = testName("builtin_functions", "abs: desc", "1.0")
require.NoError(t, err)
require.Equal(t, "builtin_functions,abs", name)
require.Equal(t, "abs", builtin)
// Test duplicate builtin
name, builtin, err = testName("builtin_functions", "abs: desc", "1.0")
require.NoError(t, err)
require.Equal(t, "builtin_functions,abs2", name)
// Test error cases
_, _, err = testName("", "desc", "1.0")
require.ErrorContains(t, err, "cannot generate test name")
_, _, err = testName("id", "", "1.0")
require.ErrorContains(t, err, "cannot generate test name")
_, _, err = testName("id", "desc", "")
require.ErrorContains(t, err, "cannot generate test name")
}
func TestGetSha1(t *testing.T) {
resetGlobals()
s1, err := getSha1("a", "b")
require.NoError(t, err)
require.NotEmpty(t, s1)
s2, err := getSha1("a", "b")
require.ErrorContains(t, err, "sha1 is not unique")
require.Empty(t, s2)
}
func TestGetUnimplementedTestPlan(t *testing.T) {
resetGlobals()
w := oswrapper.CreateFSTestOSWrapper()
htmlStr := `
<div id="builtin-functions"></div>
<section data-level="3.0">
<p>MUST do something</p>
</section>
`
node := createHTMLNode(t, htmlStr)
parser, err := Parse(node)
require.NoError(t, err)
require.NotEmpty(t, parser.rules)
err = getUnimplementedTestPlan(*parser, "/cts", w)
require.NoError(t, err)
// Verify files created
// validation/section3.spec.ts should exist
_, err = w.ReadFile("/cts/validation/section3.spec.ts")
require.NoError(t, err)
}
func TestGetTestPlanFilePath(t *testing.T) {
path, err := getTestPlanFilePath("root", 1, -1, 2)
require.NoError(t, err)
require.Equal(t, "root/section01.spec.ts", path)
path, err = getTestPlanFilePath("root", 1, 2, 2)
require.NoError(t, err)
require.Equal(t, "root/section1_02.spec.ts", path)
}
func TestValidationTestPlan(t *testing.T) {
resetGlobals()
w := oswrapper.CreateFSTestOSWrapper()
rules := []rule{
{
Section: 1,
SubSection: "1.0",
URL: "url",
Description: "desc",
TestName: "test",
Sha: "sha",
},
}
// Setup parser state for start/end sections
// This function uses start/end passed to it, usually from parser state.
err := validationTestPlan(rules, "/val", 0, 2, w)
require.NoError(t, err)
// Check file existence
_, err = w.ReadFile("/val/section1.spec.ts")
require.NoError(t, err)
// Test write failure
mockW := &mockOSWrapper{
OSWrapper: oswrapper.CreateFSTestOSWrapper(),
writeFileErr: fmt.Errorf("write error"),
}
err = validationTestPlan(rules, "/val", 0, 2, mockW)
require.ErrorContains(t, err, "write error")
}
func TestExecutionTestPlan(t *testing.T) {
resetGlobals()
w := oswrapper.CreateFSTestOSWrapper()
rules := []rule{
{
Section: 1,
SubSection: "1.0",
URL: "builtin-functions",
Description: "abs: desc",
TestName: "builtin_functions,abs",
},
}
err := executionTestPlan(rules, "/execution", w)
require.NoError(t, err)
_, err = w.ReadFile("/execution/section1_0.spec.ts")
require.NoError(t, err)
}
func TestExecutionTestPlan_Extended(t *testing.T) {
resetGlobals()
w := oswrapper.CreateFSTestOSWrapper()
// Case 1: No builtin section
rulesNoBuiltin := []rule{{Section: 1, URL: "other", SubSection: "1.0"}}
err := executionTestPlan(rulesNoBuiltin, "/exec", w)
require.ErrorContains(t, err, "unable to find the built-in function section")
// Case 2: Write failure
mockW := &mockOSWrapper{
OSWrapper: oswrapper.CreateFSTestOSWrapper(),
writeFileErr: fmt.Errorf("write error"),
}
rules := []rule{
{
Section: 2,
SubSection: "2.0",
URL: "builtin-functions",
Description: "abs: desc",
TestName: "builtin_functions,abs",
},
}
err = executionTestPlan(rules, "/exec", mockW)
require.ErrorContains(t, err, "write error")
// Case 3: Index out of bounds (SubSection with negative index)
// Need a valid rule to establish the range (so getSectionRange doesn't fail),
// and an invalid rule to trigger the index check failure.
rulesOutOfBounds := []rule{
{Section: 2, SubSection: "2.0", URL: "builtin-functions"},
{Section: 2, SubSection: "2.-1", URL: "builtin-functions"},
}
err = executionTestPlan(rulesOutOfBounds, "/exec", w)
require.ErrorContains(t, err, "index -1 out of range")
}
func TestGetBuiltinSectionNum(t *testing.T) {
resetGlobals()
// Test getBuiltinSectionNum
rules := []rule{
{Section: 1, URL: "other"},
{Section: 2, URL: "builtin-functions"},
}
sect, err := getBuiltinSectionNum(rules)
require.NoError(t, err)
require.Equal(t, 2, sect)
rulesNoBuiltin := []rule{
{Section: 1, URL: "other"},
}
_, err = getBuiltinSectionNum(rulesNoBuiltin)
require.Error(t, err)
}
func TestIsBuiltInFunctionRule(t *testing.T) {
resetGlobals()
rBuiltin := rule{
URL: "builtin-functions",
}
require.True(t, isBuiltinFunctionRule(rBuiltin))
rNotBuiltin := rule{
URL: "other",
}
require.False(t, isBuiltinFunctionRule(rNotBuiltin))
}
func TestTestPlan(t *testing.T) {
resetGlobals()
r := rule{
TestName: "test/name",
Sha: "12345",
URL: "http://example.com",
Description: "description",
}
plan := testPlan(r)
require.Contains(t, plan, "g.test('test/name')")
require.Contains(t, plan, ".uniqueId('12345')")
require.Contains(t, plan, ".specURL('http://example.com')")
require.Contains(t, plan, "description")
}