// Copyright 2017 The Bazel Authors. 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 com.google.devtools.build.lib.analysis.util; import com.google.common.base.Preconditions; import com.google.devtools.build.lib.analysis.BaseRuleClasses; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory; import com.google.devtools.build.lib.analysis.RuleDefinition; import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.RuleClass; import java.util.Arrays; /** * Provides a simple API for creating custom rule classes for tests. * *

Use this whenever you want to test language-agnostic Bazel functionality, i.e. behavior that * isn't specific to individual rule implementations. If you find yourself searching through rule * implementations trying to find one that matches whatever you're trying to test, you probably * want this instead. * *

This prevents the anti-pattern of tests with commingled dependencies. For example, when a test * uses cc_library to test generic logic that cc_library happens to * provide, the test can break if the cc_library implementation changes. This means C++ * rule developers have to understand the test to change C++ logic: a dependency that helps no one. * *

Even if C++ logic doesn't change, cc_library may not make it clear what's being * tested (e.g. "why is the "malloc" attribute used here?"). Using a mock rule class offers the * ability to write a clearer, more focused, easier to understand test (e.g. * mock_rule(name = "foo", attr_that_tests_this_specific_test_logic = ":bar")Usage for a custom rule type that just needs to exist (no special attributes or behavior * needed): * *

 *   MockRule fooRule = () -> MockRule.define("foo_rule");
 * 
* *

Usage for custom attributes: * *

 *   MockRule fooRule = () -> MockRule.define("foo_rule", attr("some_attr", Type.STRING));
 * 
* *

Usage for arbitrary customization: * *

 *   MockRule fooRule = () -> MockRule.define(
 *       "foo_rule",
 *       (builder, env) ->
 *           builder
 *               .removeAttribute("tags")
 *               .requiresConfigurationFragments(FooConfiguration.class);
 *       );
 * 
* * Custom {@link RuleDefinition} ancestors and {@link RuleConfiguredTargetFactory} implementations * can also be specified: * *
 *   MockRule customAncestor = () -> MockRule.ancestor(BaseRule.class).define(...);
 *   MockRule customImpl = () -> MockRule.factory(FooRuleFactory.class).define(...);
 *   MockRule customEverything = () ->
 *       MockRule.ancestor(BaseRule.class).factory(FooRuleFactory.class).define(...);
 * 
* * When unspecified, {@link State#DEFAULT_ANCESTOR} and {@link State#DEFAULT_FACTORY} apply. * *

We use lambdas for custom rule classes because {@link ConfiguredRuleClassProvider} indexes * rule class definitions by their Java class names. So each definition has to have its own * unique Java class. * *

Both of the following forms are valid: * *

MockRule fooRule = () -> MockRule.define("foo_rule");
*
RuleDefinition fooRule = (MockRule) () -> MockRule.define("foo_rule");
* *

Use discretion in choosing your preferred form. The first is more compact. The second makes * it clearer that fooRule is a proper rule class definition. */ public interface MockRule extends RuleDefinition { // MockRule is designed to be easy to use. That doesn't necessarily mean its implementation is // easy to undestand. // // If you just want to mock a rule, it's best to rely on the interface javadoc above, rather than // trying to parse what's going on below. You really only need to understand the below if you want // to customize MockRule itself. /** * Container for the desired name and custom settings for this rule class. */ class State { private final String name; private final MockRuleCustomBehavior customBehavior; private final Class factory; private final Class ancestor; /** The default {@link RuleConfiguredTargetFactory} for this rule class. */ private static final Class DEFAULT_FACTORY = MockRuleDefaults.DefaultConfiguredTargetFactory.class; /** The default {@link RuleDefinition} for this rule class. */ private static final Class DEFAULT_ANCESTOR = BaseRuleClasses.RootRule.class; State(String ruleClassName, MockRuleCustomBehavior customBehavior, Class factory, Class ancestor) { this.name = Preconditions.checkNotNull(ruleClassName); this.customBehavior = Preconditions.checkNotNull(customBehavior); this.factory = factory; this.ancestor = ancestor; } public static class Builder { private Class factory = DEFAULT_FACTORY; private Class ancestor = DEFAULT_ANCESTOR; public Builder factory(Class factory) { this.factory = factory; return this; } public Builder ancestor(Class ancestor) { this.ancestor = ancestor; return this; } public State define(String ruleClassName, Attribute.Builder... attributes) { return build(ruleClassName, new MockRuleCustomBehavior.CustomAttributes(Arrays.asList(attributes))); } public State define(String ruleClassName, MockRuleCustomBehavior customBehavior) { return build(ruleClassName, customBehavior); } private State build(String ruleClassName, MockRuleCustomBehavior customBehavior) { return new State(ruleClassName, customBehavior, factory, ancestor); } } } /** * Sets a custom {@link RuleConfiguredTargetFactory} for this mock rule. * *

If not set, {@link State#DEFAULT_FACTORY} is used. */ static State.Builder factory(Class factory) { return new State.Builder().factory(factory); } /** * Sets a custom ancestor {@link RuleDefinition} for this mock rule. * *

If not set, {@link State#DEFAULT_ANCESTOR} is used. */ static State.Builder ancestor(Class ancestor) { return new State.Builder().ancestor(ancestor); } /** * Returns a new {@link State} for this rule class with custom attributes. This is a convenience * method for lambda definitions: * *

   *   MockRule myRule = () -> MockRule.define("my_rule", attr("myattr", Type.STRING));
   * 
*/ static State define(String ruleClassName, Attribute.Builder... attributes) { return new State.Builder().define(ruleClassName, attributes); } /** * Returns a new {@link State} for this rule class with arbitrary custom behavior. This is a * convenience method for lambda definitions: * *
   *   MockRule myRule = () -> MockRule.define(
   *       "my_rule",
   *       (builder, env) -> builder.requiresConfigurationFragments(FooConfiguration.class));
   * 
*/ static State define(String ruleClassName, MockRuleCustomBehavior customBehavior) { return new State.Builder().define(ruleClassName, customBehavior); } /** * Returns the basic state that defines this rule class. This is the only interface method * implementers must override. */ State define(); /** * Builds out this rule with default attributes Blaze expects of all rules * ({@link MockRuleDefaults#DEFAULT_ATTRIBUTES}) plus the custom attributes defined by this * implementation's {@link State}. * *

Do not override this method. For extra custom behavior, use * {@link #define(String, MockRuleCustomBehavior)} */ @Override default RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { State state = define(); if (state.ancestor == State.DEFAULT_ANCESTOR) { MockRuleDefaults.DEFAULT_ATTRIBUTES.stream().forEach(builder::add); } state.customBehavior.customize(builder, environment); return builder.build(); } /** * Sets this rule class's metadata with the name defined by {@link State}, configured target * factory declared by {@link State.Builder#factory}, and ancestor rule class declared by * {@link State.Builder#ancestor}. */ @Override default RuleDefinition.Metadata getMetadata() { State state = define(); return RuleDefinition.Metadata.builder() .name(state.name) .type(RuleClass.Builder.RuleClassType.NORMAL) .factoryClass(state.factory) .ancestors(state.ancestor) .build(); } }