[machine] Add Windows CPU interrogation to TMM.

It turns out we can use KernelArch() for all 3 platforms, which lets us
factor that up to crossplatform.CPUs().

Factor cpuModel() out of crossplatform.CPUs() so we can unit-test it.

isaAndBitness() can be private now; we didn't end up calling it from
Windows-specific code.

Change-Id: I79dc5ffa869aba99f8fa9c9eddaa670fb5d21333
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/576156
Commit-Queue: Erik Rose <erikrose@google.com>
Reviewed-by: Eric Boren <borenet@google.com>
diff --git a/machine/go/test_machine_monitor/standalone/BUILD.bazel b/machine/go/test_machine_monitor/standalone/BUILD.bazel
index 8d8095a..d899ef5 100644
--- a/machine/go/test_machine_monitor/standalone/BUILD.bazel
+++ b/machine/go/test_machine_monitor/standalone/BUILD.bazel
@@ -43,6 +43,7 @@
         ],
         "@io_bazel_rules_go//go/platform:windows": [
             "//go/skerr",
+            "//machine/go/test_machine_monitor/standalone/crossplatform",
             "//machine/go/test_machine_monitor/standalone/windows",
             "@com_github_shirou_gopsutil//host",
         ],
diff --git a/machine/go/test_machine_monitor/standalone/crossplatform/BUILD.bazel b/machine/go/test_machine_monitor/standalone/crossplatform/BUILD.bazel
index d3d768e..587a423 100644
--- a/machine/go/test_machine_monitor/standalone/crossplatform/BUILD.bazel
+++ b/machine/go/test_machine_monitor/standalone/crossplatform/BUILD.bazel
@@ -6,7 +6,10 @@
     srcs = ["crossplatform.go"],
     importpath = "go.skia.org/infra/machine/go/test_machine_monitor/standalone/crossplatform",
     visibility = ["//visibility:public"],
-    deps = ["//go/skerr"],
+    deps = [
+        "//go/skerr",
+        "@com_github_shirou_gopsutil//host",
+    ],
 )
 
 go_test(
@@ -15,6 +18,8 @@
     embed = [":crossplatform"],
     deps = [
         "//go/testutils/unittest",
+        "@com_github_shirou_gopsutil//host",
         "@com_github_stretchr_testify//assert",
+        "@com_github_stretchr_testify//require",
     ],
 )
diff --git a/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform.go b/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform.go
index 5da0ca0..19befce 100644
--- a/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform.go
+++ b/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform.go
@@ -7,12 +7,13 @@
 	"strconv"
 	"strings"
 
+	"github.com/shirou/gopsutil/host"
 	"go.skia.org/infra/go/skerr"
 )
 
-// ISAAndBitness, given an architecture like "x86_64", extracts both an instruction set architecture
+// isaAndBitness, given an architecture like "x86_64", extracts both an instruction set architecture
 // and bit width, e.g. "x86" and "64".
-func ISAAndBitness(arch string) (isa, bitness string, err error) {
+func isaAndBitness(arch string) (isa, bitness string, err error) {
 	// gopsutil can spit out i386, i686, arm, aarch64, ia64, or x86_64 under Windows; "" as a
 	// fallback; whatever uname's utsname.machine struct member (a string) has on POSIX. On our lab
 	// Macs, that's arm64 or x64_86 (exposed by uname -m). Our lab Linuxes: x86_64.
@@ -57,26 +58,36 @@
 	return ""
 }
 
+// cpuModel takes a vendor name and a brand string (see CPUs()) and synthesizes a descriptive model
+// string from the two. If it fails to do so, it returns "".
+func cpuModel(vendor, brandString string) string {
+	if vendor == "GenuineIntel" {
+		return intelModel(brandString)
+	} else {
+		return strings.ReplaceAll(brandString, " ", "_")
+	}
+}
+
 // CPUs is the brains behind various platform-specific CPUs() functions, broken off for testing. It
 // takes the architecture, vendor name and a "brand string", which is a model signifier whose format
 // is vendor-specific, and returns a Swarming-style description of the host's CPU, in various
 // precisions, e.g. ["x86", "x86-64", "x86-64-i5-5350U"]. The first (ISA) and second (bit width)
 // will always be returned (if returned error is nil). The third (model number) will be added if we
 // succeed in extracting it.
-func CPUs(arch string, vendor string, brandString string) ([]string, error) {
-	isa, bitness, err := ISAAndBitness(arch)
+func CPUs(vendor, brandString string) ([]string, error) {
+	arch, err := host.KernelArch()
+	if err != nil {
+		return nil, skerr.Wrapf(err, "failed to get CPU architecture")
+	}
+
+	isa, bitness, err := isaAndBitness(arch)
 	if err != nil {
 		return nil, skerr.Wrap(err)
 	}
 
 	ret := []string{isa, fmt.Sprintf("%s-%s", isa, bitness)}
 
-	var model string
-	if vendor == "GenuineIntel" {
-		model = intelModel(brandString)
-	} else {
-		model = strings.ReplaceAll(brandString, " ", "_")
-	}
+	model := cpuModel(vendor, brandString)
 	if model != "" {
 		ret = append(ret, fmt.Sprintf("%s-%s-%s", isa, bitness, model))
 	}
diff --git a/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform_test.go b/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform_test.go
index a869d8e..163b630 100644
--- a/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform_test.go
+++ b/machine/go/test_machine_monitor/standalone/crossplatform/crossplatform_test.go
@@ -3,68 +3,92 @@
 import (
 	"testing"
 
+	"github.com/shirou/gopsutil/host"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.skia.org/infra/go/testutils/unittest"
 )
 
-func assertCPUDimensions(t *testing.T, arch string, vendor string, brandString string, expected []string, failureMessage string) {
-	dimensions, err := CPUs(arch, vendor, brandString)
-	assert.NoError(t, err)
-	assert.Equal(t, expected, dimensions, failureMessage)
+func assertIsaAndBitnessEqual(t *testing.T, arch, expectedIsa, expectedBitness string) {
+	isa, bitness, err := isaAndBitness(arch)
+	require.NoError(t, err)
+	assert.Equal(t, expectedIsa, isa)
+	assert.Equal(t, expectedBitness, bitness)
 }
 
-func TestCPUs_ParsingAndBitWidthAndArchMapping(t *testing.T) {
+func TestIsaAndBitness(t *testing.T) {
 	unittest.SmallTest(t)
-	assertCPUDimensions(
+	assertIsaAndBitnessEqual(t, "x86_64", "x86", "64")
+	assertIsaAndBitnessEqual(t, "amd64", "x86", "64")
+	assertIsaAndBitnessEqual(t, "aarch64", "arm64", "64")
+
+	_, _, err := isaAndBitness("kersmoo")
+	assert.Error(t, err, "An unknown CPU architecture should result in an error (and we should add it to the mapping).")
+}
+
+func assertCPUModelEqual(t *testing.T, vendor, brandString, expected, failureMessage string) {
+	assert.Equal(t, expected, cpuModel(vendor, brandString), failureMessage)
+}
+
+func TestCPUModel(t *testing.T) {
+	unittest.SmallTest(t)
+	assertCPUModelEqual(
 		t,
-		"x86_64",
 		"GenuineIntel",
 		"Intel(R) Core(TM) i7-9750H v2 CPU @ 2.60GHz",
-		[]string{"x86", "x86-64", "x86-64-i7-9750H v2"},
-		"x86_64 should be recognized as x86 ISA, and Intel model numbers should be extracted.",
+		"i7-9750H v2",
+		"Intel model numbers should be extracted.",
 	)
-	assertCPUDimensions(
+	assertCPUModelEqual(
 		t,
-		"amd64",
 		"Wackadoo Inc.",
 		"Wackadoo ALU i5-9600",
-		[]string{"x86", "x86-64", "x86-64-Wackadoo_ALU_i5-9600"},
-		"amd64 should be recognized as x86 ISA, and non-Intel model numbers should be smooshed into snake_case.",
+		"Wackadoo_ALU_i5-9600",
+		"Non-Intel model numbers should be smooshed into snake_case.",
 	)
-	assertCPUDimensions(
+	assertCPUModelEqual(
 		t,
-		"aarch64",
 		"GenuineIntel",
 		"something it fails to extract anything from",
-		[]string{"arm64", "arm64-64"},
-		"aarch64 should be recognized as arm64 ISA, and an unrecognizable Intel brand string should result in no third element.",
-	)
-	assertCPUDimensions(
-		t,
-		"arm64",
 		"",
-		"",
-		[]string{"arm64", "arm64-64"},
-		"Empty vendor and brand string should result in no third element.",
+		"An unrecognizable Intel brand string should result in no extracted model.",
 	)
-	assertCPUDimensions(
+	assertCPUModelEqual(
 		t,
-		"arm64",
 		"",
 		"Wackadoo ALU",
-		[]string{"arm64", "arm64-64", "arm64-64-Wackadoo_ALU"},
-		"Empty vendor and full brand string should result in smooshed brand string for third element.",
+		"Wackadoo_ALU",
+		"An unrecognizable Intel brand string should result in no extracted model.",
 	)
 }
 
-func TestCPUs_UnrecognizedArch_ReturnsError(t *testing.T) {
+func TestCPUs(t *testing.T) {
 	unittest.SmallTest(t)
-	_, err := CPUs(
-		"kersmoo",
-		"GenuineIntel",
-		"Intel(R) Core(TM) i7-9750H v2 CPU @ 2.60GHz",
+
+	arch, err := host.KernelArch()
+	require.NoError(t, err)
+	isa, bitness, err := isaAndBitness(arch)
+	require.NoError(t, err)
+	vendor := "GenuineIntel"
+	brandString := "Intel(R) Core(TM) i7-9750H v2 CPU @ 2.60GHz"
+	model := cpuModel(vendor, brandString)
+
+	dimensions, err := CPUs(vendor, brandString)
+	require.NoError(t, err)
+	assert.Equal(
+		t,
+		[]string{isa, isa + "-" + bitness, isa + "-" + bitness + "-" + model},
+		dimensions,
+		"If a model can be extracted, there should be a third slice element containing it.",
 	)
-	assert.Error(t, err, "An unknown CPU architecture should result in an error (and we should add it to the mapping).")
+	dimensions, err = CPUs("", "")
+	require.NoError(t, err)
+	assert.Equal(
+		t,
+		[]string{isa, isa + "-" + bitness},
+		dimensions,
+		"Empty vendor and brand string should result in no third element.",
+	)
 }
 
 func TestVersionsOfAllPrecisions(t *testing.T) {
diff --git a/machine/go/test_machine_monitor/standalone/standalone_darwin.go b/machine/go/test_machine_monitor/standalone/standalone_darwin.go
index 06ff1b1..9bf9c04 100644
--- a/machine/go/test_machine_monitor/standalone/standalone_darwin.go
+++ b/machine/go/test_machine_monitor/standalone/standalone_darwin.go
@@ -23,14 +23,10 @@
 
 // CPUs returns a Swarming-style description of the host's CPU, in various precisions.
 func CPUs(ctx context.Context) ([]string, error) {
-	arch, err := host.KernelArch()
-	if err != nil {
-		return nil, skerr.Wrapf(err, "failed to get Mac CPU architecture")
-	}
 	// It is perfectly normal for these sysctl keys to be missing sometimes:
 	vendor, _ := unix.Sysctl("machdep.cpu.vendor") // Sysctl returns "" on failure.
 	brandString, _ := unix.Sysctl("machdep.cpu.brand_string")
-	return crossplatform.CPUs(arch, vendor, brandString)
+	return crossplatform.CPUs(vendor, brandString)
 }
 
 // GPUs returns Swarming-style descriptions of all the host's GPUs, in various precisions, all
diff --git a/machine/go/test_machine_monitor/standalone/standalone_linux.go b/machine/go/test_machine_monitor/standalone/standalone_linux.go
index 1d362cb..2d22cef 100644
--- a/machine/go/test_machine_monitor/standalone/standalone_linux.go
+++ b/machine/go/test_machine_monitor/standalone/standalone_linux.go
@@ -27,11 +27,6 @@
 // kernel, we do not. None of our jobs care about that distinction, nor, I think, do any of our
 // boxes run like that.
 func CPUs(ctx context.Context) ([]string, error) {
-	arch, err := host.KernelArch()
-	if err != nil {
-		return nil, skerr.Wrapf(err, "failed to get Linux CPU architecture")
-	}
-
 	procFile, err := os.Open("/proc/cpuinfo")
 	if err != nil {
 		return nil, skerr.Wrap(err)
@@ -43,7 +38,7 @@
 		return nil, skerr.Wrapf(err, "failed to get vendor and brand string")
 	}
 
-	return crossplatform.CPUs(arch, vendor, brandString)
+	return crossplatform.CPUs(vendor, brandString)
 }
 
 // nvidiaVersion returns the version of the installed Nvidia GPU driver, "" if not available.
diff --git a/machine/go/test_machine_monitor/standalone/standalone_windows.go b/machine/go/test_machine_monitor/standalone/standalone_windows.go
index b2fe6e5..c324040 100644
--- a/machine/go/test_machine_monitor/standalone/standalone_windows.go
+++ b/machine/go/test_machine_monitor/standalone/standalone_windows.go
@@ -5,6 +5,7 @@
 
 	"github.com/shirou/gopsutil/host"
 	"go.skia.org/infra/go/skerr"
+	"go.skia.org/infra/machine/go/test_machine_monitor/standalone/crossplatform"
 	"go.skia.org/infra/machine/go/test_machine_monitor/standalone/windows"
 )
 
@@ -21,8 +22,7 @@
 }
 
 func CPUs(ctx context.Context) ([]string, error) {
-	var ret []string
-	return ret, nil
+	return crossplatform.CPUs("", "")
 }
 
 func GPUs(ctx context.Context) ([]string, error) {