Source file src/cmd/go/internal/modfetch/zip_sum_test/zip_sum_test.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package zip_sum_test tests that the module zip files produced by modfetch
     6  // have consistent content sums. Ideally the zip files themselves are also
     7  // stable over time, though this is not strictly necessary.
     8  //
     9  // This test loads a table from testdata/zip_sums.csv. The table has columns
    10  // for module path, version, content sum, and zip file hash. The table
    11  // includes a large number of real modules. The test downloads these modules
    12  // in direct mode and verifies the zip files.
    13  //
    14  // This test is very slow, and it depends on outside modules that change
    15  // frequently, so this is a manual test. To enable it, pass the -zipsum flag.
    16  package zip_sum_test
    17  
    18  import (
    19  	"context"
    20  	"crypto/sha256"
    21  	"encoding/csv"
    22  	"encoding/hex"
    23  	"flag"
    24  	"fmt"
    25  	"internal/testenv"
    26  	"io"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  	"testing"
    31  
    32  	"cmd/go/internal/cfg"
    33  	"cmd/go/internal/modfetch"
    34  	"cmd/go/internal/modload"
    35  
    36  	"golang.org/x/mod/module"
    37  )
    38  
    39  var (
    40  	updateTestData = flag.Bool("u", false, "when set, tests may update files in testdata instead of failing")
    41  	enableZipSum   = flag.Bool("zipsum", false, "enable TestZipSums")
    42  	debugZipSum    = flag.Bool("testwork", false, "when set, TestZipSums will preserve its test directory")
    43  	modCacheDir    = flag.String("zipsumcache", "", "module cache to use instead of temp directory")
    44  	shardCount     = flag.Int("zipsumshardcount", 1, "number of shards to divide TestZipSums into")
    45  	shardIndex     = flag.Int("zipsumshard", 0, "index of TestZipSums shard to test (0 <= zipsumshard < zipsumshardcount)")
    46  )
    47  
    48  const zipSumsPath = "testdata/zip_sums.csv"
    49  
    50  type zipSumTest struct {
    51  	m                     module.Version
    52  	wantSum, wantFileHash string
    53  }
    54  
    55  func TestZipSums(t *testing.T) {
    56  	if !*enableZipSum {
    57  		// This test is very slow and heavily dependent on external repositories.
    58  		// Only run it explicitly.
    59  		t.Skip("TestZipSum not enabled with -zipsum")
    60  	}
    61  	if *shardCount < 1 {
    62  		t.Fatal("-zipsumshardcount must be a positive integer")
    63  	}
    64  	if *shardIndex < 0 || *shardCount <= *shardIndex {
    65  		t.Fatal("-zipsumshard must be between 0 and -zipsumshardcount")
    66  	}
    67  
    68  	testenv.MustHaveGoBuild(t)
    69  	testenv.MustHaveExternalNetwork(t)
    70  	testenv.MustHaveExecPath(t, "bzr")
    71  	testenv.MustHaveExecPath(t, "git")
    72  	// TODO(jayconrod): add hg, svn, and fossil modules to testdata.
    73  	// Could not find any for now.
    74  
    75  	tests, err := readZipSumTests()
    76  	if err != nil {
    77  		t.Fatal(err)
    78  	}
    79  
    80  	if *modCacheDir != "" {
    81  		cfg.BuildContext.GOPATH = *modCacheDir
    82  	} else {
    83  		tmpDir, err := os.MkdirTemp("", "TestZipSums")
    84  		if err != nil {
    85  			t.Fatal(err)
    86  		}
    87  		if *debugZipSum {
    88  			fmt.Fprintf(os.Stderr, "TestZipSums: modCacheDir: %s\n", tmpDir)
    89  		} else {
    90  			defer os.RemoveAll(tmpDir)
    91  		}
    92  		cfg.BuildContext.GOPATH = tmpDir
    93  	}
    94  
    95  	cfg.GOPROXY = "direct"
    96  	cfg.GOSUMDB = "off"
    97  	modload.Init()
    98  
    99  	// Shard tests by downloading only every nth module when shard flags are set.
   100  	// This makes it easier to test small groups of modules quickly. We avoid
   101  	// testing similarly named modules together (the list is sorted by module
   102  	// path and version).
   103  	if *shardCount > 1 {
   104  		r := *shardIndex
   105  		w := 0
   106  		for r < len(tests) {
   107  			tests[w] = tests[r]
   108  			w++
   109  			r += *shardCount
   110  		}
   111  		tests = tests[:w]
   112  	}
   113  
   114  	// Download modules with a rate limit. We may run out of file descriptors
   115  	// or cause timeouts without a limit.
   116  	needUpdate := false
   117  	for i := range tests {
   118  		test := &tests[i]
   119  		name := fmt.Sprintf("%s@%s", strings.ReplaceAll(test.m.Path, "/", "_"), test.m.Version)
   120  		t.Run(name, func(t *testing.T) {
   121  			t.Parallel()
   122  			ctx := context.Background()
   123  
   124  			zipPath, err := modfetch.DownloadZip(ctx, test.m)
   125  			if err != nil {
   126  				if *updateTestData {
   127  					t.Logf("%s: could not download module: %s (will remove from testdata)", test.m, err)
   128  					test.m.Path = "" // mark for deletion
   129  					needUpdate = true
   130  				} else {
   131  					t.Errorf("%s: could not download module: %s", test.m, err)
   132  				}
   133  				return
   134  			}
   135  
   136  			sum := modfetch.Sum(ctx, test.m)
   137  			if sum != test.wantSum {
   138  				if *updateTestData {
   139  					t.Logf("%s: updating content sum to %s", test.m, sum)
   140  					test.wantSum = sum
   141  					needUpdate = true
   142  				} else {
   143  					t.Errorf("%s: got content sum %s; want sum %s", test.m, sum, test.wantSum)
   144  					return
   145  				}
   146  			}
   147  
   148  			h := sha256.New()
   149  			f, err := os.Open(zipPath)
   150  			if err != nil {
   151  				t.Errorf("%s: %v", test.m, err)
   152  			}
   153  			defer f.Close()
   154  			if _, err := io.Copy(h, f); err != nil {
   155  				t.Errorf("%s: %v", test.m, err)
   156  			}
   157  			zipHash := hex.EncodeToString(h.Sum(nil))
   158  			if zipHash != test.wantFileHash {
   159  				if *updateTestData {
   160  					t.Logf("%s: updating zip file hash to %s", test.m, zipHash)
   161  					test.wantFileHash = zipHash
   162  					needUpdate = true
   163  				} else {
   164  					t.Errorf("%s: got zip file hash %s; want hash %s (but content sum matches)", test.m, zipHash, test.wantFileHash)
   165  				}
   166  			}
   167  		})
   168  	}
   169  
   170  	if needUpdate {
   171  		// Remove tests marked for deletion
   172  		r, w := 0, 0
   173  		for r < len(tests) {
   174  			if tests[r].m.Path != "" {
   175  				tests[w] = tests[r]
   176  				w++
   177  			}
   178  			r++
   179  		}
   180  		tests = tests[:w]
   181  
   182  		if err := writeZipSumTests(tests); err != nil {
   183  			t.Error(err)
   184  		}
   185  	}
   186  }
   187  
   188  func readZipSumTests() ([]zipSumTest, error) {
   189  	f, err := os.Open(filepath.FromSlash(zipSumsPath))
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	defer f.Close()
   194  	r := csv.NewReader(f)
   195  
   196  	var tests []zipSumTest
   197  	for {
   198  		line, err := r.Read()
   199  		if err == io.EOF {
   200  			break
   201  		} else if err != nil {
   202  			return nil, err
   203  		} else if len(line) != 4 {
   204  			return nil, fmt.Errorf("%s:%d: malformed line", f.Name(), len(tests)+1)
   205  		}
   206  		test := zipSumTest{m: module.Version{Path: line[0], Version: line[1]}, wantSum: line[2], wantFileHash: line[3]}
   207  		tests = append(tests, test)
   208  	}
   209  	return tests, nil
   210  }
   211  
   212  func writeZipSumTests(tests []zipSumTest) (err error) {
   213  	f, err := os.Create(filepath.FromSlash(zipSumsPath))
   214  	if err != nil {
   215  		return err
   216  	}
   217  	defer func() {
   218  		if cerr := f.Close(); err == nil && cerr != nil {
   219  			err = cerr
   220  		}
   221  	}()
   222  	w := csv.NewWriter(f)
   223  	line := make([]string, 0, 4)
   224  	for _, test := range tests {
   225  		line = append(line[:0], test.m.Path, test.m.Version, test.wantSum, test.wantFileHash)
   226  		if err := w.Write(line); err != nil {
   227  			return err
   228  		}
   229  	}
   230  	w.Flush()
   231  	return nil
   232  }
   233  

View as plain text