Add error handling to test fixtures

Adds support for customizing the error handling behavior of test
fixtures and converts a test to use it.

Bug: 181070625
Test: m nothing
Change-Id: I736c41311819d57d8688fc3b0e021dbb50c491c1
diff --git a/android/fixture.go b/android/fixture.go
index 0efe329..ae24b91 100644
--- a/android/fixture.go
+++ b/android/fixture.go
@@ -182,7 +182,13 @@
 	// Create a Fixture.
 	Fixture(t *testing.T, preparers ...FixturePreparer) Fixture
 
-	// Run the test, expecting no errors, returning a TestResult instance.
+	// Set the error handler that will be used to check any errors reported by the test.
+	//
+	// The default handlers is FixtureExpectsNoErrors which will fail the go test immediately if any
+	// errors are reported.
+	SetErrorHandler(errorHandler FixtureErrorHandler) FixtureFactory
+
+	// Run the test, checking any errors reported and returning a TestResult instance.
 	//
 	// Shorthand for Fixture(t, preparers...).RunTest()
 	RunTest(t *testing.T, preparers ...FixturePreparer) *TestResult
@@ -202,6 +208,9 @@
 	return &fixtureFactory{
 		buildDirSupplier: buildDirSupplier,
 		preparers:        dedupAndFlattenPreparers(nil, preparers),
+
+		// Set the default error handler.
+		errorHandler: FixtureExpectsNoErrors,
 	}
 }
 
@@ -352,9 +361,84 @@
 	return &simpleFixturePreparer{function: preparer}
 }
 
+// FixtureErrorHandler determines how to respond to errors reported by the code under test.
+//
+// Some possible responses:
+// * Fail the test if any errors are reported, see FixtureExpectsNoErrors.
+// * Fail the test if at least one error that matches a pattern is not reported see
+//   FixtureExpectsAtLeastOneErrorMatchingPattern
+// * Fail the test if any unexpected errors are reported.
+//
+// Although at the moment all the error handlers are implemented as simply a wrapper around a
+// function this is defined as an interface to allow future enhancements, e.g. provide different
+// ways other than patterns to match an error and to combine handlers together.
+type FixtureErrorHandler interface {
+	// CheckErrors checks the errors reported.
+	//
+	// The supplied result can be used to access the state of the code under test just as the main
+	// body of the test would but if any errors other than ones expected are reported the state may
+	// be indeterminate.
+	CheckErrors(result *TestResult, errs []error)
+}
+
+type simpleErrorHandler struct {
+	function func(result *TestResult, errs []error)
+}
+
+func (h simpleErrorHandler) CheckErrors(result *TestResult, errs []error) {
+	h.function(result, errs)
+}
+
+// The default fixture error handler.
+//
+// Will fail the test immediately if any errors are reported.
+var FixtureExpectsNoErrors = FixtureCustomErrorHandler(
+	func(result *TestResult, errs []error) {
+		FailIfErrored(result.T, errs)
+	},
+)
+
+// FixtureExpectsAtLeastOneMatchingError returns an error handler that will cause the test to fail
+// if at least one error that matches the regular expression is not found.
+//
+// The test will be failed if:
+// * No errors are reported.
+// * One or more errors are reported but none match the pattern.
+//
+// The test will not fail if:
+// * Multiple errors are reported that do not match the pattern as long as one does match.
+func FixtureExpectsAtLeastOneErrorMatchingPattern(pattern string) FixtureErrorHandler {
+	return FixtureCustomErrorHandler(func(result *TestResult, errs []error) {
+		FailIfNoMatchingErrors(result.T, pattern, errs)
+	})
+}
+
+// FixtureExpectsOneErrorToMatchPerPattern returns an error handler that will cause the test to fail
+// if there are any unexpected errors.
+//
+// The test will be failed if:
+// * The number of errors reported does not exactly match the patterns.
+// * One or more of the reported errors do not match a pattern.
+// * No patterns are provided and one or more errors are reported.
+//
+// The test will not fail if:
+// * One or more of the patterns does not match an error.
+func FixtureExpectsAllErrorsToMatchAPattern(patterns []string) FixtureErrorHandler {
+	return FixtureCustomErrorHandler(func(result *TestResult, errs []error) {
+		CheckErrorsAgainstExpectations(result.T, errs, patterns)
+	})
+}
+
+// FixtureCustomErrorHandler creates a custom error handler
+func FixtureCustomErrorHandler(function func(result *TestResult, errs []error)) FixtureErrorHandler {
+	return simpleErrorHandler{
+		function: function,
+	}
+}
+
 // Fixture defines the test environment.
 type Fixture interface {
-	// Run the test, expecting no errors, returning a TestResult instance.
+	// Run the test, checking any errors reported and returning a TestResult instance.
 	RunTest() *TestResult
 }
 
@@ -454,25 +538,29 @@
 type fixtureFactory struct {
 	buildDirSupplier *string
 	preparers        []*simpleFixturePreparer
+	errorHandler     FixtureErrorHandler
 }
 
 func (f *fixtureFactory) Extend(preparers ...FixturePreparer) FixtureFactory {
 	all := append(f.preparers, dedupAndFlattenPreparers(f.preparers, preparers)...)
-	return &fixtureFactory{
-		buildDirSupplier: f.buildDirSupplier,
-		preparers:        all,
-	}
+	// Copy the existing factory.
+	extendedFactory := &fixtureFactory{}
+	*extendedFactory = *f
+	// Use the extended list of preparers.
+	extendedFactory.preparers = all
+	return extendedFactory
 }
 
 func (f *fixtureFactory) Fixture(t *testing.T, preparers ...FixturePreparer) Fixture {
 	config := TestConfig(*f.buildDirSupplier, nil, "", nil)
 	ctx := NewTestContext(config)
 	fixture := &fixture{
-		factory: f,
-		t:       t,
-		config:  config,
-		ctx:     ctx,
-		mockFS:  make(MockFS),
+		factory:      f,
+		t:            t,
+		config:       config,
+		ctx:          ctx,
+		mockFS:       make(MockFS),
+		errorHandler: f.errorHandler,
 	}
 
 	for _, preparer := range f.preparers {
@@ -486,6 +574,11 @@
 	return fixture
 }
 
+func (f *fixtureFactory) SetErrorHandler(errorHandler FixtureErrorHandler) FixtureFactory {
+	f.errorHandler = errorHandler
+	return f
+}
+
 func (f *fixtureFactory) RunTest(t *testing.T, preparers ...FixturePreparer) *TestResult {
 	t.Helper()
 	fixture := f.Fixture(t, preparers...)
@@ -498,11 +591,23 @@
 }
 
 type fixture struct {
+	// The factory used to create this fixture.
 	factory *fixtureFactory
-	t       *testing.T
-	config  Config
-	ctx     *TestContext
-	mockFS  MockFS
+
+	// The gotest state of the go test within which this was created.
+	t *testing.T
+
+	// The configuration prepared for this fixture.
+	config Config
+
+	// The test context prepared for this fixture.
+	ctx *TestContext
+
+	// The mock filesystem prepared for this fixture.
+	mockFS MockFS
+
+	// The error handler used to check the errors, if any, that are reported.
+	errorHandler FixtureErrorHandler
 }
 
 func (f *fixture) RunTest() *TestResult {
@@ -525,9 +630,9 @@
 
 	ctx.Register()
 	_, errs := ctx.ParseBlueprintsFiles("ignored")
-	FailIfErrored(f.t, errs)
-	_, errs = ctx.PrepareBuildActions(f.config)
-	FailIfErrored(f.t, errs)
+	if len(errs) == 0 {
+		_, errs = ctx.PrepareBuildActions(f.config)
+	}
 
 	result := &TestResult{
 		TestHelper:  TestHelper{T: f.t},
@@ -535,6 +640,9 @@
 		fixture:     f,
 		Config:      f.config,
 	}
+
+	f.errorHandler.CheckErrors(result, errs)
+
 	return result
 }
 
diff --git a/android/package.go b/android/package.go
index 7012fc7..878e4c4 100644
--- a/android/package.go
+++ b/android/package.go
@@ -23,6 +23,8 @@
 	RegisterPackageBuildComponents(InitRegistrationContext)
 }
 
+var PrepareForTestWithPackageModule = FixtureRegisterWithContext(RegisterPackageBuildComponents)
+
 // Register the package module type.
 func RegisterPackageBuildComponents(ctx RegistrationContext) {
 	ctx.RegisterModuleType("package", PackageFactory)
diff --git a/android/prebuilt_test.go b/android/prebuilt_test.go
index 164b1bc..32af5df 100644
--- a/android/prebuilt_test.go
+++ b/android/prebuilt_test.go
@@ -388,6 +388,8 @@
 	ctx.PostDepsMutators(RegisterOverridePostDepsMutators)
 }
 
+var prepareForTestWithFakePrebuiltModules = FixtureRegisterWithContext(registerTestPrebuiltModules)
+
 func registerTestPrebuiltModules(ctx RegistrationContext) {
 	ctx.RegisterModuleType("prebuilt", newPrebuiltModule)
 	ctx.RegisterModuleType("source", newSourceModule)
diff --git a/android/visibility.go b/android/visibility.go
index 7eac471..631e88f 100644
--- a/android/visibility.go
+++ b/android/visibility.go
@@ -202,6 +202,18 @@
 	ExcludeFromVisibilityEnforcement()
 }
 
+var PrepareForTestWithVisibilityRuleChecker = FixtureRegisterWithContext(func(ctx RegistrationContext) {
+	ctx.PreArchMutators(RegisterVisibilityRuleChecker)
+})
+
+var PrepareForTestWithVisibilityRuleGatherer = FixtureRegisterWithContext(func(ctx RegistrationContext) {
+	ctx.PreArchMutators(RegisterVisibilityRuleGatherer)
+})
+
+var PrepareForTestWithVisibilityRuleEnforcer = FixtureRegisterWithContext(func(ctx RegistrationContext) {
+	ctx.PostDepsMutators(RegisterVisibilityRuleEnforcer)
+})
+
 // The rule checker needs to be registered before defaults expansion to correctly check that
 // //visibility:xxx isn't combined with other packages in the same list in any one module.
 func RegisterVisibilityRuleChecker(ctx RegisterMutatorsContext) {
diff --git a/android/visibility_test.go b/android/visibility_test.go
index 87a295e..30cdcbf 100644
--- a/android/visibility_test.go
+++ b/android/visibility_test.go
@@ -9,13 +9,13 @@
 
 var visibilityTests = []struct {
 	name                string
-	fs                  map[string][]byte
+	fs                  MockFS
 	expectedErrors      []string
 	effectiveVisibility map[qualifiedModuleName][]string
 }{
 	{
 		name: "invalid visibility: empty list",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -26,7 +26,7 @@
 	},
 	{
 		name: "invalid visibility: empty rule",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -37,7 +37,7 @@
 	},
 	{
 		name: "invalid visibility: unqualified",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -48,7 +48,7 @@
 	},
 	{
 		name: "invalid visibility: empty namespace",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -59,7 +59,7 @@
 	},
 	{
 		name: "invalid visibility: empty module",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -70,7 +70,7 @@
 	},
 	{
 		name: "invalid visibility: empty namespace and module",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -81,7 +81,7 @@
 	},
 	{
 		name: "//visibility:unknown",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -92,7 +92,7 @@
 	},
 	{
 		name: "//visibility:xxx mixed",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -113,7 +113,7 @@
 	},
 	{
 		name: "//visibility:legacy_public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -129,7 +129,7 @@
 		// Verify that //visibility:public will allow the module to be referenced from anywhere, e.g.
 		// the current directory, a nested directory and a directory in a separate tree.
 		name: "//visibility:public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -156,7 +156,7 @@
 		// Verify that //visibility:private allows the module to be referenced from the current
 		// directory only.
 		name: "//visibility:private",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -188,7 +188,7 @@
 	{
 		// Verify that :__pkg__ allows the module to be referenced from the current directory only.
 		name: ":__pkg__",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -221,7 +221,7 @@
 		// Verify that //top/nested allows the module to be referenced from the current directory and
 		// the top/nested directory only, not a subdirectory of top/nested and not peak directory.
 		name: "//top/nested",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -259,7 +259,7 @@
 		// Verify that :__subpackages__ allows the module to be referenced from the current directory
 		// and sub directories but nowhere else.
 		name: ":__subpackages__",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -290,7 +290,7 @@
 		// Verify that //top/nested:__subpackages__ allows the module to be referenced from the current
 		// directory and sub directories but nowhere else.
 		name: "//top/nested:__subpackages__",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -321,7 +321,7 @@
 		// Verify that ["//top/nested", "//peak:__subpackages"] allows the module to be referenced from
 		// the current directory, top/nested and peak and all its subpackages.
 		name: `["//top/nested", "//peak:__subpackages__"]`,
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -347,7 +347,7 @@
 	{
 		// Verify that //vendor... cannot be used outside vendor apart from //vendor:__subpackages__
 		name: `//vendor`,
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -381,7 +381,7 @@
 	{
 		// Check that visibility is the union of the defaults modules.
 		name: "defaults union, basic",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -419,7 +419,7 @@
 	},
 	{
 		name: "defaults union, multiple defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults_1",
@@ -460,7 +460,7 @@
 	},
 	{
 		name: "//visibility:public mixed with other in defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -478,7 +478,7 @@
 	},
 	{
 		name: "//visibility:public overriding defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -501,7 +501,7 @@
 	},
 	{
 		name: "//visibility:public mixed with other from different defaults 1",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults_1",
@@ -524,7 +524,7 @@
 	},
 	{
 		name: "//visibility:public mixed with other from different defaults 2",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults_1",
@@ -547,7 +547,7 @@
 	},
 	{
 		name: "//visibility:private in defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -581,7 +581,7 @@
 	},
 	{
 		name: "//visibility:private mixed with other in defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -599,7 +599,7 @@
 	},
 	{
 		name: "//visibility:private overriding defaults",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -618,7 +618,7 @@
 	},
 	{
 		name: "//visibility:private in defaults overridden",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -637,7 +637,7 @@
 	},
 	{
 		name: "//visibility:private override //visibility:public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -655,7 +655,7 @@
 	},
 	{
 		name: "//visibility:public override //visibility:private",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -673,7 +673,7 @@
 	},
 	{
 		name: "//visibility:override must be first in the list",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_library {
 					name: "libexample",
@@ -686,7 +686,7 @@
 	},
 	{
 		name: "//visibility:override discards //visibility:private",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -707,7 +707,7 @@
 	},
 	{
 		name: "//visibility:override discards //visibility:public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -736,7 +736,7 @@
 	},
 	{
 		name: "//visibility:override discards defaults supplied rules",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -765,7 +765,7 @@
 	},
 	{
 		name: "//visibility:override can override //visibility:public with //visibility:private",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -788,7 +788,7 @@
 	},
 	{
 		name: "//visibility:override can override //visibility:private with //visibility:public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults",
@@ -808,7 +808,7 @@
 	},
 	{
 		name: "//visibility:private mixed with itself",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "libexample_defaults_1",
@@ -838,7 +838,7 @@
 	// Defaults module's defaults_visibility tests
 	{
 		name: "defaults_visibility invalid",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_defaults {
 					name: "top_defaults",
@@ -851,7 +851,7 @@
 	},
 	{
 		name: "defaults_visibility overrides package default",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:private"],
@@ -871,7 +871,7 @@
 	// Package default_visibility tests
 	{
 		name: "package default_visibility property is checked",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:invalid"],
@@ -882,7 +882,7 @@
 	{
 		// This test relies on the default visibility being legacy_public.
 		name: "package default_visibility property used when no visibility specified",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:private"],
@@ -904,7 +904,7 @@
 	},
 	{
 		name: "package default_visibility public does not override visibility private",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:public"],
@@ -927,7 +927,7 @@
 	},
 	{
 		name: "package default_visibility private does not override visibility public",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:private"],
@@ -946,7 +946,7 @@
 	},
 	{
 		name: "package default_visibility :__subpackages__",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: [":__subpackages__"],
@@ -973,7 +973,7 @@
 	},
 	{
 		name: "package default_visibility inherited to subpackages",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//outsider"],
@@ -1001,7 +1001,7 @@
 	},
 	{
 		name: "package default_visibility inherited to subpackages",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				package {
 					default_visibility: ["//visibility:private"],
@@ -1031,7 +1031,7 @@
 	},
 	{
 		name: "verify that prebuilt dependencies are ignored for visibility reasons (not preferred)",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"prebuilts/Blueprints": []byte(`
 				prebuilt {
 					name: "module",
@@ -1053,7 +1053,7 @@
 	},
 	{
 		name: "verify that prebuilt dependencies are ignored for visibility reasons (preferred)",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"prebuilts/Blueprints": []byte(`
 				prebuilt {
 					name: "module",
@@ -1076,7 +1076,7 @@
 	},
 	{
 		name: "ensure visibility properties are checked for correctness",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_parent {
 					name: "parent",
@@ -1093,7 +1093,7 @@
 	},
 	{
 		name: "invalid visibility added to child detected during gather phase",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_parent {
 					name: "parent",
@@ -1115,7 +1115,7 @@
 	},
 	{
 		name: "automatic visibility inheritance enabled",
-		fs: map[string][]byte{
+		fs: MockFS{
 			"top/Blueprints": []byte(`
 				mock_parent {
 					name: "parent",
@@ -1142,55 +1142,43 @@
 func TestVisibility(t *testing.T) {
 	for _, test := range visibilityTests {
 		t.Run(test.name, func(t *testing.T) {
-			ctx, errs := testVisibility(buildDir, test.fs)
-
-			CheckErrorsAgainstExpectations(t, errs, test.expectedErrors)
+			result := emptyTestFixtureFactory.Extend(
+				FixtureRegisterWithContext(func(ctx RegistrationContext) {
+					ctx.RegisterModuleType("mock_library", newMockLibraryModule)
+					ctx.RegisterModuleType("mock_parent", newMockParentFactory)
+					ctx.RegisterModuleType("mock_defaults", defaultsFactory)
+				}),
+				prepareForTestWithFakePrebuiltModules,
+				PrepareForTestWithPackageModule,
+				// Order of the following method calls is significant as they register mutators.
+				PrepareForTestWithArchMutator,
+				PrepareForTestWithPrebuilts,
+				PrepareForTestWithOverrides,
+				PrepareForTestWithVisibilityRuleChecker,
+				PrepareForTestWithDefaults,
+				PrepareForTestWithVisibilityRuleGatherer,
+				PrepareForTestWithVisibilityRuleEnforcer,
+				// Add additional files to the mock filesystem
+				test.fs.AddToFixture(),
+			).
+				SetErrorHandler(FixtureExpectsAllErrorsToMatchAPattern(test.expectedErrors)).
+				RunTest(t)
 
 			if test.effectiveVisibility != nil {
-				checkEffectiveVisibility(t, ctx, test.effectiveVisibility)
+				checkEffectiveVisibility(result, test.effectiveVisibility)
 			}
 		})
 	}
 }
 
-func checkEffectiveVisibility(t *testing.T, ctx *TestContext, effectiveVisibility map[qualifiedModuleName][]string) {
+func checkEffectiveVisibility(result *TestResult, effectiveVisibility map[qualifiedModuleName][]string) {
 	for moduleName, expectedRules := range effectiveVisibility {
-		rule := effectiveVisibilityRules(ctx.config, moduleName)
+		rule := effectiveVisibilityRules(result.Config, moduleName)
 		stringRules := rule.Strings()
-		if !reflect.DeepEqual(expectedRules, stringRules) {
-			t.Errorf("effective rules mismatch: expected %q, found %q", expectedRules, stringRules)
-		}
+		result.AssertDeepEquals("effective rules mismatch", expectedRules, stringRules)
 	}
 }
 
-func testVisibility(buildDir string, fs map[string][]byte) (*TestContext, []error) {
-
-	// Create a new config per test as visibility information is stored in the config.
-	config := TestArchConfig(buildDir, nil, "", fs)
-
-	ctx := NewTestArchContext(config)
-	ctx.RegisterModuleType("mock_library", newMockLibraryModule)
-	ctx.RegisterModuleType("mock_parent", newMockParentFactory)
-	ctx.RegisterModuleType("mock_defaults", defaultsFactory)
-
-	// Order of the following method calls is significant.
-	RegisterPackageBuildComponents(ctx)
-	registerTestPrebuiltBuildComponents(ctx)
-	ctx.PreArchMutators(RegisterVisibilityRuleChecker)
-	ctx.PreArchMutators(RegisterDefaultsPreArchMutators)
-	ctx.PreArchMutators(RegisterVisibilityRuleGatherer)
-	ctx.PostDepsMutators(RegisterVisibilityRuleEnforcer)
-	ctx.Register()
-
-	_, errs := ctx.ParseBlueprintsFiles(".")
-	if len(errs) > 0 {
-		return ctx, errs
-	}
-
-	_, errs = ctx.PrepareBuildActions(config)
-	return ctx, errs
-}
-
 type mockLibraryProperties struct {
 	Deps []string
 }