github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modfetch/zip_sum_test/zip_sum_test.go (about)

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