Move transition code to transition.go

Split the transition code out of mutator.go in preparation for adding
more complexity in TransitionMutator.

Bug: 372543712
Test: builds
Change-Id: I30f82aea1f296e0db365e0ca3606ef6a4c6cb778
diff --git a/android/Android.bp b/android/Android.bp
index dfea8f9..20cd28b 100644
--- a/android/Android.bp
+++ b/android/Android.bp
@@ -109,6 +109,7 @@
         "test_asserts.go",
         "test_suites.go",
         "testing.go",
+        "transition.go",
         "util.go",
         "variable.go",
         "vendor_api_levels.go",
@@ -154,6 +155,7 @@
         "singleton_module_test.go",
         "soong_config_modules_test.go",
         "test_suites_test.go",
+        "transition_test.go",
         "util_test.go",
         "variable_test.go",
         "vintf_fragment_test.go",
diff --git a/android/mutator.go b/android/mutator.go
index fdd16a8..1523794 100644
--- a/android/mutator.go
+++ b/android/mutator.go
@@ -337,248 +337,6 @@
 	return mutator
 }
 
-type IncomingTransitionContext interface {
-	ArchModuleContext
-	ModuleProviderContext
-	ModuleErrorContext
-
-	// Module returns the target of the dependency edge for which the transition
-	// is being computed
-	Module() Module
-
-	// Config returns the configuration for the build.
-	Config() Config
-
-	DeviceConfig() DeviceConfig
-
-	// IsAddingDependency returns true if the transition is being called while adding a dependency
-	// after the transition mutator has already run, or false if it is being called when the transition
-	// mutator is running.  This should be used sparingly, all uses will have to be removed in order
-	// to support creating variants on demand.
-	IsAddingDependency() bool
-}
-
-type OutgoingTransitionContext interface {
-	ArchModuleContext
-	ModuleProviderContext
-
-	// Module returns the target of the dependency edge for which the transition
-	// is being computed
-	Module() Module
-
-	// DepTag() Returns the dependency tag through which this dependency is
-	// reached
-	DepTag() blueprint.DependencyTag
-
-	// Config returns the configuration for the build.
-	Config() Config
-
-	DeviceConfig() DeviceConfig
-}
-
-// TransitionMutator implements a top-down mechanism where a module tells its
-// direct dependencies what variation they should be built in but the dependency
-// has the final say.
-//
-// When implementing a transition mutator, one needs to implement four methods:
-//   - Split() that tells what variations a module has by itself
-//   - OutgoingTransition() where a module tells what it wants from its
-//     dependency
-//   - IncomingTransition() where a module has the final say about its own
-//     variation
-//   - Mutate() that changes the state of a module depending on its variation
-//
-// That the effective variation of module B when depended on by module A is the
-// composition the outgoing transition of module A and the incoming transition
-// of module B.
-//
-// the outgoing transition should not take the properties of the dependency into
-// account, only those of the module that depends on it. For this reason, the
-// dependency is not even passed into it as an argument. Likewise, the incoming
-// transition should not take the properties of the depending module into
-// account and is thus not informed about it. This makes for a nice
-// decomposition of the decision logic.
-//
-// A given transition mutator only affects its own variation; other variations
-// stay unchanged along the dependency edges.
-//
-// Soong makes sure that all modules are created in the desired variations and
-// that dependency edges are set up correctly. This ensures that "missing
-// variation" errors do not happen and allows for more flexible changes in the
-// value of the variation among dependency edges (as oppposed to bottom-up
-// mutators where if module A in variation X depends on module B and module B
-// has that variation X, A must depend on variation X of B)
-//
-// The limited power of the context objects passed to individual mutators
-// methods also makes it more difficult to shoot oneself in the foot. Complete
-// safety is not guaranteed because no one prevents individual transition
-// mutators from mutating modules in illegal ways and for e.g. Split() or
-// Mutate() to run their own visitations of the transitive dependency of the
-// module and both of these are bad ideas, but it's better than no guardrails at
-// all.
-//
-// This model is pretty close to Bazel's configuration transitions. The mapping
-// between concepts in Soong and Bazel is as follows:
-//   - Module == configured target
-//   - Variant == configuration
-//   - Variation name == configuration flag
-//   - Variation == configuration flag value
-//   - Outgoing transition == attribute transition
-//   - Incoming transition == rule transition
-//
-// The Split() method does not have a Bazel equivalent and Bazel split
-// transitions do not have a Soong equivalent.
-//
-// Mutate() does not make sense in Bazel due to the different models of the
-// two systems: when creating new variations, Soong clones the old module and
-// thus some way is needed to change it state whereas Bazel creates each
-// configuration of a given configured target anew.
-type TransitionMutator interface {
-	// Split returns the set of variations that should be created for a module no
-	// matter who depends on it. Used when Make depends on a particular variation
-	// or when the module knows its variations just based on information given to
-	// it in the Blueprint file. This method should not mutate the module it is
-	// called on.
-	Split(ctx BaseModuleContext) []string
-
-	// OutgoingTransition is called on a module to determine which variation it wants
-	// from its direct dependencies. The dependency itself can override this decision.
-	// This method should not mutate the module itself.
-	OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string
-
-	// IncomingTransition is called on a module to determine which variation it should
-	// be in based on the variation modules that depend on it want. This gives the module
-	// a final say about its own variations. This method should not mutate the module
-	// itself.
-	IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string
-
-	// Mutate is called after a module was split into multiple variations on each variation.
-	// It should not split the module any further but adding new dependencies is
-	// fine. Unlike all the other methods on TransitionMutator, this method is
-	// allowed to mutate the module.
-	Mutate(ctx BottomUpMutatorContext, variation string)
-}
-
-type androidTransitionMutator struct {
-	finalPhase bool
-	mutator    TransitionMutator
-	name       string
-}
-
-func (a *androidTransitionMutator) Split(ctx blueprint.BaseModuleContext) []string {
-	if a.finalPhase {
-		panic("TransitionMutator not allowed in FinalDepsMutators")
-	}
-	if m, ok := ctx.Module().(Module); ok {
-		moduleContext := m.base().baseModuleContextFactory(ctx)
-		return a.mutator.Split(&moduleContext)
-	} else {
-		return []string{""}
-	}
-}
-
-type outgoingTransitionContextImpl struct {
-	archModuleContext
-	bp blueprint.OutgoingTransitionContext
-}
-
-func (c *outgoingTransitionContextImpl) Module() Module {
-	return c.bp.Module().(Module)
-}
-
-func (c *outgoingTransitionContextImpl) DepTag() blueprint.DependencyTag {
-	return c.bp.DepTag()
-}
-
-func (c *outgoingTransitionContextImpl) Config() Config {
-	return c.bp.Config().(Config)
-}
-
-func (c *outgoingTransitionContextImpl) DeviceConfig() DeviceConfig {
-	return DeviceConfig{c.bp.Config().(Config).deviceConfig}
-}
-
-func (c *outgoingTransitionContextImpl) provider(provider blueprint.AnyProviderKey) (any, bool) {
-	return c.bp.Provider(provider)
-}
-
-func (a *androidTransitionMutator) OutgoingTransition(bpctx blueprint.OutgoingTransitionContext, sourceVariation string) string {
-	if m, ok := bpctx.Module().(Module); ok {
-		ctx := outgoingTransitionContextPool.Get().(*outgoingTransitionContextImpl)
-		defer outgoingTransitionContextPool.Put(ctx)
-		*ctx = outgoingTransitionContextImpl{
-			archModuleContext: m.base().archModuleContextFactory(bpctx),
-			bp:                bpctx,
-		}
-		return a.mutator.OutgoingTransition(ctx, sourceVariation)
-	} else {
-		return ""
-	}
-}
-
-type incomingTransitionContextImpl struct {
-	archModuleContext
-	bp blueprint.IncomingTransitionContext
-}
-
-func (c *incomingTransitionContextImpl) Module() Module {
-	return c.bp.Module().(Module)
-}
-
-func (c *incomingTransitionContextImpl) Config() Config {
-	return c.bp.Config().(Config)
-}
-
-func (c *incomingTransitionContextImpl) DeviceConfig() DeviceConfig {
-	return DeviceConfig{c.bp.Config().(Config).deviceConfig}
-}
-
-func (c *incomingTransitionContextImpl) IsAddingDependency() bool {
-	return c.bp.IsAddingDependency()
-}
-
-func (c *incomingTransitionContextImpl) provider(provider blueprint.AnyProviderKey) (any, bool) {
-	return c.bp.Provider(provider)
-}
-
-func (c *incomingTransitionContextImpl) ModuleErrorf(fmt string, args ...interface{}) {
-	c.bp.ModuleErrorf(fmt, args)
-}
-
-func (c *incomingTransitionContextImpl) PropertyErrorf(property, fmt string, args ...interface{}) {
-	c.bp.PropertyErrorf(property, fmt, args)
-}
-
-func (a *androidTransitionMutator) IncomingTransition(bpctx blueprint.IncomingTransitionContext, incomingVariation string) string {
-	if m, ok := bpctx.Module().(Module); ok {
-		ctx := incomingTransitionContextPool.Get().(*incomingTransitionContextImpl)
-		defer incomingTransitionContextPool.Put(ctx)
-		*ctx = incomingTransitionContextImpl{
-			archModuleContext: m.base().archModuleContextFactory(bpctx),
-			bp:                bpctx,
-		}
-		return a.mutator.IncomingTransition(ctx, incomingVariation)
-	} else {
-		return ""
-	}
-}
-
-func (a *androidTransitionMutator) Mutate(ctx blueprint.BottomUpMutatorContext, variation string) {
-	if am, ok := ctx.Module().(Module); ok {
-		if variation != "" {
-			// TODO: this should really be checking whether the TransitionMutator affected this module, not
-			//  the empty variant, but TransitionMutator has no concept of skipping a module.
-			base := am.base()
-			base.commonProperties.DebugMutators = append(base.commonProperties.DebugMutators, a.name)
-			base.commonProperties.DebugVariations = append(base.commonProperties.DebugVariations, variation)
-		}
-
-		mctx := bottomUpMutatorContextFactory(ctx, am, a.finalPhase)
-		defer bottomUpMutatorContextPool.Put(mctx)
-		a.mutator.Mutate(mctx, variation)
-	}
-}
-
 func (x *registerMutatorsContext) Transition(name string, m TransitionMutator) TransitionMutatorHandle {
 	atm := &androidTransitionMutator{
 		finalPhase: x.finalPhase,
diff --git a/android/mutator_test.go b/android/mutator_test.go
index 1d5f890..60a6119 100644
--- a/android/mutator_test.go
+++ b/android/mutator_test.go
@@ -83,132 +83,6 @@
 	AssertDeepEquals(t, "foo missing deps", []string{"added_missing_dep", "regular_missing_dep"}, foo.missingDeps)
 }
 
-type testTransitionMutator struct {
-	split              func(ctx BaseModuleContext) []string
-	outgoingTransition func(ctx OutgoingTransitionContext, sourceVariation string) string
-	incomingTransition func(ctx IncomingTransitionContext, incomingVariation string) string
-	mutate             func(ctx BottomUpMutatorContext, variation string)
-}
-
-func (t *testTransitionMutator) Split(ctx BaseModuleContext) []string {
-	if t.split != nil {
-		return t.split(ctx)
-	}
-	return []string{""}
-}
-
-func (t *testTransitionMutator) OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string {
-	if t.outgoingTransition != nil {
-		return t.outgoingTransition(ctx, sourceVariation)
-	}
-	return sourceVariation
-}
-
-func (t *testTransitionMutator) IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string {
-	if t.incomingTransition != nil {
-		return t.incomingTransition(ctx, incomingVariation)
-	}
-	return incomingVariation
-}
-
-func (t *testTransitionMutator) Mutate(ctx BottomUpMutatorContext, variation string) {
-	if t.mutate != nil {
-		t.mutate(ctx, variation)
-	}
-}
-
-func TestModuleString(t *testing.T) {
-	bp := `
-		test {
-			name: "foo",
-		}
-	`
-
-	var moduleStrings []string
-
-	GroupFixturePreparers(
-		FixtureRegisterWithContext(func(ctx RegistrationContext) {
-
-			ctx.PreArchMutators(func(ctx RegisterMutatorsContext) {
-				ctx.Transition("pre_arch", &testTransitionMutator{
-					split: func(ctx BaseModuleContext) []string {
-						moduleStrings = append(moduleStrings, ctx.Module().String())
-						return []string{"a", "b"}
-					},
-				})
-			})
-
-			ctx.PreDepsMutators(func(ctx RegisterMutatorsContext) {
-				ctx.Transition("pre_deps", &testTransitionMutator{
-					split: func(ctx BaseModuleContext) []string {
-						moduleStrings = append(moduleStrings, ctx.Module().String())
-						return []string{"c", "d"}
-					},
-				})
-			})
-
-			ctx.PostDepsMutators(func(ctx RegisterMutatorsContext) {
-				ctx.Transition("post_deps", &testTransitionMutator{
-					split: func(ctx BaseModuleContext) []string {
-						moduleStrings = append(moduleStrings, ctx.Module().String())
-						return []string{"e", "f"}
-					},
-					outgoingTransition: func(ctx OutgoingTransitionContext, sourceVariation string) string {
-						return ""
-					},
-				})
-				ctx.BottomUp("rename_bottom_up", func(ctx BottomUpMutatorContext) {
-					moduleStrings = append(moduleStrings, ctx.Module().String())
-					ctx.Rename(ctx.Module().base().Name() + "_renamed1")
-				}).UsesRename()
-				ctx.BottomUp("final", func(ctx BottomUpMutatorContext) {
-					moduleStrings = append(moduleStrings, ctx.Module().String())
-				})
-			})
-
-			ctx.RegisterModuleType("test", mutatorTestModuleFactory)
-		}),
-		FixtureWithRootAndroidBp(bp),
-	).RunTest(t)
-
-	want := []string{
-		// Initial name.
-		"foo{}",
-
-		// After pre_arch (reversed because rename_top_down is TopDown so it visits in reverse order).
-		"foo{pre_arch:b}",
-		"foo{pre_arch:a}",
-
-		// After pre_deps (reversed because post_deps TransitionMutator.Split is TopDown).
-		"foo{pre_arch:b,pre_deps:d}",
-		"foo{pre_arch:b,pre_deps:c}",
-		"foo{pre_arch:a,pre_deps:d}",
-		"foo{pre_arch:a,pre_deps:c}",
-
-		// After post_deps.
-		"foo{pre_arch:a,pre_deps:c,post_deps:e}",
-		"foo{pre_arch:a,pre_deps:c,post_deps:f}",
-		"foo{pre_arch:a,pre_deps:d,post_deps:e}",
-		"foo{pre_arch:a,pre_deps:d,post_deps:f}",
-		"foo{pre_arch:b,pre_deps:c,post_deps:e}",
-		"foo{pre_arch:b,pre_deps:c,post_deps:f}",
-		"foo{pre_arch:b,pre_deps:d,post_deps:e}",
-		"foo{pre_arch:b,pre_deps:d,post_deps:f}",
-
-		// After rename_bottom_up.
-		"foo_renamed1{pre_arch:a,pre_deps:c,post_deps:e}",
-		"foo_renamed1{pre_arch:a,pre_deps:c,post_deps:f}",
-		"foo_renamed1{pre_arch:a,pre_deps:d,post_deps:e}",
-		"foo_renamed1{pre_arch:a,pre_deps:d,post_deps:f}",
-		"foo_renamed1{pre_arch:b,pre_deps:c,post_deps:e}",
-		"foo_renamed1{pre_arch:b,pre_deps:c,post_deps:f}",
-		"foo_renamed1{pre_arch:b,pre_deps:d,post_deps:e}",
-		"foo_renamed1{pre_arch:b,pre_deps:d,post_deps:f}",
-	}
-
-	AssertDeepEquals(t, "module String() values", want, moduleStrings)
-}
-
 func TestFinalDepsPhase(t *testing.T) {
 	bp := `
 		test {
@@ -288,22 +162,3 @@
 
 	AssertDeepEquals(t, "final", finalWant, finalGotMap)
 }
-
-func TestTransitionMutatorInFinalDeps(t *testing.T) {
-	GroupFixturePreparers(
-		FixtureRegisterWithContext(func(ctx RegistrationContext) {
-			ctx.FinalDepsMutators(func(ctx RegisterMutatorsContext) {
-				ctx.Transition("vars", &testTransitionMutator{
-					split: func(ctx BaseModuleContext) []string {
-						return []string{"a", "b"}
-					},
-				})
-			})
-
-			ctx.RegisterModuleType("test", mutatorTestModuleFactory)
-		}),
-		FixtureWithRootAndroidBp(`test {name: "foo"}`),
-	).
-		ExtendWithErrorHandler(FixtureExpectsOneErrorPattern("not allowed in FinalDepsMutators")).
-		RunTest(t)
-}
diff --git a/android/transition.go b/android/transition.go
new file mode 100644
index 0000000..7c04eff
--- /dev/null
+++ b/android/transition.go
@@ -0,0 +1,259 @@
+// Copyright 2024 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android
+
+import "github.com/google/blueprint"
+
+// TransitionMutator implements a top-down mechanism where a module tells its
+// direct dependencies what variation they should be built in but the dependency
+// has the final say.
+//
+// When implementing a transition mutator, one needs to implement four methods:
+//   - Split() that tells what variations a module has by itself
+//   - OutgoingTransition() where a module tells what it wants from its
+//     dependency
+//   - IncomingTransition() where a module has the final say about its own
+//     variation
+//   - Mutate() that changes the state of a module depending on its variation
+//
+// That the effective variation of module B when depended on by module A is the
+// composition the outgoing transition of module A and the incoming transition
+// of module B.
+//
+// the outgoing transition should not take the properties of the dependency into
+// account, only those of the module that depends on it. For this reason, the
+// dependency is not even passed into it as an argument. Likewise, the incoming
+// transition should not take the properties of the depending module into
+// account and is thus not informed about it. This makes for a nice
+// decomposition of the decision logic.
+//
+// A given transition mutator only affects its own variation; other variations
+// stay unchanged along the dependency edges.
+//
+// Soong makes sure that all modules are created in the desired variations and
+// that dependency edges are set up correctly. This ensures that "missing
+// variation" errors do not happen and allows for more flexible changes in the
+// value of the variation among dependency edges (as oppposed to bottom-up
+// mutators where if module A in variation X depends on module B and module B
+// has that variation X, A must depend on variation X of B)
+//
+// The limited power of the context objects passed to individual mutators
+// methods also makes it more difficult to shoot oneself in the foot. Complete
+// safety is not guaranteed because no one prevents individual transition
+// mutators from mutating modules in illegal ways and for e.g. Split() or
+// Mutate() to run their own visitations of the transitive dependency of the
+// module and both of these are bad ideas, but it's better than no guardrails at
+// all.
+//
+// This model is pretty close to Bazel's configuration transitions. The mapping
+// between concepts in Soong and Bazel is as follows:
+//   - Module == configured target
+//   - Variant == configuration
+//   - Variation name == configuration flag
+//   - Variation == configuration flag value
+//   - Outgoing transition == attribute transition
+//   - Incoming transition == rule transition
+//
+// The Split() method does not have a Bazel equivalent and Bazel split
+// transitions do not have a Soong equivalent.
+//
+// Mutate() does not make sense in Bazel due to the different models of the
+// two systems: when creating new variations, Soong clones the old module and
+// thus some way is needed to change it state whereas Bazel creates each
+// configuration of a given configured target anew.
+type TransitionMutator interface {
+	// Split returns the set of variations that should be created for a module no
+	// matter who depends on it. Used when Make depends on a particular variation
+	// or when the module knows its variations just based on information given to
+	// it in the Blueprint file. This method should not mutate the module it is
+	// called on.
+	Split(ctx BaseModuleContext) []string
+
+	// OutgoingTransition is called on a module to determine which variation it wants
+	// from its direct dependencies. The dependency itself can override this decision.
+	// This method should not mutate the module itself.
+	OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string
+
+	// IncomingTransition is called on a module to determine which variation it should
+	// be in based on the variation modules that depend on it want. This gives the module
+	// a final say about its own variations. This method should not mutate the module
+	// itself.
+	IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string
+
+	// Mutate is called after a module was split into multiple variations on each variation.
+	// It should not split the module any further but adding new dependencies is
+	// fine. Unlike all the other methods on TransitionMutator, this method is
+	// allowed to mutate the module.
+	Mutate(ctx BottomUpMutatorContext, variation string)
+}
+
+type IncomingTransitionContext interface {
+	ArchModuleContext
+	ModuleProviderContext
+	ModuleErrorContext
+
+	// Module returns the target of the dependency edge for which the transition
+	// is being computed
+	Module() Module
+
+	// Config returns the configuration for the build.
+	Config() Config
+
+	DeviceConfig() DeviceConfig
+
+	// IsAddingDependency returns true if the transition is being called while adding a dependency
+	// after the transition mutator has already run, or false if it is being called when the transition
+	// mutator is running.  This should be used sparingly, all uses will have to be removed in order
+	// to support creating variants on demand.
+	IsAddingDependency() bool
+}
+
+type OutgoingTransitionContext interface {
+	ArchModuleContext
+	ModuleProviderContext
+
+	// Module returns the target of the dependency edge for which the transition
+	// is being computed
+	Module() Module
+
+	// DepTag() Returns the dependency tag through which this dependency is
+	// reached
+	DepTag() blueprint.DependencyTag
+
+	// Config returns the configuration for the build.
+	Config() Config
+
+	DeviceConfig() DeviceConfig
+}
+
+type androidTransitionMutator struct {
+	finalPhase bool
+	mutator    TransitionMutator
+	name       string
+}
+
+func (a *androidTransitionMutator) Split(ctx blueprint.BaseModuleContext) []string {
+	if a.finalPhase {
+		panic("TransitionMutator not allowed in FinalDepsMutators")
+	}
+	if m, ok := ctx.Module().(Module); ok {
+		moduleContext := m.base().baseModuleContextFactory(ctx)
+		return a.mutator.Split(&moduleContext)
+	} else {
+		return []string{""}
+	}
+}
+
+func (a *androidTransitionMutator) OutgoingTransition(bpctx blueprint.OutgoingTransitionContext, sourceVariation string) string {
+	if m, ok := bpctx.Module().(Module); ok {
+		ctx := outgoingTransitionContextPool.Get().(*outgoingTransitionContextImpl)
+		defer outgoingTransitionContextPool.Put(ctx)
+		*ctx = outgoingTransitionContextImpl{
+			archModuleContext: m.base().archModuleContextFactory(bpctx),
+			bp:                bpctx,
+		}
+		return a.mutator.OutgoingTransition(ctx, sourceVariation)
+	} else {
+		return ""
+	}
+}
+
+func (a *androidTransitionMutator) IncomingTransition(bpctx blueprint.IncomingTransitionContext, incomingVariation string) string {
+	if m, ok := bpctx.Module().(Module); ok {
+		ctx := incomingTransitionContextPool.Get().(*incomingTransitionContextImpl)
+		defer incomingTransitionContextPool.Put(ctx)
+		*ctx = incomingTransitionContextImpl{
+			archModuleContext: m.base().archModuleContextFactory(bpctx),
+			bp:                bpctx,
+		}
+		return a.mutator.IncomingTransition(ctx, incomingVariation)
+	} else {
+		return ""
+	}
+}
+
+func (a *androidTransitionMutator) Mutate(ctx blueprint.BottomUpMutatorContext, variation string) {
+	if am, ok := ctx.Module().(Module); ok {
+		if variation != "" {
+			// TODO: this should really be checking whether the TransitionMutator affected this module, not
+			//  the empty variant, but TransitionMutator has no concept of skipping a module.
+			base := am.base()
+			base.commonProperties.DebugMutators = append(base.commonProperties.DebugMutators, a.name)
+			base.commonProperties.DebugVariations = append(base.commonProperties.DebugVariations, variation)
+		}
+
+		mctx := bottomUpMutatorContextFactory(ctx, am, a.finalPhase)
+		defer bottomUpMutatorContextPool.Put(mctx)
+		a.mutator.Mutate(mctx, variation)
+	}
+}
+
+type incomingTransitionContextImpl struct {
+	archModuleContext
+	bp blueprint.IncomingTransitionContext
+}
+
+func (c *incomingTransitionContextImpl) Module() Module {
+	return c.bp.Module().(Module)
+}
+
+func (c *incomingTransitionContextImpl) Config() Config {
+	return c.bp.Config().(Config)
+}
+
+func (c *incomingTransitionContextImpl) DeviceConfig() DeviceConfig {
+	return DeviceConfig{c.bp.Config().(Config).deviceConfig}
+}
+
+func (c *incomingTransitionContextImpl) IsAddingDependency() bool {
+	return c.bp.IsAddingDependency()
+}
+
+func (c *incomingTransitionContextImpl) provider(provider blueprint.AnyProviderKey) (any, bool) {
+	return c.bp.Provider(provider)
+}
+
+func (c *incomingTransitionContextImpl) ModuleErrorf(fmt string, args ...interface{}) {
+	c.bp.ModuleErrorf(fmt, args)
+}
+
+func (c *incomingTransitionContextImpl) PropertyErrorf(property, fmt string, args ...interface{}) {
+	c.bp.PropertyErrorf(property, fmt, args)
+}
+
+type outgoingTransitionContextImpl struct {
+	archModuleContext
+	bp blueprint.OutgoingTransitionContext
+}
+
+func (c *outgoingTransitionContextImpl) Module() Module {
+	return c.bp.Module().(Module)
+}
+
+func (c *outgoingTransitionContextImpl) DepTag() blueprint.DependencyTag {
+	return c.bp.DepTag()
+}
+
+func (c *outgoingTransitionContextImpl) Config() Config {
+	return c.bp.Config().(Config)
+}
+
+func (c *outgoingTransitionContextImpl) DeviceConfig() DeviceConfig {
+	return DeviceConfig{c.bp.Config().(Config).deviceConfig}
+}
+
+func (c *outgoingTransitionContextImpl) provider(provider blueprint.AnyProviderKey) (any, bool) {
+	return c.bp.Provider(provider)
+}
diff --git a/android/transition_test.go b/android/transition_test.go
new file mode 100644
index 0000000..f7618f3
--- /dev/null
+++ b/android/transition_test.go
@@ -0,0 +1,162 @@
+// Copyright 2024 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android
+
+import "testing"
+
+type testTransitionMutator struct {
+	split              func(ctx BaseModuleContext) []string
+	outgoingTransition func(ctx OutgoingTransitionContext, sourceVariation string) string
+	incomingTransition func(ctx IncomingTransitionContext, incomingVariation string) string
+	mutate             func(ctx BottomUpMutatorContext, variation string)
+}
+
+func (t *testTransitionMutator) Split(ctx BaseModuleContext) []string {
+	if t.split != nil {
+		return t.split(ctx)
+	}
+	return []string{""}
+}
+
+func (t *testTransitionMutator) OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string {
+	if t.outgoingTransition != nil {
+		return t.outgoingTransition(ctx, sourceVariation)
+	}
+	return sourceVariation
+}
+
+func (t *testTransitionMutator) IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string {
+	if t.incomingTransition != nil {
+		return t.incomingTransition(ctx, incomingVariation)
+	}
+	return incomingVariation
+}
+
+func (t *testTransitionMutator) Mutate(ctx BottomUpMutatorContext, variation string) {
+	if t.mutate != nil {
+		t.mutate(ctx, variation)
+	}
+}
+
+func TestModuleString(t *testing.T) {
+	bp := `
+		test {
+			name: "foo",
+		}
+	`
+
+	var moduleStrings []string
+
+	GroupFixturePreparers(
+		FixtureRegisterWithContext(func(ctx RegistrationContext) {
+
+			ctx.PreArchMutators(func(ctx RegisterMutatorsContext) {
+				ctx.Transition("pre_arch", &testTransitionMutator{
+					split: func(ctx BaseModuleContext) []string {
+						moduleStrings = append(moduleStrings, ctx.Module().String())
+						return []string{"a", "b"}
+					},
+				})
+			})
+
+			ctx.PreDepsMutators(func(ctx RegisterMutatorsContext) {
+				ctx.Transition("pre_deps", &testTransitionMutator{
+					split: func(ctx BaseModuleContext) []string {
+						moduleStrings = append(moduleStrings, ctx.Module().String())
+						return []string{"c", "d"}
+					},
+				})
+			})
+
+			ctx.PostDepsMutators(func(ctx RegisterMutatorsContext) {
+				ctx.Transition("post_deps", &testTransitionMutator{
+					split: func(ctx BaseModuleContext) []string {
+						moduleStrings = append(moduleStrings, ctx.Module().String())
+						return []string{"e", "f"}
+					},
+					outgoingTransition: func(ctx OutgoingTransitionContext, sourceVariation string) string {
+						return ""
+					},
+				})
+				ctx.BottomUp("rename_bottom_up", func(ctx BottomUpMutatorContext) {
+					moduleStrings = append(moduleStrings, ctx.Module().String())
+					ctx.Rename(ctx.Module().base().Name() + "_renamed1")
+				}).UsesRename()
+				ctx.BottomUp("final", func(ctx BottomUpMutatorContext) {
+					moduleStrings = append(moduleStrings, ctx.Module().String())
+				})
+			})
+
+			ctx.RegisterModuleType("test", mutatorTestModuleFactory)
+		}),
+		FixtureWithRootAndroidBp(bp),
+	).RunTest(t)
+
+	want := []string{
+		// Initial name.
+		"foo{}",
+
+		// After pre_arch (reversed because rename_top_down is TopDown so it visits in reverse order).
+		"foo{pre_arch:b}",
+		"foo{pre_arch:a}",
+
+		// After pre_deps (reversed because post_deps TransitionMutator.Split is TopDown).
+		"foo{pre_arch:b,pre_deps:d}",
+		"foo{pre_arch:b,pre_deps:c}",
+		"foo{pre_arch:a,pre_deps:d}",
+		"foo{pre_arch:a,pre_deps:c}",
+
+		// After post_deps.
+		"foo{pre_arch:a,pre_deps:c,post_deps:e}",
+		"foo{pre_arch:a,pre_deps:c,post_deps:f}",
+		"foo{pre_arch:a,pre_deps:d,post_deps:e}",
+		"foo{pre_arch:a,pre_deps:d,post_deps:f}",
+		"foo{pre_arch:b,pre_deps:c,post_deps:e}",
+		"foo{pre_arch:b,pre_deps:c,post_deps:f}",
+		"foo{pre_arch:b,pre_deps:d,post_deps:e}",
+		"foo{pre_arch:b,pre_deps:d,post_deps:f}",
+
+		// After rename_bottom_up.
+		"foo_renamed1{pre_arch:a,pre_deps:c,post_deps:e}",
+		"foo_renamed1{pre_arch:a,pre_deps:c,post_deps:f}",
+		"foo_renamed1{pre_arch:a,pre_deps:d,post_deps:e}",
+		"foo_renamed1{pre_arch:a,pre_deps:d,post_deps:f}",
+		"foo_renamed1{pre_arch:b,pre_deps:c,post_deps:e}",
+		"foo_renamed1{pre_arch:b,pre_deps:c,post_deps:f}",
+		"foo_renamed1{pre_arch:b,pre_deps:d,post_deps:e}",
+		"foo_renamed1{pre_arch:b,pre_deps:d,post_deps:f}",
+	}
+
+	AssertDeepEquals(t, "module String() values", want, moduleStrings)
+}
+
+func TestTransitionMutatorInFinalDeps(t *testing.T) {
+	GroupFixturePreparers(
+		FixtureRegisterWithContext(func(ctx RegistrationContext) {
+			ctx.FinalDepsMutators(func(ctx RegisterMutatorsContext) {
+				ctx.Transition("vars", &testTransitionMutator{
+					split: func(ctx BaseModuleContext) []string {
+						return []string{"a", "b"}
+					},
+				})
+			})
+
+			ctx.RegisterModuleType("test", mutatorTestModuleFactory)
+		}),
+		FixtureWithRootAndroidBp(`test {name: "foo"}`),
+	).
+		ExtendWithErrorHandler(FixtureExpectsOneErrorPattern("not allowed in FinalDepsMutators")).
+		RunTest(t)
+}