Move tools to a tools/src directory

Allows bash scripts to share the same name as the directory.

Add fix-tests bash script.

Change-Id: Iaf1943d50ec1fd3f382a2c7823fb7cdd13b1d9a2
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/47766
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
This commit is contained in:
Ben Clayton
2021-04-16 10:42:24 +00:00
committed by Commit Bot service account
parent 648b05e4e2
commit 434cd396fe
17 changed files with 42 additions and 12 deletions

View File

@@ -0,0 +1,329 @@
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// fix-tests is a tool to update tests with new expected output.
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"dawn.googlesource.com/tint/tools/src/fix-tests/substr"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func showUsage() {
fmt.Println(`
fix-tests is a tool to update tests with new expected output.
fix-tests performs string matching and heuristics to fix up expected results of
tests that use EXPECT_EQ(a, b) and EXPECT_THAT(a, HasSubstr(b))
WARNING: Always thoroughly check the generated output for mistakes.
This may produce incorrect output
Usage:
fix-tests <executable>
executable - the path to the test executable to run.`)
os.Exit(1)
}
func run() error {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
showUsage()
}
exe := args[0] // The path to the test executable
wd := filepath.Dir(exe) // The directory holding the test exe
// Create a temporary directory to hold the 'test-results.json' file
tmpDir, err := ioutil.TempDir("", "fix-tests")
if err != nil {
return err
}
if err := os.MkdirAll(tmpDir, 0666); err != nil {
return fmt.Errorf("Failed to create temporary directory: %w", err)
}
defer os.RemoveAll(tmpDir)
// Full path to the 'test-results.json' in the temporary directory
testResultsPath := filepath.Join(tmpDir, "test-results.json")
// Run the tests
switch err := exec.Command(exe, "--gtest_output=json:"+testResultsPath).Run().(type) {
default:
return err
case nil:
fmt.Println("All tests passed")
case *exec.ExitError:
}
// Read the 'test-results.json' file
testResultsFile, err := os.Open(testResultsPath)
if err != nil {
return err
}
var testResults Results
if err := json.NewDecoder(testResultsFile).Decode(&testResults); err != nil {
return err
}
// For each failing test...
seen := map[string]bool{}
numFixed, numFailed := 0, 0
for _, group := range testResults.Groups {
for _, suite := range group.Testsuites {
for _, failure := range suite.Failures {
// .. attempt to fix the problem
test := testName(group, suite)
if seen[test] {
continue
}
seen[test] = true
if err := processFailure(test, wd, failure.Failure); err != nil {
fmt.Println(fmt.Errorf("%v: %w", test, err))
numFailed++
} else {
numFixed++
}
}
}
}
fmt.Println()
if numFailed > 0 {
fmt.Println(numFailed, "tests could not be fixed")
}
if numFixed > 0 {
fmt.Println(numFixed, "tests fixed")
}
return nil
}
func testName(group TestsuiteGroup, suite Testsuite) string {
groupParts := strings.Split(group.Name, "/")
suiteParts := strings.Split(suite.Name, "/")
return groupParts[len(groupParts)-1] + "." + suiteParts[0]
}
var (
// Regular expression to match a test declaration
reTests = regexp.MustCompile(`TEST(?:_[FP])?\([ \n]*(\w+),[ \n]*(\w+)\)`)
// Regular expression to match a `EXPECT_EQ(a, b)` failure for strings
reExpectEq = regexp.MustCompile(`([./\\a-z_-]*):(\d+).*\nExpected equality of these values:\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?[^\\])"\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?[^\\])"`)
// Regular expression to match a `EXPECT_THAT(a, HasSubstr(b))` failure for strings
reExpectHasSubstr = regexp.MustCompile(`([./\\a-z_-]*):(\d+).*\nValue of: .*\nExpected: has substring "((?:.|\n)*?[^\\])"\n Actual: "((?:.|\n)*?[^\\])"`)
)
func processFailure(test, wd, failure string) error {
// Start by un-escaping newlines in the failure message
failure = strings.ReplaceAll(failure, "\\n", "\n")
// Matched regex strings will also need to be un-escaped, but do this after
// the match, as unescaped quotes may upset the regex patterns
unescape := func(s string) string {
return strings.ReplaceAll(s, `\"`, `"`)
}
escape := func(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\"", `\"`)
return s
}
// Look for a EXPECT_EQ failure pattern
var file string
var fix func(testSource string) (string, error)
if parts := reExpectEq.FindStringSubmatch(failure); len(parts) == 5 {
// EXPECT_EQ(a, b)
a, b := unescape(parts[3]), unescape(parts[4])
file = parts[1]
fix = func(testSource string) (string, error) {
// We don't know if a or b is the expected, so just try flipping the string
// to the other form.
switch {
case strings.Contains(testSource, a):
testSource = strings.Replace(testSource, a, b, -1)
case strings.Contains(testSource, b):
testSource = strings.Replace(testSource, b, a, -1)
default:
// Try escaping for R"(...)" strings
a, b = escape(a), escape(b)
switch {
case strings.Contains(testSource, a):
testSource = strings.Replace(testSource, a, b, -1)
case strings.Contains(testSource, b):
testSource = strings.Replace(testSource, b, a, -1)
default:
return "", fmt.Errorf("Could not fix 'EXPECT_EQ' pattern in '%v'", file)
}
}
return testSource, nil
}
} else if parts := reExpectHasSubstr.FindStringSubmatch(failure); len(parts) == 5 {
// EXPECT_THAT(a, HasSubstr(b))
a, b := unescape(parts[4]), unescape(parts[3])
file = parts[1]
fix = func(testSource string) (string, error) {
if fix := substr.Fix(a, b); fix != "" {
if !strings.Contains(testSource, b) {
// Try escaping for R"(...)" strings
b, fix = escape(b), escape(fix)
}
if strings.Contains(testSource, b) {
testSource = strings.Replace(testSource, b, fix, -1)
return testSource, nil
}
return "", fmt.Errorf("Could apply fix for 'HasSubstr' pattern in '%v'", file)
}
return "", fmt.Errorf("Could find fix for 'HasSubstr' pattern in '%v'", file)
}
} else {
return fmt.Errorf("Cannot fix this type of failure")
}
// Get the path to the source file containing the test failure
sourcePath := filepath.Join(wd, file)
// Parse the source file, split into tests
sourceFile, err := parseSourceFile(sourcePath)
if err != nil {
return fmt.Errorf("Couldn't parse tests from file '%v': %w", file, err)
}
// Find the test
testIdx, ok := sourceFile.tests[test]
if !ok {
return fmt.Errorf("Test not found in '%v'", file)
}
// Grab the source for the particular test
testSource := sourceFile.parts[testIdx]
if testSource, err = fix(testSource); err != nil {
return err
}
// Replace the part of the source file
sourceFile.parts[testIdx] = testSource
// Write out the source file
return writeSourceFile(sourcePath, sourceFile)
}
// parseSourceFile() reads the file at path, splitting the content into chunks
// for each TEST.
func parseSourceFile(path string) (sourceFile, error) {
fileBytes, err := ioutil.ReadFile(path)
if err != nil {
return sourceFile{}, err
}
fileContent := string(fileBytes)
out := sourceFile{
tests: map[string]int{},
}
pos := 0
for _, span := range reTests.FindAllStringIndex(fileContent, -1) {
out.parts = append(out.parts, fileContent[pos:span[0]])
pos = span[0]
match := reTests.FindStringSubmatch(fileContent[span[0]:span[1]])
group := match[1]
suite := match[2]
out.tests[group+"."+suite] = len(out.parts)
}
out.parts = append(out.parts, fileContent[pos:])
return out, nil
}
// writeSourceFile() joins the chunks of the file, and writes the content out to
// path.
func writeSourceFile(path string, file sourceFile) error {
body := strings.Join(file.parts, "")
return ioutil.WriteFile(path, []byte(body), 0666)
}
type sourceFile struct {
parts []string
tests map[string]int // "X.Y" -> part index
}
// Results is the root JSON structure of the JSON --gtest_output file .
type Results struct {
Tests int `json:"tests"`
Failures int `json:"failures"`
Disabled int `json:"disabled"`
Errors int `json:"errors"`
Timestamp string `json:"timestamp"`
Time string `json:"time"`
Name string `json:"name"`
Groups []TestsuiteGroup `json:"testsuites"`
}
// TestsuiteGroup is a group of test suites in the JSON --gtest_output file .
type TestsuiteGroup struct {
Name string `json:"name"`
Tests int `json:"tests"`
Failures int `json:"failures"`
Disabled int `json:"disabled"`
Errors int `json:"errors"`
Timestamp string `json:"timestamp"`
Time string `json:"time"`
Testsuites []Testsuite `json:"testsuite"`
}
// Testsuite is a suite of tests in the JSON --gtest_output file.
type Testsuite struct {
Name string `json:"name"`
ValueParam string `json:"value_param,omitempty"`
Status Status `json:"status"`
Result Result `json:"result"`
Timestamp string `json:"timestamp"`
Time string `json:"time"`
Classname string `json:"classname"`
Failures []Failure `json:"failures,omitempty"`
}
// Failure is a reported test failure in the JSON --gtest_output file.
type Failure struct {
Failure string `json:"failure"`
Type string `json:"type"`
}
// Status is a status code in the JSON --gtest_output file.
type Status string
// Result is a result code in the JSON --gtest_output file.
type Result string

View File

@@ -0,0 +1,5 @@
module dawn.googlesource.com/tint/tools/src/fix-tests
go 1.16
require github.com/sergi/go-diff v1.2.0

View File

@@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -0,0 +1,52 @@
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package substr
import (
diff "github.com/sergi/go-diff/diffmatchpatch"
)
// Fix attempts to reconstruct substr by comparing it to body.
// substr is a fuzzy substring of body.
// Fix returns a new exact substring of body, by calculating a diff of the text.
// If no match could be made, Fix() returns an empty string.
func Fix(body, substr string) string {
dmp := diff.New()
diffs := dmp.DiffMain(body, substr, false)
if len(diffs) == 0 {
return ""
}
front := func() diff.Diff { return diffs[0] }
back := func() diff.Diff { return diffs[len(diffs)-1] }
start, end := 0, len(body)
// Trim edits that remove text from body start
for len(diffs) > 0 && front().Type == diff.DiffDelete {
start += len(front().Text)
diffs = diffs[1:]
}
// Trim edits that remove text from body end
for len(diffs) > 0 && back().Type == diff.DiffDelete {
end -= len(back().Text)
diffs = diffs[:len(diffs)-1]
}
// New substring is the span for the remainder of the edits
return body[start:end]
}

View File

@@ -0,0 +1,275 @@
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package substr
import (
"strings"
"testing"
)
func TestFixSubstr(t *testing.T) {
type test struct {
body string
substr string
expect string
}
for _, test := range []test{
{
body: "abc_def_ghi_jkl_mno",
substr: "def_XXX_jkl",
expect: "def_ghi_jkl",
},
{
body: "abc\ndef\nghi\njkl\nmno",
substr: "def\nXXX\njkl",
expect: "def\nghi\njkl",
},
{
body: "aaaaa12345ccccc",
substr: "1x345",
expect: "12345",
},
{
body: "aaaaa12345ccccc",
substr: "12x45",
expect: "12345",
},
{
body: "aaaaa12345ccccc",
substr: "123x5",
expect: "12345",
},
{
body: "aaaaaaaaaaaaa",
substr: "bbbbbbbbbbbbb",
expect: "", // cannot produce a sensible diff
}, { ///////////////////////////////////////////////////////////////////
body: `Return{
{
ScalarConstructor[not set]{42u}
}
}
`,
substr: `Return{
{
ScalarConstructor[not set]{42}
}
}`,
expect: `Return{
{
ScalarConstructor[not set]{42u}
}
}`,
}, { ///////////////////////////////////////////////////////////////////
body: `VariableDeclStatement{
Variable{
x_1
function
__u32
}
}
Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{42u}
}
Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{0u}
}
Return{}
`,
substr: `Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{42}
}
Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{0}
}`,
expect: `Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{42u}
}
Assignment{
Identifier[not set]{x_1}
ScalarConstructor[not set]{0u}
}`,
}, { ///////////////////////////////////////////////////////////////////
body: `VariableDeclStatement{
Variable{
a
function
__bool
{
ScalarConstructor[not set]{true}
}
}
}
VariableDeclStatement{
Variable{
b
function
__bool
{
ScalarConstructor[not set]{false}
}
}
}
VariableDeclStatement{
Variable{
c
function
__i32
{
ScalarConstructor[not set]{-1}
}
}
}
VariableDeclStatement{
Variable{
d
function
__u32
{
ScalarConstructor[not set]{1u}
}
}
}
VariableDeclStatement{
Variable{
e
function
__f32
{
ScalarConstructor[not set]{1.500000}
}
}
}
`,
substr: `VariableDeclStatement{
Variable{
a
function
__bool
{
ScalarConstructor[not set]{true}
}
}
}
VariableDeclStatement{
Variable{
b
function
__bool
{
ScalarConstructor[not set]{false}
}
}
}
VariableDeclStatement{
Variable{
c
function
__i32
{
ScalarConstructor[not set]{-1}
}
}
}
VariableDeclStatement{
Variable{
d
function
__u32
{
ScalarConstructor[not set]{1}
}
}
}
VariableDeclStatement{
Variable{
e
function
__f32
{
ScalarConstructor[not set]{1.500000}
}
}
}
`,
expect: `VariableDeclStatement{
Variable{
a
function
__bool
{
ScalarConstructor[not set]{true}
}
}
}
VariableDeclStatement{
Variable{
b
function
__bool
{
ScalarConstructor[not set]{false}
}
}
}
VariableDeclStatement{
Variable{
c
function
__i32
{
ScalarConstructor[not set]{-1}
}
}
}
VariableDeclStatement{
Variable{
d
function
__u32
{
ScalarConstructor[not set]{1u}
}
}
}
VariableDeclStatement{
Variable{
e
function
__f32
{
ScalarConstructor[not set]{1.500000}
}
}
}
`,
},
} {
body := strings.ReplaceAll(test.body, "\n", "␤")
substr := strings.ReplaceAll(test.substr, "\n", "␤")
expect := strings.ReplaceAll(test.expect, "\n", ``)
got := strings.ReplaceAll(Fix(test.body, test.substr), "\n", "␤")
if got != expect {
t.Errorf("Test failure:\nbody: '%v'\nsubstr: '%v'\nexpect: '%v'\ngot: '%v'\n\n", body, substr, expect, got)
}
}
}

View File

@@ -0,0 +1,133 @@
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// run-parallel is a tool to run an executable with the provided templated
// arguments across all the hardware threads.
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"sync"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func showUsage() {
fmt.Println(`
run-parallel is a tool to run an executable with the provided templated
arguments across all the hardware threads.
Usage:
run-parallel <executable> [arguments...] -- [per-instance-value...]
executable - the path to the executable to run.
arguments - a list of arguments to pass to the executable.
Any occurrance of $ will be substituted with the
per-instance-value for the given invocation.
per-instance-value - a list of values. The executable will be invoked for each
value in this list.`)
os.Exit(1)
}
func run() error {
onlyPrintFailures := flag.Bool("only-print-failures", false, "Omit output for processes that did not fail")
flag.Parse()
args := flag.Args()
if len(args) < 2 {
showUsage()
}
exe := args[0]
args = args[1:]
var perInstanceValues []string
for i, arg := range args {
if arg == "--" {
perInstanceValues = args[i+1:]
args = args[:i]
break
}
}
if perInstanceValues == nil {
showUsage()
}
taskIndices := make(chan int, 64)
type result struct {
msg string
failed bool
}
results := make([]result, len(perInstanceValues))
numCPU := runtime.NumCPU()
wg := sync.WaitGroup{}
wg.Add(numCPU)
for i := 0; i < numCPU; i++ {
go func() {
defer wg.Done()
for idx := range taskIndices {
taskArgs := make([]string, len(args))
for i, arg := range args {
taskArgs[i] = strings.ReplaceAll(arg, "$", perInstanceValues[idx])
}
success, out := invoke(exe, taskArgs)
if !success || !*onlyPrintFailures {
results[idx] = result{out, !success}
}
}
}()
}
for i := range perInstanceValues {
taskIndices <- i
}
close(taskIndices)
wg.Wait()
failed := false
for _, result := range results {
if result.msg != "" {
fmt.Println(result.msg)
}
failed = failed || result.failed
}
if failed {
os.Exit(1)
}
return nil
}
func invoke(exe string, args []string) (ok bool, output string) {
cmd := exec.Command(exe, args...)
out, err := cmd.CombinedOutput()
str := string(out)
if err != nil {
if str != "" {
return false, str
}
return false, err.Error()
}
return true, str
}

View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e # Fail on any error.
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
ROOT_DIR="$( cd "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd )"
cd $ROOT_DIR
autoninja -C out/Debug
cd $ROOT_DIR/build
ninja

View File

@@ -0,0 +1,6 @@
{
"paths": [
{ "include": [ "src/**.h", "src/**.cc" ] },
{ "exclude": [ "src/**_windows.*", "src/**_other.*" ] }
]
}

View File

@@ -0,0 +1,185 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package glob provides file globbing utilities
package glob
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"dawn.googlesource.com/tint/tools/src/trim-includes/match"
)
// Scan walks all files and subdirectories from root, returning those
// that Config.shouldExamine() returns true for.
func Scan(root string, cfg Config) ([]string, error) {
files := []string{}
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
rel, err := filepath.Rel(root, path)
if err != nil {
rel = path
}
if rel == ".git" {
return filepath.SkipDir
}
if !cfg.shouldExamine(root, path) {
return nil
}
if !info.IsDir() {
files = append(files, rel)
}
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
// Configs is a slice of Config.
type Configs []Config
// Config is used to parse the JSON configuration file.
type Config struct {
// Paths holds a number of JSON objects that contain either a "includes" or
// "excludes" key to an array of path patterns.
// Each path pattern is considered in turn to either include or exclude the
// file path for license scanning. Pattern use forward-slashes '/' for
// directory separators, and may use the following wildcards:
// ? - matches any single non-separator character
// * - matches any sequence of non-separator characters
// ** - matches any sequence of characters including separators
//
// Rules are processed in the order in which they are declared, with later
// rules taking precedence over earlier rules.
//
// All files are excluded before the first rule is evaluated.
//
// Example:
//
// {
// "paths": [
// { "exclude": [ "out/*", "build/*" ] },
// { "include": [ "out/foo.txt" ] }
// ],
// }
Paths searchRules
}
// LoadConfig loads a config file at path.
func LoadConfig(path string) (Config, error) {
cfgBody, err := ioutil.ReadFile(path)
if err != nil {
return Config{}, err
}
d := json.NewDecoder(bytes.NewReader(cfgBody))
cfg := Config{}
if err := d.Decode(&cfg); err != nil {
return Config{}, err
}
return cfg, nil
}
// rule is a search path predicate.
// root is the project relative path.
// cond is the value to return if the rule doesn't either include or exclude.
type rule func(path string, cond bool) bool
// searchRules is a ordered list of search rules.
// searchRules is its own type as it has to perform custom JSON unmarshalling.
type searchRules []rule
// UnmarshalJSON unmarshals the array of rules in the form:
// { "include": [ ... ] } or { "exclude": [ ... ] }
func (l *searchRules) UnmarshalJSON(body []byte) error {
type parsed struct {
Include []string
Exclude []string
}
p := []parsed{}
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&p); err != nil {
return err
}
*l = searchRules{}
for _, rule := range p {
rule := rule
switch {
case len(rule.Include) > 0 && len(rule.Exclude) > 0:
return fmt.Errorf("Rule cannot contain both include and exclude")
case len(rule.Include) > 0:
tests := make([]match.Test, len(rule.Include))
for i, pattern := range rule.Include {
test, err := match.New(pattern)
if err != nil {
return err
}
tests[i] = test
}
*l = append(*l, func(path string, cond bool) bool {
for _, test := range tests {
if test(path) {
return true
}
}
return cond
})
case len(rule.Exclude) > 0:
tests := make([]match.Test, len(rule.Exclude))
for i, pattern := range rule.Exclude {
test, err := match.New(pattern)
if err != nil {
return err
}
tests[i] = test
}
*l = append(*l, func(path string, cond bool) bool {
for _, test := range tests {
if test(path) {
return false
}
}
return cond
})
}
}
return nil
}
// shouldExamine returns true if the file at absPath should be scanned.
func (c Config) shouldExamine(root, absPath string) bool {
root = filepath.ToSlash(root) // Canonicalize
absPath = filepath.ToSlash(absPath) // Canonicalize
relPath, err := filepath.Rel(root, absPath)
if err != nil {
return false
}
res := false
for _, rule := range c.Paths {
res = rule(relPath, res)
}
return res
}

View File

@@ -0,0 +1,3 @@
module dawn.googlesource.com/tint/tools/src/trim-includes
go 1.16

View File

@@ -0,0 +1,295 @@
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// trim-includes is a tool to try removing unnecessary include statements from
// all .cc and .h files in the tint project.
//
// trim-includes removes each #include from each file, then runs the provided
// build script to determine whether the line was necessary. If the include is
// required, it is restored, otherwise it is left deleted.
// After all the #include statements have been tested, the file is
// clang-formatted and git staged.
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
"dawn.googlesource.com/tint/tools/src/trim-includes/glob"
)
var (
// Directory to this .go file
toolRoot = getToolRoot()
// Root directory of the Tint project
projectRoot = getProjectRoot(toolRoot)
// Path to the build script to run after each attempting to remove each
// #include
buildScript = ""
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func showUsage() {
fmt.Println(`
trim-includes is a tool to try removing unnecessary include statements from all
.cc and .h files in the tint project.
trim-includes removes each #include from each file, then runs the provided build
script to determine whether the line was necessary. If the include is required,
it is restored, otherwise it is left deleted.
After all the #include statements have been tested, the file is clang-formatted
and git staged.
Usage:
trim-includes <path-to-build-script>`)
os.Exit(1)
}
func run() error {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
showUsage()
}
var err error
buildScript, err = exec.LookPath(args[0])
if err != nil {
return err
}
buildScript, err = filepath.Abs(buildScript)
if err != nil {
return err
}
cfg, err := glob.LoadConfig("config.cfg")
if err != nil {
return err
}
fmt.Println("Checking the project builds with no changes...")
ok, err := tryBuild()
if err != nil {
return err
}
if !ok {
return fmt.Errorf("Project does not build without edits")
}
fmt.Println("Scanning for files...")
paths, err := glob.Scan(projectRoot, cfg)
if err != nil {
return err
}
fmt.Printf("Loading %v source files...\n", len(paths))
files, err := loadFiles(paths)
if err != nil {
return err
}
for fileIdx, file := range files {
fmt.Printf("[%d/%d]: %v\n", fileIdx+1, len(files), file.path)
includeLines := file.includesLineNumbers()
enabled := make(map[int]bool, len(file.lines))
for i := range file.lines {
enabled[i] = true
}
for includeIdx, line := range includeLines {
fmt.Printf(" [%d/%d]: %v", includeIdx+1, len(includeLines), file.lines[line])
enabled[line] = false
if err := file.save(enabled); err != nil {
return err
}
ok, err := tryBuild()
if err != nil {
return err
}
if ok {
fmt.Printf(" removed\n")
// Wait a bit so file timestamps get an opportunity to change.
// Attempting to save too soon after a successful build may
// result in a false-positive build.
time.Sleep(time.Second)
} else {
fmt.Printf(" required\n")
enabled[line] = true
}
}
if err := file.save(enabled); err != nil {
return err
}
if err := file.format(); err != nil {
return err
}
if err := file.stage(); err != nil {
return err
}
}
fmt.Println("Done")
return nil
}
// Attempt to build the project. Returns true on successful build, false if
// there was a build failure.
func tryBuild() (bool, error) {
cmd := exec.Command("sh", "-c", buildScript)
out, err := cmd.CombinedOutput()
switch err := err.(type) {
case nil:
return cmd.ProcessState.Success(), nil
case *exec.ExitError:
return false, nil
default:
return false, fmt.Errorf("Test failed with error: %v\n%v", err, string(out))
}
}
type file struct {
path string
lines []string
}
var includeRE = regexp.MustCompile(`^\s*#include (?:\"([^"]*)\"|:?\<([^"]*)\>)`)
// Returns the file path with the extension stripped
func stripExtension(path string) string {
if dot := strings.IndexRune(path, '.'); dot > 0 {
return path[:dot]
}
return path
}
// Returns the zero-based line numbers of all #include statements in the file
func (f *file) includesLineNumbers() []int {
out := []int{}
for i, l := range f.lines {
matches := includeRE.FindStringSubmatch(l)
if len(matches) == 0 {
continue
}
include := matches[1]
if include == "" {
include = matches[2]
}
if strings.HasSuffix(stripExtension(f.path), stripExtension(include)) {
// Don't remove #include for header of cc
continue
}
out = append(out, i)
}
return out
}
// Saves the file, omitting the lines with the zero-based line number that are
// either not in `lines` or have a `false` value.
func (f *file) save(lines map[int]bool) error {
content := []string{}
for i, l := range f.lines {
if lines[i] {
content = append(content, l)
}
}
data := []byte(strings.Join(content, "\n"))
return ioutil.WriteFile(f.path, data, 0666)
}
// Runs clang-format on the file
func (f *file) format() error {
err := exec.Command("clang-format", "-i", f.path).Run()
if err != nil {
return fmt.Errorf("Couldn't format file '%v': %w", f.path, err)
}
return nil
}
// Runs git add on the file
func (f *file) stage() error {
err := exec.Command("git", "-C", projectRoot, "add", f.path).Run()
if err != nil {
return fmt.Errorf("Couldn't stage file '%v': %w", f.path, err)
}
return nil
}
// Loads all the files with the given file paths, splitting their content into
// into lines.
func loadFiles(paths []string) ([]file, error) {
wg := sync.WaitGroup{}
wg.Add(len(paths))
files := make([]file, len(paths))
errs := make([]error, len(paths))
for i, path := range paths {
i, path := i, filepath.Join(projectRoot, path)
go func() {
defer wg.Done()
body, err := ioutil.ReadFile(path)
if err != nil {
errs[i] = fmt.Errorf("Failed to open %v: %w", path, err)
} else {
content := string(body)
lines := strings.Split(content, "\n")
files[i] = file{path: path, lines: lines}
}
}()
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
return files, nil
}
// Returns the path to the directory holding this .go file
func getToolRoot() string {
_, filename, _, ok := runtime.Caller(0)
if !ok {
panic("No caller information")
}
mainPath, err := filepath.Abs(filename)
if err != nil {
panic(err)
}
return filepath.Dir(mainPath)
}
// Returns the path to the project root
func getProjectRoot(toolRoot string) string {
root, err := filepath.Abs(filepath.Join(toolRoot, "../.."))
if err != nil {
panic(err)
}
return root
}

View File

@@ -0,0 +1,76 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package match provides functions for performing filepath [?,*,**] wildcard
// matching.
package match
import (
"fmt"
"regexp"
"strings"
)
// Test is the match predicate returned by New.
type Test func(path string) bool
// New returns a Test function that returns true iff the path matches the
// provided pattern.
//
// pattern uses forward-slashes for directory separators '/', and may use the
// following wildcards:
// ? - matches any single non-separator character
// * - matches any sequence of non-separator characters
// ** - matches any sequence of characters including separators
func New(pattern string) (Test, error) {
// Transform pattern into a regex by replacing the uses of `?`, `*`, `**`
// with corresponding regex patterns.
// As the pattern may contain other regex sequences, the string has to be
// escaped. So:
// a) Replace the patterns of `?`, `*`, `**` with unique placeholder tokens.
// b) Escape the expression so that other sequences don't confuse the regex
// parser.
// c) Replace the placeholder tokens with the corresponding regex tokens.
// Temporary placeholder tokens
const (
starstar = "••"
star = "•"
questionmark = "¿"
)
// Check pattern doesn't contain any of our placeholder tokens
for _, r := range []rune{'•', '¿'} {
if strings.ContainsRune(pattern, r) {
return nil, fmt.Errorf("Pattern must not contain '%c'", r)
}
}
// Replace **, * and ? with placeholder tokens
subbed := pattern
subbed = strings.ReplaceAll(subbed, "**", starstar)
subbed = strings.ReplaceAll(subbed, "*", star)
subbed = strings.ReplaceAll(subbed, "?", questionmark)
// Escape any remaining regex characters
escaped := regexp.QuoteMeta(subbed)
// Insert regex matchers for the substituted tokens
regex := "^" + escaped + "$"
regex = strings.ReplaceAll(regex, starstar, ".*")
regex = strings.ReplaceAll(regex, star, "[^/]*")
regex = strings.ReplaceAll(regex, questionmark, "[^/]")
re, err := regexp.Compile(regex)
if err != nil {
return nil, fmt.Errorf(`Failed to compile regex "%v" for pattern "%v": %w`, regex, pattern, err)
}
return re.MatchString, nil
}

View File

@@ -0,0 +1,106 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package match_test
import (
"strings"
"testing"
match "."
)
func TestMatch(t *testing.T) {
for _, test := range []struct {
pattern string
path string
expect bool
}{
{"a", "a", true},
{"b", "a", false},
{"?", "a", true},
{"a/?/c", "a/x/c", true},
{"a/??/c", "a/x/c", false},
{"a/??/c", "a/xx/c", true},
{"a/???/c", "a/x z/c", true},
{"a/?/c", "a/xx/c", false},
{"a/?/?/c", "a/x/y/c", true},
{"a/?/?/?/c", "a/x/y/z/c", true},
{"a/???/c", "a/x/y/c", false},
{"a/?????", "a/x/y/c", false},
{"*", "a", true},
{"*", "abc", true},
{"*", "abc 123", true},
{"*", "xxx/yyy", false},
{"*/*", "xxx/yyy", true},
{"*/*", "xxx/yyy/zzz", false},
{"*/*/c", "xxx/yyy/c", true},
{"a/*/*", "a/xxx/yyy", true},
{"a/*/c", "a/xxx/c", true},
{"a/*/c", "a/xxx/c", true},
{"a/*/*/c", "a/b/c", false},
{"**", "a", true},
{"**", "abc", true},
{"**", "abc 123", true},
{"**", "xxx/yyy", true},
{"**", "xxx/yyy/zzz", true},
{"**/**", "xxx", false},
{"**/**", "xxx/yyy", true},
{"**/**", "xxx/yyy/zzz", true},
{"**/**/**", "xxx/yyy/zzz", true},
{"**/**/c", "xxx/yyy/c", true},
{"**/**/c", "xxx/yyy/c/d", false},
{"a/**/**", "a/xxx/yyy", true},
{"a/**/c", "a/xxx/c", true},
{"a/**/c", "a/xxx/yyy/c", true},
{"a/**/c", "a/xxx/y y/zzz/c", true},
{"a/**/c", "a/c", false},
{"a/**c", "a/c", true},
{"xxx/**.foo", "xxx/aaa.foo", true},
{"xxx/**.foo", "xxx/yyy/zzz/.foo", true},
{"xxx/**.foo", "xxx/yyy/zzz/bar.foo", true},
} {
f, err := match.New(test.pattern)
if err != nil {
t.Errorf(`match.New("%v")`, test.pattern)
continue
}
matched := f(test.path)
switch {
case matched && !test.expect:
t.Errorf(`Path "%v" matched against pattern "%v"`, test.path, test.pattern)
case !matched && test.expect:
t.Errorf(`Path "%v" did not match against pattern "%v"`, test.path, test.pattern)
}
}
}
func TestErrOnPlaceholder(t *testing.T) {
for _, pattern := range []string{"a/b••c", "a/b•c", "a/b/¿c"} {
_, err := match.New(pattern)
if err == nil {
t.Errorf(`match.New("%v") did not return an expected error`, pattern)
continue
}
if !strings.Contains(err.Error(), "Pattern must not contain") {
t.Errorf(`match.New("%v") returned unrecognised error: %v`, pattern, err)
continue
}
}
}