cos-dkms: Add test cases for --force

BUG=None
TEST=presubmit

Change-Id: Ib4b9ac208d218f7af8ab49b13096a75bc80d5263
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/111330
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Tested-by: Kevin Berry <kpberry@google.com>
Reviewed-by: Angel Adetula <angeladetula@google.com>
diff --git a/src/pkg/dkms/dkms_test.go b/src/pkg/dkms/dkms_test.go
index 5c01f47..adcfcb9 100644
--- a/src/pkg/dkms/dkms_test.go
+++ b/src/pkg/dkms/dkms_test.go
@@ -21,10 +21,12 @@
 
 // This is used by TestTransitions and TestCachedTransitions to ensure that
 // DKMS state transitions behave as expected.
-var transitions = []struct {
+type testTransition struct {
 	action         Action
 	expectedStatus PackageStatus
-}{
+}
+
+var baseTransitions = []testTransition{
 	// tests +1 transition cases
 	{add, Added},
 	{build, Built},
@@ -60,8 +62,11 @@
 	// tests -3 transition cases
 	{install, Installed},
 	{remove, Broken},
+}
 
-	// tests no-op cases
+// test cases where the state is not affected because the target
+// state is already reached/passed
+var noOpTransitions = []testTransition{
 	{remove, Broken},
 	{unbuild, Broken},
 	{uninstall, Broken},
@@ -82,6 +87,42 @@
 	{install, Installed},
 }
 
+// test cases that are no-ops even when options.Force is true
+var noOpTransitionsForce = []testTransition{
+	{remove, Broken},
+	{unbuild, Broken},
+	{uninstall, Broken},
+
+	{add, Added},
+	{add, Added},
+	{unbuild, Added},
+	{uninstall, Added},
+
+	{build, Built},
+	{build, Built},
+	{uninstall, Built},
+
+	{install, Installed},
+	{install, Installed},
+}
+
+// these test cases are no-ops by default because they would transition from
+// a higher state to a lower state, but with options.Force, the state is
+// lowered before performing the action
+var loweringTransitionsForce = []testTransition{
+	{build, Built},
+	{add, Added}, // forced Add will remove/unbuild/uninstall, then add
+
+	{install, Installed},
+	{add, Added}, // forced Add will remove/unbuild/uninstall, then add
+
+	{install, Installed},
+	{build, Built}, // forced Build will unbuild/uninstall, then build
+}
+
+var transitions = append(baseTransitions, noOpTransitions...)
+var transitionsForce = append(append(baseTransitions, noOpTransitionsForce...), loweringTransitionsForce...)
+
 func TestTransitions(t *testing.T) {
 	options := &Options{
 		Force:         false,
@@ -108,6 +149,60 @@
 	}
 
 	for _, transition := range transitions {
+		t.Logf("testing transition %v", transition)
+		var err error
+		switch transition.action {
+		case add:
+			err = Add(pkg, options)
+		case build:
+			err = Build(pkg, options)
+		case install:
+			err = Install(pkg, options)
+		case remove:
+			err = Remove(pkg)
+		case unbuild:
+			err = Unbuild(pkg)
+		case uninstall:
+			err = Uninstall(pkg)
+		}
+
+		if err != nil {
+			t.Fatalf("%v", err)
+		}
+
+		status := Status(pkg)
+		if status != transition.expectedStatus {
+			t.Fatalf("status was expected to be %v; got %v instead", transition.expectedStatus, status)
+		}
+	}
+}
+
+func TestTransitionsForce(t *testing.T) {
+	options := &Options{
+		// setting Force should also skip the version check, so we don't need
+		// to pass ForceVersionOverride
+		Force:         true,
+		MakeVariables: "",
+	}
+
+	trees := &Trees{
+		Dkms:    t.TempDir(),
+		Source:  "testdata/source-tree",
+		Kernel:  "testdata/kernel-sources",
+		Install: t.TempDir(),
+	}
+
+	pkg := &Package{
+		Name:          "mymodule",
+		Version:       "1.0",
+		KernelVersion: "6.1.100",
+		Arch:          "x86_64",
+		BuildId:       "18244.151.14",
+		Trees:         trees,
+	}
+
+	for _, transition := range transitionsForce {
+		t.Logf("testing transition %v", transition)
 		var err error
 		switch transition.action {
 		case add: