github.com/stackb/rules_proto@v0.0.0-20240221195024-5428336c51f1/pkg/goldentest/cases.go (about)

     1  package goldentest
     2  
     3  /* Copyright 2020 The Bazel Authors. All rights reserved.
     4  
     5  Licensed under the Apache License, Version 2.0 (the "License");
     6  you may not use this file except in compliance with the License.
     7  You may obtain a copy of the License at
     8  
     9     http://www.apache.org/licenses/LICENSE-2.0
    10  
    11  Unless required by applicable law or agreed to in writing, software
    12  distributed under the License is distributed on an "AS IS" BASIS,
    13  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  See the License for the specific language governing permissions and
    15  limitations under the License.
    16  */
    17  
    18  import (
    19  	"bufio"
    20  	"bytes"
    21  	"io"
    22  	"io/ioutil"
    23  	"log"
    24  	"os"
    25  	"os/exec"
    26  	"path"
    27  	"path/filepath"
    28  	"strings"
    29  	"testing"
    30  
    31  	"github.com/bazelbuild/bazel-gazelle/testtools"
    32  	"github.com/bazelbuild/rules_go/go/tools/bazel"
    33  )
    34  
    35  var doCleanup = true
    36  
    37  // GoldenTests is a helper for running glob(["testdata/**"]) style test setups.
    38  type GoldenTests struct {
    39  	extensionDir string
    40  	testDataPath string
    41  	extraArgs    []string
    42  	dataFiles    []bazel.RunfileEntry
    43  	onlyTests    []string
    44  }
    45  
    46  type GoldenTestOption func(*GoldenTests)
    47  
    48  func WithExtraArgs(args ...string) GoldenTestOption {
    49  	return func(g *GoldenTests) {
    50  		g.extraArgs = args
    51  	}
    52  }
    53  
    54  func WithDataFiles(files ...bazel.RunfileEntry) GoldenTestOption {
    55  	return func(g *GoldenTests) {
    56  		g.dataFiles = files
    57  	}
    58  }
    59  
    60  func WithOnlyTests(tests ...string) GoldenTestOption {
    61  	return func(g *GoldenTests) {
    62  		g.onlyTests = tests
    63  	}
    64  }
    65  
    66  // FromDir construct a GoldenTests tester that searches the given directory.
    67  func FromDir(extensionDir string, options ...GoldenTestOption) *GoldenTests {
    68  	test := &GoldenTests{
    69  		extensionDir: extensionDir,
    70  		testDataPath: path.Join(extensionDir, "testdata") + "/",
    71  	}
    72  	for _, opt := range options {
    73  		opt(test)
    74  	}
    75  	return test
    76  }
    77  
    78  func (g *GoldenTests) Run(t *testing.T, gazelleName string) {
    79  	t.Log("Run", g.extensionDir)
    80  	// listFiles(".")
    81  
    82  	gazellePath, ok := bazel.FindBinary(g.extensionDir, gazelleName)
    83  	if !ok {
    84  		t.Fatalf("could not find gazelle: %q in %s", gazelleName, g.extensionDir)
    85  	}
    86  	// t.Log("Found gazelle binary:", gazellePath)
    87  
    88  	tests := map[string][]bazel.RunfileEntry{}
    89  
    90  	files, err := bazel.ListRunfiles()
    91  	if err != nil {
    92  		t.Fatalf("bazel.ListRunfiles() error: %v", err)
    93  	}
    94  
    95  	for _, f := range files {
    96  		if strings.HasPrefix(f.ShortPath, g.testDataPath) {
    97  			relativePath := strings.TrimPrefix(f.ShortPath, g.testDataPath)
    98  			parts := strings.SplitN(relativePath, "/", 2)
    99  			if len(parts) < 2 {
   100  				// This file is not a part of a testcase since it must be in a dir that
   101  				// is the test case and then have a path inside of that.
   102  				continue
   103  			}
   104  
   105  			tests[parts[0]] = append(tests[parts[0]], f)
   106  		}
   107  	}
   108  	if len(tests) == 0 {
   109  		t.Fatal("no tests found")
   110  	}
   111  
   112  	for testName, files := range tests {
   113  		shouldTest := true
   114  		if len(g.onlyTests) > 0 {
   115  			shouldTest = false
   116  			for _, name := range g.onlyTests {
   117  				if name == testName {
   118  					shouldTest = true
   119  					break
   120  				}
   121  			}
   122  		}
   123  		if shouldTest {
   124  			g.testPath(t, gazellePath, testName, files)
   125  		} else {
   126  			log.Println("skipped test:", testName)
   127  		}
   128  	}
   129  }
   130  
   131  func (g *GoldenTests) testPath(t *testing.T, gazellePath, name string, files []bazel.RunfileEntry) {
   132  	t.Run(name, func(t *testing.T) {
   133  		var inputs []testtools.FileSpec
   134  		var goldens []testtools.FileSpec
   135  		extraArgs := g.extraArgs
   136  
   137  		for _, f := range files {
   138  			path := f.Path
   139  			trim := g.testDataPath + name + "/"
   140  			shortPath := strings.TrimPrefix(f.ShortPath, trim)
   141  			info, err := os.Stat(path)
   142  			if err != nil {
   143  				t.Fatalf("os.Stat(%q) error: %v", path, err)
   144  			}
   145  
   146  			// Skip dirs.
   147  			if info.IsDir() {
   148  				continue
   149  			}
   150  
   151  			content, err := ioutil.ReadFile(path)
   152  			if err != nil {
   153  				t.Errorf("ioutil.ReadFile(%q) error: %v", path, err)
   154  			}
   155  
   156  			if shortPath == ".gazelle.args" {
   157  				extraArgs = append(extraArgs, parseArgsFile(bytes.NewReader(content))...)
   158  				continue
   159  			}
   160  
   161  			// Now trim the common prefix off.
   162  			if strings.HasSuffix(shortPath, ".in") {
   163  				inputs = append(inputs, testtools.FileSpec{
   164  					Path:    strings.TrimSuffix(shortPath, ".in"),
   165  					Content: string(content),
   166  				})
   167  			} else if strings.HasSuffix(shortPath, ".out") {
   168  				goldens = append(goldens, testtools.FileSpec{
   169  					Path:    strings.TrimSuffix(shortPath, ".out"),
   170  					Content: string(content),
   171  				})
   172  			} else {
   173  				inputs = append(inputs, testtools.FileSpec{
   174  					Path:    shortPath,
   175  					Content: string(content),
   176  				})
   177  				goldens = append(goldens, testtools.FileSpec{
   178  					Path:    shortPath,
   179  					Content: string(content),
   180  				})
   181  			}
   182  		}
   183  
   184  		dir, cleanup := testtools.CreateFiles(t, inputs)
   185  		if doCleanup {
   186  			defer cleanup()
   187  		}
   188  
   189  		for _, f := range g.dataFiles {
   190  			newName := filepath.Join(dir, f.ShortPath)
   191  			newDir := filepath.Dir(newName)
   192  			if err := os.MkdirAll(newDir, os.ModePerm); err != nil {
   193  				t.Fatal("data file symlink setup error:", f, err)
   194  			}
   195  			if err := os.Symlink(f.Path, newName); err != nil {
   196  				t.Fatal("data file symlink setup error:", f, err)
   197  			}
   198  		}
   199  
   200  		t.Log("running test dir:", dir)
   201  		args := append([]string{"-build_file_name=BUILD"}, extraArgs...)
   202  		cmd := exec.Command(gazellePath, args...)
   203  		cmd.Stdout = os.Stdout
   204  		cmd.Stderr = os.Stderr
   205  		cmd.Dir = dir
   206  		if err := cmd.Run(); err != nil {
   207  			t.Fatal("gazelle command failed!", err)
   208  		}
   209  
   210  		t.Log("checking files:", dir)
   211  
   212  		testtools.CheckFiles(t, dir, goldens)
   213  
   214  		if t.Failed() {
   215  			filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   216  				if err != nil {
   217  					return err
   218  				}
   219  				t.Log("file:", path)
   220  				return nil
   221  			})
   222  		}
   223  	})
   224  }
   225  
   226  // listFiles - convenience debugging function to log the files under a given dir
   227  func listFiles(dir string) error {
   228  	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   229  		if err != nil {
   230  			log.Printf("%v\n", err)
   231  			return err
   232  		}
   233  		if info.Mode()&os.ModeSymlink > 0 {
   234  			link, err := os.Readlink(path)
   235  			if err != nil {
   236  				return err
   237  			}
   238  			log.Printf("%s -> %s", path, link)
   239  			return nil
   240  		}
   241  
   242  		log.Println(path)
   243  		return nil
   244  	})
   245  }
   246  
   247  func parseArgsFile(in io.Reader) []string {
   248  	args := make([]string, 0)
   249  	scanner := bufio.NewScanner(in)
   250  	for scanner.Scan() {
   251  		line := scanner.Text()
   252  		if strings.HasPrefix(line, "#") {
   253  			continue
   254  		}
   255  		args = append(args, line)
   256  	}
   257  	return args
   258  }