github.com/glimps-jbo/go-licenses@v0.0.0-20230908151000-e06d3c113277/internal/third_party/pkgsite/source/source_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  package source
     5  
     6  import (
     7  	"context"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/google/go-cmp/cmp"
    19  	"github.com/google/go-replayers/httpreplay"
    20  )
    21  
    22  var (
    23  	testTimeout = 2 * time.Second
    24  	record      = flag.Bool("record", false, "record interactions with other systems, for replay")
    25  )
    26  
    27  func TestModuleInfo(t *testing.T) {
    28  	client, done := newReplayClient(t, *record)
    29  	defer done()
    30  
    31  	// Test names where we don't replay/record actual URLs.
    32  	skipReplayTests := map[string]bool{
    33  		// On 5-Jan-2022, gitee.com took too long to respond, so it wasn't possible
    34  		// to record the results.
    35  		"gitee.com": true,
    36  	}
    37  
    38  	check := func(t *testing.T, msg, got, want string, skipReplay bool) {
    39  		t.Helper()
    40  		if got != want {
    41  			t.Fatalf("%s:\ngot  %s\nwant %s", msg, got, want)
    42  		}
    43  		if !skipReplay {
    44  			res, err := client.Head(got)
    45  			if err != nil {
    46  				t.Fatalf("%s: %v", got, err)
    47  			}
    48  			defer res.Body.Close()
    49  			if res.StatusCode != 200 {
    50  				t.Fatalf("%s: %v", got, res.Status)
    51  			}
    52  		}
    53  	}
    54  
    55  	for _, test := range []struct {
    56  		desc                                              string
    57  		modulePath, version, file                         string
    58  		wantRepo, wantModule, wantFile, wantLine, wantRaw string
    59  	}{
    60  		{
    61  			"standard library",
    62  			"std", "v1.12.0", "bytes/buffer.go",
    63  			"https://cs.opensource.google/go/go",
    64  			"https://cs.opensource.google/go/go/+/go1.12:src",
    65  			"https://cs.opensource.google/go/go/+/go1.12:src/bytes/buffer.go",
    66  			"https://cs.opensource.google/go/go/+/go1.12:src/bytes/buffer.go;l=1",
    67  			// The raw URLs for the standard library are relative to the repo root, not
    68  			// the module directory.
    69  			"",
    70  		},
    71  		{
    72  			"old standard library",
    73  			"std", "v1.3.0", "bytes/buffer.go",
    74  			"https://cs.opensource.google/go/go",
    75  			"https://cs.opensource.google/go/go/+/go1.3:src/pkg",
    76  			"https://cs.opensource.google/go/go/+/go1.3:src/pkg/bytes/buffer.go",
    77  			"https://cs.opensource.google/go/go/+/go1.3:src/pkg/bytes/buffer.go;l=1",
    78  			// The raw URLs for the standard library are relative to the repo root, not
    79  			// the module directory.
    80  			"",
    81  		},
    82  		{
    83  			"github module at repo root",
    84  			"github.com/pkg/errors", "v0.8.1", "errors.go",
    85  
    86  			"https://github.com/pkg/errors",
    87  			"https://github.com/pkg/errors/tree/v0.8.1",
    88  			"https://github.com/pkg/errors/blob/v0.8.1/errors.go",
    89  			"https://github.com/pkg/errors/blob/v0.8.1/errors.go#L1",
    90  			"https://github.com/pkg/errors/raw/v0.8.1/errors.go",
    91  		},
    92  		{
    93  			"github module not at repo root",
    94  			"github.com/hashicorp/consul/sdk", "v0.2.0", "freeport/freeport.go",
    95  
    96  			"https://github.com/hashicorp/consul",
    97  			"https://github.com/hashicorp/consul/tree/sdk/v0.2.0/sdk",
    98  			"https://github.com/hashicorp/consul/blob/sdk/v0.2.0/sdk/freeport/freeport.go",
    99  			"https://github.com/hashicorp/consul/blob/sdk/v0.2.0/sdk/freeport/freeport.go#L1",
   100  			"https://github.com/hashicorp/consul/raw/sdk/v0.2.0/sdk/freeport/freeport.go",
   101  		},
   102  		{
   103  			"github module with VCS suffix",
   104  			"github.com/pkg/errors.git", "v0.8.1", "errors.go",
   105  
   106  			"https://github.com/pkg/errors",
   107  			"https://github.com/pkg/errors/tree/v0.8.1",
   108  			"https://github.com/pkg/errors/blob/v0.8.1/errors.go",
   109  			"https://github.com/pkg/errors/blob/v0.8.1/errors.go#L1",
   110  			"https://github.com/pkg/errors/raw/v0.8.1/errors.go",
   111  		},
   112  		{
   113  			"bitbucket",
   114  			"bitbucket.org/plazzaro/kami", "v1.2.1", "defaults.go",
   115  
   116  			"https://bitbucket.org/plazzaro/kami",
   117  			"https://bitbucket.org/plazzaro/kami/src/v1.2.1",
   118  			"https://bitbucket.org/plazzaro/kami/src/v1.2.1/defaults.go",
   119  			"https://bitbucket.org/plazzaro/kami/src/v1.2.1/defaults.go#lines-1",
   120  			"https://bitbucket.org/plazzaro/kami/raw/v1.2.1/defaults.go",
   121  		},
   122  		{
   123  			"incompatible",
   124  			"github.com/airbrake/gobrake", "v3.5.1+incompatible", "gobrake.go",
   125  
   126  			"https://github.com/airbrake/gobrake",
   127  			"https://github.com/airbrake/gobrake/tree/v3.5.1",
   128  			"https://github.com/airbrake/gobrake/blob/v3.5.1/gobrake.go",
   129  			"https://github.com/airbrake/gobrake/blob/v3.5.1/gobrake.go#L1",
   130  			"https://github.com/airbrake/gobrake/raw/v3.5.1/gobrake.go",
   131  		},
   132  		{
   133  			"golang x-tools",
   134  			"golang.org/x/tools", "v0.0.0-20190927191325-030b2cf1153e", "README.md",
   135  
   136  			"https://cs.opensource.google/go/x/tools",
   137  			"https://cs.opensource.google/go/x/tools/+/030b2cf1:",
   138  			"https://cs.opensource.google/go/x/tools/+/030b2cf1:README.md",
   139  			"https://cs.opensource.google/go/x/tools/+/030b2cf1:README.md;l=1",
   140  			"https://github.com/golang/tools/raw/030b2cf1/README.md",
   141  		},
   142  		{
   143  			"golang x-tools-gopls",
   144  			"golang.org/x/tools/gopls", "v0.4.0", "main.go",
   145  
   146  			"https://cs.opensource.google/go/x/tools",
   147  			"https://cs.opensource.google/go/x/tools/+/gopls/v0.4.0:gopls",
   148  			"https://cs.opensource.google/go/x/tools/+/gopls/v0.4.0:gopls/main.go",
   149  			"https://cs.opensource.google/go/x/tools/+/gopls/v0.4.0:gopls/main.go;l=1",
   150  			"https://github.com/golang/tools/raw/gopls/v0.4.0/gopls/main.go",
   151  		},
   152  		{
   153  			"golang dl",
   154  			"golang.org/dl", "c5c89f6c", "go1.16/main.go",
   155  
   156  			"https://cs.opensource.google/go/dl",
   157  			"https://cs.opensource.google/go/dl/+/c5c89f6c:",
   158  			"https://cs.opensource.google/go/dl/+/c5c89f6c:go1.16/main.go",
   159  			"https://cs.opensource.google/go/dl/+/c5c89f6c:go1.16/main.go;l=1",
   160  			"https://github.com/golang/dl/raw/c5c89f6c/go1.16/main.go",
   161  		},
   162  		{
   163  			"golang x-image",
   164  			"golang.org/x/image", "v0.0.0-20190910094157-69e4b8554b2a", "math/fixed/fixed.go",
   165  
   166  			"https://cs.opensource.google/go/x/image",
   167  			"https://cs.opensource.google/go/x/image/+/69e4b855:",
   168  			"https://cs.opensource.google/go/x/image/+/69e4b855:math/fixed/fixed.go",
   169  			"https://cs.opensource.google/go/x/image/+/69e4b855:math/fixed/fixed.go;l=1",
   170  			"https://github.com/golang/image/raw/69e4b855/math/fixed/fixed.go",
   171  		},
   172  		{
   173  			"git.apache.org",
   174  			"git.apache.org/thrift.git", "v0.12.0", "lib/go/thrift/client.go",
   175  
   176  			"https://github.com/apache/thrift",
   177  			"https://github.com/apache/thrift/tree/v0.12.0",
   178  			"https://github.com/apache/thrift/blob/v0.12.0/lib/go/thrift/client.go",
   179  			"https://github.com/apache/thrift/blob/v0.12.0/lib/go/thrift/client.go#L1",
   180  			"https://github.com/apache/thrift/raw/v0.12.0/lib/go/thrift/client.go",
   181  		},
   182  		{
   183  			"vanity for github",
   184  			"cloud.google.com/go/spanner", "v1.0.0", "doc.go",
   185  
   186  			"https://github.com/googleapis/google-cloud-go",
   187  			"https://github.com/googleapis/google-cloud-go/tree/spanner/v1.0.0/spanner",
   188  			"https://github.com/googleapis/google-cloud-go/blob/spanner/v1.0.0/spanner/doc.go",
   189  			"https://github.com/googleapis/google-cloud-go/blob/spanner/v1.0.0/spanner/doc.go#L1",
   190  			"https://github.com/googleapis/google-cloud-go/raw/spanner/v1.0.0/spanner/doc.go",
   191  		},
   192  		{
   193  			"vanity for bitbucket",
   194  			"go.niquid.tech/civic-sip-api", "v0.2.0", "client.go",
   195  
   196  			"https://bitbucket.org/niquid/civic-sip-api.git",
   197  			"https://bitbucket.org/niquid/civic-sip-api.git/src/v0.2.0",
   198  			"https://bitbucket.org/niquid/civic-sip-api.git/src/v0.2.0/client.go",
   199  			"https://bitbucket.org/niquid/civic-sip-api.git/src/v0.2.0/client.go#lines-1",
   200  			"https://bitbucket.org/niquid/civic-sip-api.git/raw/v0.2.0/client.go",
   201  		},
   202  		{
   203  			"vanity for googlesource.com",
   204  			"go.chromium.org/goma/server", "v0.0.23", "log/log.go",
   205  
   206  			"https://chromium.googlesource.com/infra/goma/server",
   207  			"https://chromium.googlesource.com/infra/goma/server/+/v0.0.23",
   208  			"https://chromium.googlesource.com/infra/goma/server/+/v0.0.23/log/log.go",
   209  			"https://chromium.googlesource.com/infra/goma/server/+/v0.0.23/log/log.go#1",
   210  			"",
   211  		},
   212  		{
   213  			"gitlab.com",
   214  			"gitlab.com/tozd/go/errors", "v0.3.0", "errors.go",
   215  
   216  			"https://gitlab.com/tozd/go/errors",
   217  			"https://gitlab.com/tozd/go/errors/-/tree/v0.3.0",
   218  			"https://gitlab.com/tozd/go/errors/-/blob/v0.3.0/errors.go",
   219  			"https://gitlab.com/tozd/go/errors/-/blob/v0.3.0/errors.go#L1",
   220  			"https://gitlab.com/tozd/go/errors/-/raw/v0.3.0/errors.go",
   221  		},
   222  		{
   223  			"other gitlab",
   224  			"gitlab.void-ptr.org/go/nu40c16", "v0.1.2", "nu40c16.go",
   225  
   226  			"https://gitlab.void-ptr.org/go/nu40c16",
   227  			"https://gitlab.void-ptr.org/go/nu40c16/-/tree/v0.1.2",
   228  			"https://gitlab.void-ptr.org/go/nu40c16/-/blob/v0.1.2/nu40c16.go",
   229  			"https://gitlab.void-ptr.org/go/nu40c16/-/blob/v0.1.2/nu40c16.go#L1",
   230  			"https://gitlab.void-ptr.org/go/nu40c16/-/raw/v0.1.2/nu40c16.go",
   231  		},
   232  		{
   233  			"gitee.com",
   234  			"gitee.com/eden-framework/plugins", "v0.0.7", "file.go",
   235  
   236  			"https://gitee.com/eden-framework/plugins",
   237  			"https://gitee.com/eden-framework/plugins/tree/v0.0.7",
   238  			"https://gitee.com/eden-framework/plugins/blob/v0.0.7/file.go",
   239  			"https://gitee.com/eden-framework/plugins/blob/v0.0.7/file.go#L1",
   240  			"https://gitee.com/eden-framework/plugins/raw/v0.0.7/file.go",
   241  		},
   242  		{
   243  			"sourcehut",
   244  			"gioui.org", "v0.0.0-20200726090130-3b95e2918359", "op/op.go",
   245  
   246  			"https://git.sr.ht/~eliasnaur/gio",
   247  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359",
   248  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359/op/op.go",
   249  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359/op/op.go#L1",
   250  			"https://git.sr.ht/~eliasnaur/gio/blob/3b95e2918359/op/op.go",
   251  		},
   252  		{
   253  			"sourcehut nested",
   254  			"gioui.org/app", "v0.0.0-20200726090130-3b95e2918359", "app.go",
   255  
   256  			"https://git.sr.ht/~eliasnaur/gio",
   257  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359/app",
   258  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359/app/app.go",
   259  			"https://git.sr.ht/~eliasnaur/gio/tree/3b95e2918359/app/app.go#L1",
   260  			"https://git.sr.ht/~eliasnaur/gio/blob/3b95e2918359/app/app.go",
   261  		},
   262  		{
   263  			"git.fd.io tag",
   264  			"git.fd.io/govpp", "v0.3.5", "doc.go",
   265  
   266  			"https://git.fd.io/govpp",
   267  			"https://git.fd.io/govpp/tree/?h=v0.3.5",
   268  			"https://git.fd.io/govpp/tree/doc.go?h=v0.3.5",
   269  			"https://git.fd.io/govpp/tree/doc.go?h=v0.3.5#n1",
   270  			"https://git.fd.io/govpp/plain/doc.go?h=v0.3.5",
   271  		},
   272  		{
   273  			"git.fd.io hash",
   274  			"git.fd.io/govpp", "v0.0.0-20200726090130-f04939006063", "doc.go",
   275  
   276  			"https://git.fd.io/govpp",
   277  			"https://git.fd.io/govpp/tree/?id=f04939006063",
   278  			"https://git.fd.io/govpp/tree/doc.go?id=f04939006063",
   279  			"https://git.fd.io/govpp/tree/doc.go?id=f04939006063#n1",
   280  			"https://git.fd.io/govpp/plain/doc.go?id=f04939006063",
   281  		},
   282  		{
   283  			"gitea",
   284  			"gitea.com/chenli/reverse", "v0.1.2", "main.go",
   285  
   286  			"https://gitea.com/chenli/reverse",
   287  			"https://gitea.com/chenli/reverse/src/tag/v0.1.2",
   288  			"https://gitea.com/chenli/reverse/src/tag/v0.1.2/main.go",
   289  			"https://gitea.com/chenli/reverse/src/tag/v0.1.2/main.go#L1",
   290  			"https://gitea.com/chenli/reverse/raw/tag/v0.1.2/main.go",
   291  		},
   292  		{
   293  			"gogs",
   294  			"gogs.buffalo-robot.com/zouhy/micro", "v0.4.2", "go.mod",
   295  
   296  			"https://gogs.buffalo-robot.com/zouhy/micro",
   297  			"https://gogs.buffalo-robot.com/zouhy/micro/src/v0.4.2",
   298  			"https://gogs.buffalo-robot.com/zouhy/micro/src/v0.4.2/go.mod",
   299  			"https://gogs.buffalo-robot.com/zouhy/micro/src/v0.4.2/go.mod#L1",
   300  			"https://gogs.buffalo-robot.com/zouhy/micro/raw/v0.4.2/go.mod",
   301  		},
   302  		{
   303  			"v2 as a branch",
   304  			"github.com/jrick/wsrpc/v2", "v2.1.1", "rpc.go",
   305  
   306  			"https://github.com/jrick/wsrpc",
   307  			"https://github.com/jrick/wsrpc/tree/v2.1.1",
   308  			"https://github.com/jrick/wsrpc/blob/v2.1.1/rpc.go",
   309  			"https://github.com/jrick/wsrpc/blob/v2.1.1/rpc.go#L1",
   310  			"https://github.com/jrick/wsrpc/raw/v2.1.1/rpc.go",
   311  		},
   312  		{
   313  			"v2 as subdirectory",
   314  			"github.com/gonutz/w32/v2", "v2.2.3", "com.go",
   315  
   316  			"https://github.com/gonutz/w32",
   317  			"https://github.com/gonutz/w32/tree/v2.2.3/v2",
   318  			"https://github.com/gonutz/w32/blob/v2.2.3/v2/com.go",
   319  			"https://github.com/gonutz/w32/blob/v2.2.3/v2/com.go#L1",
   320  			"https://github.com/gonutz/w32/raw/v2.2.3/v2/com.go",
   321  		},
   322  		{
   323  			"gopkg.in, one element",
   324  			"gopkg.in/yaml.v2", "v2.2.2", "yaml.go",
   325  
   326  			"https://github.com/go-yaml/yaml",
   327  			"https://github.com/go-yaml/yaml/tree/v2.2.2",
   328  			"https://github.com/go-yaml/yaml/blob/v2.2.2/yaml.go",
   329  			"https://github.com/go-yaml/yaml/blob/v2.2.2/yaml.go#L1",
   330  			"https://github.com/go-yaml/yaml/raw/v2.2.2/yaml.go",
   331  		},
   332  		{
   333  			"gopkg.in, two elements",
   334  			"gopkg.in/boltdb/bolt.v1", "v1.3.0", "doc.go",
   335  
   336  			"https://github.com/boltdb/bolt",
   337  			"https://github.com/boltdb/bolt/tree/v1.3.0",
   338  			"https://github.com/boltdb/bolt/blob/v1.3.0/doc.go",
   339  			"https://github.com/boltdb/bolt/blob/v1.3.0/doc.go#L1",
   340  			"https://github.com/boltdb/bolt/raw/v1.3.0/doc.go",
   341  		},
   342  		{
   343  			"gonum.org",
   344  			"gonum.org/v1/gonum", "v0.6.1", "doc.go",
   345  
   346  			"https://github.com/gonum/gonum",
   347  			"https://github.com/gonum/gonum/tree/v0.6.1",
   348  			"https://github.com/gonum/gonum/blob/v0.6.1/doc.go",
   349  			"https://github.com/gonum/gonum/blob/v0.6.1/doc.go#L1",
   350  			"https://github.com/gonum/gonum/raw/v0.6.1/doc.go",
   351  		},
   352  		{
   353  			"custom with gotools at repo root",
   354  			"dmitri.shuralyov.com/gpu/mtl", "v0.0.0-20191203043605-d42048ed14fd", "mtl.go",
   355  
   356  			"https://dmitri.shuralyov.com/gpu/mtl/...",
   357  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl?rev=d42048ed14fd",
   358  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl?rev=d42048ed14fd#mtl.go",
   359  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl?rev=d42048ed14fd#mtl.go-L1",
   360  			"",
   361  		},
   362  		{
   363  			"custom with gotools in subdir",
   364  			"dmitri.shuralyov.com/gpu/mtl", "v0.0.0-20191203043605-d42048ed14fd", "example/movingtriangle/internal/coreanim/coreanim.go",
   365  
   366  			"https://dmitri.shuralyov.com/gpu/mtl/...",
   367  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl?rev=d42048ed14fd",
   368  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/coreanim?rev=d42048ed14fd#coreanim.go",
   369  			"https://gotools.org/dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/coreanim?rev=d42048ed14fd#coreanim.go-L1",
   370  			"",
   371  		},
   372  		{
   373  			"go-source templates match gitea with transform",
   374  			"opendev.org/airship/airshipctl", "v2.0.0-beta.1", "pkg/cluster/command.go",
   375  			"https://opendev.org/airship/airshipctl",
   376  			"https://opendev.org/airship/airshipctl/src/tag/v2.0.0-beta.1",
   377  			"https://opendev.org/airship/airshipctl/src/tag/v2.0.0-beta.1/pkg/cluster/command.go",
   378  			"https://opendev.org/airship/airshipctl/src/tag/v2.0.0-beta.1/pkg/cluster/command.go#L1",
   379  			"",
   380  		},
   381  		{
   382  			"go-source templates match gitea without transform",
   383  			"git.borago.de/Marco/gqltest", "v0.0.18", "go.mod",
   384  			"https://git.borago.de/Marco/gqltest",
   385  			"https://git.borago.de/Marco/gqltest/src/v0.0.18",
   386  			"https://git.borago.de/Marco/gqltest/src/v0.0.18/go.mod",
   387  			"https://git.borago.de/Marco/gqltest/src/v0.0.18/go.mod#L1",
   388  			"https://git.borago.de/Marco/gqltest/raw/v0.0.18/go.mod",
   389  		},
   390  		{
   391  			"go-source templates match gitlab2",
   392  			"git.pluggableideas.com/destrealm/3rdparty/go-yaml", "v2.2.6", "go.mod",
   393  			"https://git.pluggableideas.com/destrealm/3rdparty/go-yaml",
   394  			"https://git.pluggableideas.com/destrealm/3rdparty/go-yaml/-/tree/v2.2.6",
   395  			"https://git.pluggableideas.com/destrealm/3rdparty/go-yaml/-/blob/v2.2.6/go.mod",
   396  			"https://git.pluggableideas.com/destrealm/3rdparty/go-yaml/-/blob/v2.2.6/go.mod#L1",
   397  			"https://git.pluggableideas.com/destrealm/3rdparty/go-yaml/-/raw/v2.2.6/go.mod",
   398  		},
   399  		{
   400  			"go-source templates match fdio",
   401  			"golang.zx2c4.com/wireguard/windows", "v0.3.4", "go.mod",
   402  			"https://git.zx2c4.com/wireguard-windows",
   403  			"https://git.zx2c4.com/wireguard-windows/tree/?h=v0.3.4",
   404  			"https://git.zx2c4.com/wireguard-windows/tree/go.mod?h=v0.3.4",
   405  			"https://git.zx2c4.com/wireguard-windows/tree/go.mod?h=v0.3.4#n1",
   406  			"https://git.zx2c4.com/wireguard-windows/plain/go.mod?h=v0.3.4",
   407  		},
   408  		{
   409  			"go-source templates match blitiri.com.ar",
   410  			"blitiri.com.ar/go/log", "v1.1.0", "go.mod",
   411  			"https://blitiri.com.ar/git/r/log",
   412  			"https://blitiri.com.ar/git/r/log/b/master/t",
   413  			"https://blitiri.com.ar/git/r/log/b/master/t/f=go.mod.html",
   414  			"https://blitiri.com.ar/git/r/log/b/master/t/f=go.mod.html#line-1",
   415  			"",
   416  		},
   417  	} {
   418  		t.Run(test.desc, func(t *testing.T) {
   419  			info, err := ModuleInfo(context.Background(), &Client{client}, test.modulePath, test.version)
   420  			if err != nil {
   421  				t.Fatal(err)
   422  			}
   423  
   424  			skip := skipReplayTests[test.desc]
   425  			check(t, "file", info.FileURL(test.file), test.wantFile, skip)
   426  		})
   427  	}
   428  }
   429  
   430  func newReplayClient(t *testing.T, record bool) (*http.Client, func()) {
   431  	replayFilePath := filepath.Join("testdata", t.Name()+".replay")
   432  	if record {
   433  		httpreplay.DebugHeaders()
   434  		t.Logf("Recording into %s", replayFilePath)
   435  		if err := os.MkdirAll(filepath.Dir(replayFilePath), 0755); err != nil {
   436  			t.Fatal(err)
   437  		}
   438  		rec, err := httpreplay.NewRecorder(replayFilePath, nil)
   439  		if err != nil {
   440  			t.Fatal(err)
   441  		}
   442  		return rec.Client(), func() {
   443  			if err := rec.Close(); err != nil {
   444  				t.Fatal(err)
   445  			}
   446  		}
   447  	} else {
   448  		rep, err := httpreplay.NewReplayer(replayFilePath)
   449  		if err != nil {
   450  			t.Fatal(err)
   451  		}
   452  		return rep.Client(), func() { _ = rep.Close() }
   453  	}
   454  }
   455  
   456  func TestMatchStatic(t *testing.T) {
   457  	for _, test := range []struct {
   458  		in                   string
   459  		wantRepo, wantSuffix string
   460  	}{
   461  		{"github.com/a/b", "github.com/a/b", ""},
   462  		{"bitbucket.org/a/b", "bitbucket.org/a/b", ""},
   463  		{"github.com/a/b/c/d", "github.com/a/b", "c/d"},
   464  		{"bitbucket.org/a/b/c/d", "bitbucket.org/a/b", "c/d"},
   465  		{"foo.googlesource.com/a/b/c", "foo.googlesource.com/a/b/c", ""},
   466  		{"foo.googlesource.com/a/b/c.git", "foo.googlesource.com/a/b/c", ""},
   467  		{"foo.googlesource.com/a/b/c.git/d", "foo.googlesource.com/a/b/c", "d"},
   468  		{"git.com/repo.git", "git.com/repo", ""},
   469  		{"git.com/repo.git/dir", "git.com/repo", "dir"},
   470  		{"mercurial.com/repo.hg", "mercurial.com/repo", ""},
   471  		{"mercurial.com/repo.hg/dir", "mercurial.com/repo", "dir"},
   472  	} {
   473  		t.Run(test.in, func(t *testing.T) {
   474  			gotRepo, gotSuffix, _, _, err := matchStatic(test.in)
   475  			if err != nil {
   476  				t.Fatal(err)
   477  			}
   478  			if gotRepo != test.wantRepo || gotSuffix != test.wantSuffix {
   479  				t.Errorf("got %q, %q; want %q, %q", gotRepo, gotSuffix, test.wantRepo, test.wantSuffix)
   480  			}
   481  		})
   482  	}
   483  }
   484  
   485  // This test adapted from gddo/gosrc/gosrc_test.go:TestGetDynamic.
   486  func TestModuleInfoDynamic(t *testing.T) {
   487  	// For this test, fake the HTTP requests so we can cover cases that may not appear in the wild.
   488  	client := &Client{
   489  		httpClient: &http.Client{
   490  			Transport: testTransport(testWeb),
   491  			Timeout:   testTimeout,
   492  		},
   493  	}
   494  	// The version doesn't figure into the interesting work and we test versions to commits
   495  	// elsewhere, so use the same version throughout.
   496  	const version = "v1.2.3"
   497  	for _, test := range []struct {
   498  		modulePath string
   499  		want       *Info // if nil, then want error
   500  	}{
   501  		{
   502  			"alice.org/pkg",
   503  			&Info{
   504  				repoURL:   "https://github.com/alice/pkg",
   505  				moduleDir: "",
   506  				commit:    "v1.2.3",
   507  				templates: githubURLTemplates,
   508  			},
   509  		},
   510  		{
   511  			"alice.org/pkg/sub",
   512  			&Info{
   513  				repoURL:   "https://github.com/alice/pkg",
   514  				moduleDir: "sub",
   515  				commit:    "sub/v1.2.3",
   516  				templates: githubURLTemplates,
   517  			},
   518  		},
   519  		{
   520  			"alice.org/pkg/http",
   521  			&Info{
   522  				repoURL:   "https://github.com/alice/pkg",
   523  				moduleDir: "http",
   524  				commit:    "http/v1.2.3",
   525  				templates: githubURLTemplates,
   526  			},
   527  		},
   528  		{
   529  			"alice.org/pkg/source",
   530  			// Has a go-source tag; we try to use the templates.
   531  			&Info{
   532  				repoURL:   "http://alice.org/pkg",
   533  				moduleDir: "source",
   534  				commit:    "source/v1.2.3",
   535  				templates: urlTemplates{
   536  					Repo:      "http://alice.org/pkg",
   537  					Directory: "http://alice.org/pkg/{dir}",
   538  					File:      "http://alice.org/pkg/{dir}?f={file}",
   539  					Line:      "http://alice.org/pkg/{dir}?f={file}#Line{line}",
   540  				},
   541  			},
   542  		},
   543  
   544  		{
   545  			"alice.org/pkg/ignore",
   546  			// Stop at the first go-source.
   547  			&Info{
   548  				repoURL:   "http://alice.org/pkg",
   549  				moduleDir: "ignore",
   550  				commit:    "ignore/v1.2.3",
   551  				templates: urlTemplates{
   552  					Repo:      "http://alice.org/pkg",
   553  					Directory: "http://alice.org/pkg/{dir}",
   554  					File:      "http://alice.org/pkg/{dir}?f={file}",
   555  					Line:      "http://alice.org/pkg/{dir}?f={file}#Line{line}",
   556  				},
   557  			},
   558  		},
   559  		{"alice.org/pkg/multiple", nil},
   560  		{"alice.org/pkg/notfound", nil},
   561  		{
   562  			"bob.com/pkg",
   563  			&Info{
   564  				// The go-import tag's repo root ends in ".git", but according to the spec
   565  				// there should not be a .vcs suffix, so we include the ".git" in the repo URL.
   566  				repoURL:   "https://vcs.net/bob/pkg",
   567  				moduleDir: "",
   568  				commit:    "v1.2.3",
   569  				// empty templates
   570  			},
   571  		},
   572  		{
   573  			"bob.com/pkg/sub",
   574  			&Info{
   575  				repoURL:   "https://vcs.net/bob/pkg",
   576  				moduleDir: "sub",
   577  				commit:    "sub/v1.2.3",
   578  				// empty templates
   579  			},
   580  		},
   581  		{
   582  			"azul3d.org/examples/abs",
   583  			// The go-source tag has a template that is handled incorrectly by godoc; but we
   584  			// ignore the templates.
   585  			&Info{
   586  				repoURL:   "https://github.com/azul3d/examples",
   587  				moduleDir: "abs",
   588  				commit:    "abs/v1.2.3",
   589  				templates: githubURLTemplates,
   590  			},
   591  		},
   592  		{
   593  			"myitcv.io/blah2",
   594  			// Ignore the "mod" vcs type.
   595  			&Info{
   596  				repoURL:   "https://github.com/myitcv/x",
   597  				moduleDir: "",
   598  				commit:    "v1.2.3",
   599  				templates: githubURLTemplates,
   600  			},
   601  		},
   602  		{
   603  			"alice.org/pkg/default",
   604  			&Info{
   605  				repoURL:   "https://github.com/alice/pkg",
   606  				moduleDir: "default",
   607  				commit:    "default/v1.2.3",
   608  				templates: githubURLTemplates,
   609  			},
   610  		},
   611  		{
   612  			// Bad repo URLs. These are not escaped here, but they are whenever we render a template.
   613  			"bob.com/bad/github",
   614  			&Info{
   615  				repoURL:   `https://github.com/bob/bad/">$`,
   616  				moduleDir: "",
   617  				commit:    "v1.2.3",
   618  				templates: githubURLTemplates,
   619  			},
   620  		},
   621  		{
   622  
   623  			"bob.com/bad/apache",
   624  			&Info{
   625  				repoURL:   "https://git.apache.org/>$",
   626  				moduleDir: "",
   627  				commit:    "v1.2.3",
   628  				templates: githubURLTemplates,
   629  			},
   630  		},
   631  	} {
   632  		t.Run(test.modulePath, func(t *testing.T) {
   633  			got, err := moduleInfoDynamic(context.Background(), client, test.modulePath, version)
   634  			if err != nil {
   635  				if test.want == nil {
   636  					return
   637  				}
   638  				t.Fatal(err)
   639  			}
   640  			if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(Info{}, urlTemplates{})); diff != "" {
   641  				t.Errorf("mismatch (-want +got):\n%s", diff)
   642  			}
   643  		})
   644  	}
   645  }
   646  
   647  func TestRemoveVersionSuffix(t *testing.T) {
   648  	for _, test := range []struct {
   649  		in   string
   650  		want string
   651  	}{
   652  		{"", ""},
   653  		{"v1", "v1"},
   654  		{"v2", ""},
   655  		{"v17", ""},
   656  		{"foo/bar", "foo/bar"},
   657  		{"foo/bar/v1", "foo/bar/v1"},
   658  		{"foo/bar.v2", "foo/bar.v2"},
   659  		{"foo/bar/v2", "foo/bar"},
   660  		{"foo/bar/v17", "foo/bar"},
   661  	} {
   662  		got := removeVersionSuffix(test.in)
   663  		if got != test.want {
   664  			t.Errorf("%q: got %q, want %q", test.in, got, test.want)
   665  		}
   666  	}
   667  }
   668  
   669  func TestAdjustVersionedModuleDirectory(t *testing.T) {
   670  	ctx := context.Background()
   671  	client := NewClient(testTimeout)
   672  	client.httpClient.Transport = testTransport(map[string]string{
   673  		// Repo "branch" follows the "major branch" convention: versions 2 and higher
   674  		// live in the same directory as versions 0 and 1, but on a different branch (or tag).
   675  		"http://x.com/branch/v1.0.0/go.mod":         "", // v1 module at the root
   676  		"http://x.com/branch/v2.0.0/go.mod":         "", // v2 module at the root
   677  		"http://x.com/branch/dir/v1.0.0/dir/go.mod": "", // v1 module in a subdirectory
   678  		"http://x.com/branch/dir/v2.0.0/dir/go.mod": "", // v2 module in a subdirectory
   679  		// Repo "sub" follows the "major subdirectory" convention: versions 2 and higher
   680  		// live in a "vN" subdirectory.
   681  		"http://x.com/sub/v1.0.0/go.mod":            "", // v1 module at the root
   682  		"http://x.com/sub/v2.0.0/v2/go.mod":         "", // v2 module at root/v2.
   683  		"http://x.com/sub/dir/v1.0.0/dir/go.mod":    "", // v1 module in a subdirectory
   684  		"http://x.com/sub/dir/v2.0.0/dir/v2/go.mod": "", // v2 module in subdirectory/v2
   685  	})
   686  
   687  	for _, test := range []struct {
   688  		repo, moduleDir, commit string
   689  		want                    string
   690  	}{
   691  		{
   692  			// module path is x.com/branch
   693  			"branch", "", "v1.0.0",
   694  			"",
   695  		},
   696  		{
   697  			// module path is x.com/branch/v2; remove the "v2" to get the module dir
   698  			"branch", "v2", "v2.0.0",
   699  			"",
   700  		},
   701  		{
   702  			// module path is x.com/branch/dir
   703  			"branch", "dir", "dir/v1.0.0",
   704  			"dir",
   705  		},
   706  		{
   707  			// module path is x.com/branch/dir/v2; remove the v2 to get the module dir
   708  			"branch", "dir/v2", "dir/v2.0.0",
   709  			"dir",
   710  		},
   711  		{
   712  			// module path is x.com/sub
   713  			"sub", "", "v1.0.0",
   714  			"",
   715  		},
   716  		{
   717  			// module path is x.com/sub/v2; do not remove the v2
   718  			"sub", "v2", "v2.0.0",
   719  			"v2",
   720  		},
   721  		{
   722  			// module path is x.com/sub/dir
   723  			"sub", "dir", "dir/v1.0.0",
   724  			"dir",
   725  		},
   726  		{
   727  			// module path is x.com/sub/dir/v2; do not remove the v2
   728  			"sub", "dir/v2", "dir/v2.0.0",
   729  			"dir/v2",
   730  		},
   731  	} {
   732  		t.Run(test.repo+","+test.moduleDir+","+test.commit, func(t *testing.T) {
   733  			info := &Info{
   734  				repoURL:   "http://x.com/" + test.repo,
   735  				moduleDir: test.moduleDir,
   736  				commit:    test.commit,
   737  				templates: urlTemplates{File: "{repo}/{commit}/{file}"},
   738  			}
   739  			adjustVersionedModuleDirectory(ctx, client, info)
   740  			got := info.moduleDir
   741  			if got != test.want {
   742  				t.Errorf("got %q, want %q", got, test.want)
   743  			}
   744  		})
   745  	}
   746  }
   747  
   748  func TestCommitFromVersion(t *testing.T) {
   749  	for _, test := range []struct {
   750  		version, dir string
   751  		wantCommit   string
   752  		wantIsHash   bool
   753  	}{
   754  		{
   755  			"v1.2.3", "",
   756  			"v1.2.3", false,
   757  		},
   758  		{
   759  			"v1.2.3", "foo",
   760  			"foo/v1.2.3", false,
   761  		},
   762  		{
   763  			"v1.2.3", "foo/v1",
   764  			"foo/v1/v1.2.3", // don't remove "/vN" if N = 1
   765  			false,
   766  		},
   767  		{
   768  			"v1.2.3", "v1", // ditto
   769  			"v1/v1.2.3", false,
   770  		},
   771  		{
   772  			"v3.1.0", "foo/v3",
   773  			"foo/v3.1.0", // do remove "/v2" and higher
   774  			false,
   775  		},
   776  		{
   777  			"v3.1.0", "v3",
   778  			"v3.1.0", // ditto
   779  			false,
   780  		},
   781  		{
   782  			"v6.1.1-0.20190615154606-3a9541ec9974", "",
   783  			"3a9541ec9974", true,
   784  		},
   785  		{
   786  			"v6.1.1-0.20190615154606-3a9541ec9974", "foo",
   787  			"3a9541ec9974", true,
   788  		},
   789  	} {
   790  		t.Run(fmt.Sprintf("%s,%s", test.version, test.dir), func(t *testing.T) {
   791  			check := func(v string) {
   792  				gotCommit, gotIsHash := commitFromVersion(v, test.dir)
   793  				if gotCommit != test.wantCommit {
   794  					t.Errorf("%s commit: got %s, want %s", v, gotCommit, test.wantCommit)
   795  				}
   796  				if gotIsHash != test.wantIsHash {
   797  					t.Errorf("%s isHash: got %t, want %t", v, gotIsHash, test.wantIsHash)
   798  				}
   799  			}
   800  
   801  			check(test.version)
   802  			// Adding "+incompatible" shouldn't make a difference.
   803  			check(test.version + "+incompatible")
   804  		})
   805  	}
   806  }
   807  
   808  type testTransport map[string]string
   809  
   810  func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   811  	statusCode := http.StatusOK
   812  	req.URL.RawQuery = ""
   813  	body, ok := t[req.URL.String()]
   814  	if !ok {
   815  		statusCode = http.StatusNotFound
   816  	}
   817  	resp := &http.Response{
   818  		StatusCode: statusCode,
   819  		Body:       io.NopCloser(strings.NewReader(body)),
   820  	}
   821  	return resp, nil
   822  }
   823  
   824  var testWeb = map[string]string{
   825  	// Package at root of a GitHub repo.
   826  	"https://alice.org/pkg": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg"></head>`,
   827  	// Package in sub-directory.
   828  	"https://alice.org/pkg/sub": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg"><body>`,
   829  	// Fallback to http.
   830  	"http://alice.org/pkg/http": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
   831  	// Meta tag in sub-directory does not match meta tag at root.
   832  	"https://alice.org/pkg/mismatch": `<head> <meta name="go-import" content="alice.org/pkg hg https://github.com/alice/pkg">`,
   833  	// More than one matching meta tag.
   834  	"http://alice.org/pkg/multiple": `<head> ` +
   835  		`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
   836  		`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
   837  	// Package with go-source meta tag.
   838  	"https://alice.org/pkg/source": `<head>` +
   839  		`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
   840  		`<meta name="go-source" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">`,
   841  	"https://alice.org/pkg/ignore": `<head>` +
   842  		`<title>Hello</title>` +
   843  		// Unknown meta name
   844  		`<meta name="go-junk" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">` +
   845  		// go-source before go-meta
   846  		`<meta name="go-source" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">` +
   847  		// go-import tag for the package
   848  		`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
   849  		// go-import with wrong number of fields
   850  		`<meta name="go-import" content="alice.org/pkg https://github.com/alice/pkg">` +
   851  		// go-import with no fields
   852  		`<meta name="go-import" content="">` +
   853  		// go-source with wrong number of fields
   854  		`<meta name="go-source" content="alice.org/pkg blah">` +
   855  		// meta tag for a different package
   856  		`<meta name="go-import" content="alice.org/other git https://github.com/alice/other">` +
   857  		// meta tag for a different package
   858  		`<meta name="go-import" content="alice.org/other git https://github.com/alice/other">` +
   859  		`</head>` +
   860  		// go-import outside of head
   861  		`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
   862  
   863  	// go-source repo defaults to go-import
   864  	"http://alice.org/pkg/default": `<head>
   865  		<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">
   866  		<meta name="go-source" content="alice.org/pkg _ foo bar">
   867  	</head>`,
   868  	// Package at root of a Git repo.
   869  	"https://bob.com/pkg": `<head> <meta name="go-import" content="bob.com/pkg git https://vcs.net/bob/pkg.git">`,
   870  	// Package at in sub-directory of a Git repo.
   871  	"https://bob.com/pkg/sub": `<head> <meta name="go-import" content="bob.com/pkg git https://vcs.net/bob/pkg.git">`,
   872  	"https://bob.com/bad/github": `
   873  		<head><meta name="go-import" content="bob.com/bad/github git https://github.com/bob/bad/&quot;&gt;$">`,
   874  	"https://bob.com/bad/apache": `
   875  		<head><meta name="go-import" content="bob.com/bad/apache git https://git.apache.org/&gt;$">`,
   876  	// Package with go-source meta tag, where {file} appears on the right of '#' in the file field URL template.
   877  	"https://azul3d.org/examples/abs": `<!DOCTYPE html><html><head>` +
   878  		`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
   879  		`<meta name="go-import" content="azul3d.org/examples git https://github.com/azul3d/examples">` +
   880  		`<meta name="go-source" content="azul3d.org/examples https://github.com/azul3d/examples https://gotools.org/azul3d.org/examples{/dir} https://gotools.org/azul3d.org/examples{/dir}#{file}-L{line}">` +
   881  		`<meta http-equiv="refresh" content="0; url=https://godoc.org/azul3d.org/examples/abs">` +
   882  		`</head>`,
   883  
   884  	// Multiple go-import meta tags; one of which is a vgo-special mod vcs type
   885  	"http://myitcv.io/blah2": `<!DOCTYPE html><html><head>` +
   886  		`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
   887  		`<meta name="go-import" content="myitcv.io/blah2 git https://github.com/myitcv/x">` +
   888  		`<meta name="go-import" content="myitcv.io/blah2 mod https://raw.githubusercontent.com/myitcv/pubx/master">` +
   889  		`</head>`,
   890  }
   891  
   892  func TestURLTemplates(t *testing.T) {
   893  	// Check that templates contain the right variables.
   894  
   895  	for _, p := range patterns {
   896  		if strings.Contains(p.pattern, "blitiri") {
   897  			continue
   898  		}
   899  		check := func(tmpl string, vars ...string) {
   900  			if tmpl == "" {
   901  				return
   902  			}
   903  			for _, v := range vars {
   904  				w := "{" + v + "}"
   905  				if !strings.Contains(tmpl, w) {
   906  					t.Errorf("in pattern %s, template %q is missing %s", p.pattern, tmpl, w)
   907  				}
   908  			}
   909  		}
   910  
   911  		check(p.templates.Directory, "commit")
   912  		check(p.templates.File, "commit")
   913  		check(p.templates.Line, "commit", "line")
   914  		check(p.templates.Raw, "commit", "file")
   915  	}
   916  }
   917  
   918  func TestMatchLegacyTemplates(t *testing.T) {
   919  	for _, test := range []struct {
   920  		sm                     sourceMeta
   921  		wantTemplates          urlTemplates
   922  		wantTransformCommitNil bool
   923  	}{
   924  		{
   925  			sm:                     sourceMeta{"", "", "", "https://git.blindage.org/21h/hcloud-dns/src/branch/master{/dir}/{file}#L{line}"},
   926  			wantTemplates:          giteaURLTemplates,
   927  			wantTransformCommitNil: false,
   928  		},
   929  		{
   930  			sm:                     sourceMeta{"", "", "", "https://git.lastassault.de/sup/networkoverlap/-/blob/master{/dir}/{file}#L{line}"},
   931  			wantTemplates:          gitlabURLTemplates,
   932  			wantTransformCommitNil: true,
   933  		},
   934  		{
   935  			sm:                     sourceMeta{"", "", "", "https://git.borago.de/Marco/gqltest/src/master{/dir}/{file}#L{line}"},
   936  			wantTemplates:          giteaURLTemplates,
   937  			wantTransformCommitNil: true,
   938  		},
   939  		{
   940  			sm:                     sourceMeta{"", "", "", "https://git.zx2c4.com/wireguard-windows/tree{/dir}/{file}#n{line}"},
   941  			wantTemplates:          fdioURLTemplates,
   942  			wantTransformCommitNil: false,
   943  		},
   944  		{
   945  			sm: sourceMeta{"", "", "unknown{/dir}", "unknown{/dir}/{file}#L{line}"},
   946  			wantTemplates: urlTemplates{
   947  				Repo:      "",
   948  				Directory: "unknown/{dir}",
   949  				File:      "unknown/{file}",
   950  				Line:      "unknown/{file}#L{line}",
   951  			},
   952  			wantTransformCommitNil: true,
   953  		},
   954  	} {
   955  		gotTemplates, gotTransformCommit := matchLegacyTemplates(context.Background(), &test.sm)
   956  		gotNil := gotTransformCommit == nil
   957  		if gotTemplates != test.wantTemplates || gotNil != test.wantTransformCommitNil {
   958  			t.Errorf("%+v:\ngot  (%+v, %t)\nwant (%+v, %t)",
   959  				test.sm, gotTemplates, gotNil, test.wantTemplates, test.wantTransformCommitNil)
   960  		}
   961  	}
   962  }