internal/worker: add hermetic test for analysis scan

Add a test that covers nearly all of the analysis scan code,
but does not touch the network.

- Use the proxytest package (copied from pkgsite a while ago
  but never used here) to get a fake proxy.

- Pass an openFileFunc to copy the binary from the local disk
  instead of GCS.

Change-Id: I357a9a6b6bffa689bbf1185c1c5a758c84078796
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/474545
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/worker/analysis_test.go b/internal/worker/analysis_test.go
index 39ea342..1b06ac2 100644
--- a/internal/worker/analysis_test.go
+++ b/internal/worker/analysis_test.go
@@ -5,12 +5,20 @@
 package worker
 
 import (
+	"context"
+	"errors"
+	"io"
+	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"golang.org/x/pkgsite-metrics/internal/analysis"
 	"golang.org/x/pkgsite-metrics/internal/buildtest"
+	"golang.org/x/pkgsite-metrics/internal/config"
+	"golang.org/x/pkgsite-metrics/internal/proxy/proxytest"
 	"golang.org/x/pkgsite-metrics/internal/queue"
 	"golang.org/x/pkgsite-metrics/internal/scan"
 )
@@ -85,3 +93,96 @@
 		t.Errorf("mismatch (-want +got):\n%s", diff)
 	}
 }
+
+func TestAnalysisScan(t *testing.T) {
+	const (
+		binary     = "./analyzer"
+		modulePath = "a.com/m"
+		version    = "v1.2.3"
+	)
+	binaryPath, cleanup := buildtest.GoBuild(t, "testdata/analyzer", "")
+	defer cleanup()
+	proxyClient, cleanup2 := proxytest.SetupTestClient(t, []*proxytest.Module{
+		{
+			ModulePath: modulePath,
+			Version:    version,
+			Files: map[string]string{
+				"go.mod": `module ` + modulePath,
+				"a.go": `
+package p
+func F()  { G() }
+func G() {}
+`},
+		},
+	})
+	defer cleanup2()
+
+	diff := func(want, got *analysis.Result) {
+		t.Helper()
+		d := cmp.Diff(want, got,
+			cmpopts.IgnoreFields(analysis.WorkVersion{}, "BinaryVersion", "SchemaVersion"),
+			cmpopts.IgnoreFields(analysis.Diagnostic{}, "Position"))
+		if d != "" {
+			t.Errorf("mismatch (-want, +got)\n%s", d)
+		}
+	}
+
+	s := &analysisServer{
+		Server: &Server{
+			proxyClient: proxyClient,
+			cfg: &config.Config{
+				BinaryBucket: "unused",
+			},
+		},
+		openFile: func(name string) (io.ReadCloser, error) {
+			if name == "analysis-binaries/analyzer" {
+				return os.Open(binaryPath)
+			}
+			return nil, errors.New("bad name")
+		},
+	}
+	req := &analysis.ScanRequest{
+		ModuleURLPath: scan.ModuleURLPath{Module: modulePath, Version: version},
+		ScanParams: analysis.ScanParams{
+			Binary:   "analyzer",
+			Args:     "-name G",
+			Insecure: true,
+		},
+	}
+	got := s.scan(context.Background(), req)
+	want := &analysis.Result{
+		ModulePath:    modulePath,
+		Version:       version,
+		SortVersion:   "1,2,3~",
+		CommitTime:    proxytest.CommitTime,
+		BinaryName:    "analyzer",
+		WorkVersion:   analysis.WorkVersion{BinaryArgs: "-name G"},
+		Error:         "",
+		ErrorCategory: "",
+		Diagnostics: []*analysis.Diagnostic{
+			{
+				PackageID:    "a.com/m",
+				AnalyzerName: "findcall",
+				Message:      "call of G(...)",
+			},
+		},
+	}
+	diff(want, got)
+
+	// Test that errors are put into the Result.
+	req.Binary = "bad"
+	got = s.scan(context.Background(), req)
+	// Trim varying part of error.
+	if i := strings.LastIndexByte(got.Error, ':'); i > 0 {
+		got.Error = got.Error[i+2:]
+	}
+	want = &analysis.Result{
+		ModulePath:  modulePath,
+		Version:     version,
+		SortVersion: "1,2,3~",
+		BinaryName:  "bad",
+		WorkVersion: analysis.WorkVersion{BinaryArgs: "-name G"},
+		Error:       "bad name",
+	}
+	diff(want, got)
+}