diff --git a/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java index 1bffbccd04c80c..9101208fab605a 100644 --- a/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java +++ b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java @@ -129,14 +129,20 @@ private void writeAction(ActionAnalysisMetadata action, PrintStream printStream) BuildEvent configuration = actionOwner.getConfiguration(); BuildEventStreamProtos.Configuration configProto = configuration.asStreamProto(/*context=*/ null).getConfiguration(); + stringBuilder - .append(" Owner: ") - .append(actionOwner.getLabel().toString()) + .append(" Target: ") + .append(actionOwner.getLabel()) .append('\n') .append(" Configuration: ") .append(configProto.getMnemonic()) .append('\n'); - ImmutableList aspectDescriptors = actionOwner.getAspectDescriptors(); + + // In the case of aspect-on-aspect, AspectDescriptors are listed in + // topological order of the dependency graph. + // e.g. [A -> B] would imply that aspect A is applied on top of aspect B. + ImmutableList aspectDescriptors = + actionOwner.getAspectDescriptors().reverse(); if (!aspectDescriptors.isEmpty()) { stringBuilder .append(" AspectDescriptors: [") @@ -164,8 +170,7 @@ private void writeAction(ActionAnalysisMetadata action, PrintStream printStream) .append(')'); return aspectDescription.toString(); }) - .sorted() - .collect(Collectors.joining(",\n"))) + .collect(Collectors.joining("\n -> "))) .append("]\n"); } } diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/actiongraph/ActionGraphDump.java b/src/main/java/com/google/devtools/build/lib/skyframe/actiongraph/ActionGraphDump.java index 3ee86a1ad9f4f0..7ec2080bfc3a43 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/actiongraph/ActionGraphDump.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/actiongraph/ActionGraphDump.java @@ -200,13 +200,17 @@ private void dumpSingleAction(ConfiguredTarget configuredTarget, ActionAnalysisM BuildEvent event = actionOwner.getConfiguration(); actionBuilder.setConfigurationId(knownConfigurations.dataToId(event)); - // store aspect - for (AspectDescriptor aspectDescriptor : actionOwner.getAspectDescriptors()) { + // Store aspects. + // Iterate through the aspect path and dump the aspect descriptors. + // In the case of aspect-on-aspect, AspectDescriptors are listed in topological order + // of the configured target graph. + // e.g. [A, B] would imply that aspect A is applied on top of aspect B. + for (AspectDescriptor aspectDescriptor : actionOwner.getAspectDescriptors().reverse()) { actionBuilder.addAspectDescriptorIds(knownAspectDescriptors.dataToId(aspectDescriptor)); } } - // store inputs + // Store inputs Iterable inputs = action.getInputs(); if (!(inputs instanceof NestedSet)) { inputs = NestedSetBuilder.wrap(Order.STABLE_ORDER, inputs); diff --git a/src/main/protobuf/analysis.proto b/src/main/protobuf/analysis.proto index 794b1cfec82d23..16188c9d34bbe7 100644 --- a/src/main/protobuf/analysis.proto +++ b/src/main/protobuf/analysis.proto @@ -54,6 +54,9 @@ message Action { string target_id = 1; // The aspects that were responsible for the creation of the action (if any). + // In the case of aspect-on-aspect, AspectDescriptors are listed in + // topological order of the dependency graph. + // e.g. [A, B] would imply that aspect A is applied on top of aspect B. repeated string aspect_descriptor_ids = 2; // Encodes all significant behavior that might affect the output. The key diff --git a/src/test/java/com/google/devtools/build/lib/query2/ActionGraphProtoOutputFormatterCallbackTest.java b/src/test/java/com/google/devtools/build/lib/query2/ActionGraphProtoOutputFormatterCallbackTest.java index f808013aa2c398..1dc7c157b50b0b 100644 --- a/src/test/java/com/google/devtools/build/lib/query2/ActionGraphProtoOutputFormatterCallbackTest.java +++ b/src/test/java/com/google/devtools/build/lib/query2/ActionGraphProtoOutputFormatterCallbackTest.java @@ -14,6 +14,7 @@ package com.google.devtools.build.lib.query2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -23,9 +24,11 @@ import com.google.devtools.build.lib.analysis.AnalysisProtos.Action; import com.google.devtools.build.lib.analysis.AnalysisProtos.ActionGraphContainer; import com.google.devtools.build.lib.analysis.AnalysisProtos.Artifact; +import com.google.devtools.build.lib.analysis.AnalysisProtos.AspectDescriptor; import com.google.devtools.build.lib.analysis.AnalysisProtos.DepSetOfFiles; import com.google.devtools.build.lib.analysis.AnalysisProtos.KeyValuePair; import com.google.devtools.build.lib.analysis.AnalysisProtos.ParamFile; +import com.google.devtools.build.lib.analysis.AnalysisProtos.Target; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.query2.ActionGraphProtoOutputFormatterCallback.OutputType; @@ -504,6 +507,377 @@ public void testCppActionTemplate_includesActionTemplateMnemonic() throws Except assertThat(cppCompileActionTemplates).hasSize(expectedActionsCount); } + @Test + public void testIncludeAspects_AspectOnAspect() throws Exception { + options.useAspects = true; + writeFile( + "test/rule.bzl", + "MyProvider = provider(", + " fields = {", + " 'dummy_field': 'dummy field'", + " }", + ")", + "def _my_jpl_aspect_imp(target, ctx):", + " if hasattr(ctx.rule.attr, 'srcs'):", + " out = ctx.actions.declare_file('out_jpl_{}'.format(target))", + " ctx.actions.run(", + " inputs = [f for src in ctx.rule.attr.srcs for f in src.files],", + " outputs = [out],", + " executable = 'dummy',", + " mnemonic = 'MyJplAspect'", + " )", + " return [MyProvider(dummy_field = 1)]", + "my_jpl_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [['proto_java']],", + " provides = [MyProvider],", + " implementation = _my_jpl_aspect_imp,", + ")", + "def _jpl_rule_impl(ctx):", + " return struct()", + "my_jpl_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_jpl_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " },", + " implementation = _jpl_rule_impl", + ")", + "def _aspect_impl(target, ctx):", + " if hasattr(ctx.rule.attr, 'srcs'):", + " out = ctx.actions.declare_file('out{}'.format(target))", + " ctx.actions.run(", + " inputs = [f for src in ctx.rule.attr.srcs for f in src.files],", + " outputs = [out],", + " executable = 'dummy',", + " mnemonic = 'MyAspect'", + " )", + " return [struct()]", + "my_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [[MyProvider]],", + " attrs = {", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _aspect_impl,", + ")", + "def _rule_impl(ctx):", + " return struct()", + "my_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _rule_impl", + ")"); + + writeFile( + "test/BUILD", + "load(':rule.bzl', 'my_rule', 'my_jpl_rule')", + "proto_library(", + " name = 'x',", + " srcs = [':x.proto']", + ")", + "my_jpl_rule(", + " name = 'my_java_proto',", + " deps = [':x'],", + ")", + "my_rule(", + " name = 'my_target',", + " deps = [':my_java_proto'],", + " srcs = ['foo.java'],", + " aspect_param = 'y'", + ")"); + + ActionGraphContainer actionGraphContainer = + getOutput("deps(//test:my_target)", AqueryActionFilter.emptyInstance()); + + Target protoLibraryTarget = + Iterables.getOnlyElement( + actionGraphContainer.getTargetsList().stream() + .filter(target -> target.getLabel().equals("//test:x")) + .collect(Collectors.toList())); + Action actionFromMyAspect = + Iterables.getOnlyElement( + actionGraphContainer.getActionsList().stream() + .filter( + action -> + action.getMnemonic().equals("MyAspect") + && action.getTargetId().equals(protoLibraryTarget.getId())) + .collect(Collectors.toList())); + + // Verify the aspect path of the action. + assertThat(actionFromMyAspect.getAspectDescriptorIdsCount()).isEqualTo(2); + List expectedMyAspectParams = + ImmutableList.of(KeyValuePair.newBuilder().setKey("aspect_param").setValue("y").build()); + assertCorrectAspectDescriptor( + actionGraphContainer, + actionFromMyAspect.getAspectDescriptorIds(0), + "//test:rule.bzl%my_aspect", + expectedMyAspectParams); + assertCorrectAspectDescriptor( + actionGraphContainer, + actionFromMyAspect.getAspectDescriptorIds(1), + "//test:rule.bzl%my_jpl_aspect", + ImmutableList.of()); + } + + @Test + public void testIncludeAspects_SingleAspect() throws Exception { + options.useAspects = true; + writeFile( + "test/rule.bzl", + "MyProvider = provider(", + " fields = {", + " 'dummy_field': 'dummy field'", + " }", + ")", + "def _my_jpl_aspect_imp(target, ctx):", + " if hasattr(ctx.rule.attr, 'srcs'):", + " out = ctx.actions.declare_file('out_jpl_{}'.format(target))", + " ctx.actions.run(", + " inputs = [f for src in ctx.rule.attr.srcs for f in src.files],", + " outputs = [out],", + " executable = 'dummy',", + " mnemonic = 'MyJplAspect'", + " )", + " return [MyProvider(dummy_field = 1)]", + "my_jpl_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [['proto_java']],", + " attrs = {", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _my_jpl_aspect_imp,", + ")", + "def _jpl_rule_impl(ctx):", + " return struct()", + "my_jpl_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_jpl_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _jpl_rule_impl", + ")"); + + writeFile( + "test/BUILD", + "load(':rule.bzl', 'my_jpl_rule')", + "proto_library(", + " name = 'x',", + " srcs = [':x.proto']", + ")", + "my_jpl_rule(", + " name = 'my_java_proto',", + " deps = [':x'],", + " aspect_param = 'y'", + ")"); + + ActionGraphContainer actionGraphContainer = + getOutput("deps(//test:my_java_proto)", AqueryActionFilter.emptyInstance()); + + Target protoLibraryTarget = + Iterables.getOnlyElement( + actionGraphContainer.getTargetsList().stream() + .filter(target -> target.getLabel().equals("//test:x")) + .collect(Collectors.toList())); + Action actionFromMyJplAspect = + Iterables.getOnlyElement( + actionGraphContainer.getActionsList().stream() + .filter( + action -> + action.getMnemonic().equals("MyJplAspect") + && action.getTargetId().equals(protoLibraryTarget.getId())) + .collect(Collectors.toList())); + + // Verify the aspect of the action. + assertThat(actionFromMyJplAspect.getAspectDescriptorIdsCount()).isEqualTo(1); + List expectedMyAspectParams = + ImmutableList.of(KeyValuePair.newBuilder().setKey("aspect_param").setValue("y").build()); + assertCorrectAspectDescriptor( + actionGraphContainer, + Iterables.getOnlyElement(actionFromMyJplAspect.getAspectDescriptorIdsList()), + "//test:rule.bzl%my_jpl_aspect", + expectedMyAspectParams); + } + + @Test + public void testIncludeAspects_twoAspectsOneTarget_separateAspectDescriptors() throws Exception { + options.useAspects = true; + writeFile( + "test/rule.bzl", + "JplProvider = provider(", + " fields = {", + " 'dummy_field': 'dummy field'", + " }", + ")", + "RandomProvider = provider(", + " fields = {", + " 'dummy_field': 'dummy field'", + " }", + ")", + "def _common_impl(target, ctx, outfilename, mnemonic, provider):", + " if hasattr(ctx.rule.attr, 'srcs'):", + " out = ctx.actions.declare_file(outfilename)", + " ctx.actions.run(", + " inputs = [f for src in ctx.rule.attr.srcs for f in src.files],", + " outputs = [out],", + " executable = 'dummy',", + " mnemonic = mnemonic", + " )", + " return [provider(dummy_field = 1)]", + "def _my_random_aspect_impl(target, ctx):", + " return _common_impl(target, ctx, 'rand_out', 'MyRandomAspect', RandomProvider)", + "my_random_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [['proto_java']],", + " provides = [RandomProvider],", + " implementation = _my_random_aspect_impl,", + ")", + "def _rule_impl(ctx):", + " return struct()", + "my_random_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_random_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " },", + " implementation = _rule_impl", + ")", + "def _my_jpl_aspect_impl(target, ctx):", + " return _common_impl(target, ctx, 'jpl_out', 'MyJplAspect', JplProvider)", + "my_jpl_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [['proto_java']],", + " provides = [JplProvider],", + " implementation = _my_jpl_aspect_impl,", + ")", + "my_jpl_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_jpl_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " },", + " implementation = _rule_impl", + ")"); + + writeFile( + "test/BUILD", + "load(':rule.bzl', 'my_jpl_rule', 'my_random_rule')", + "proto_library(", + " name = 'x',", + " srcs = [':x.proto']", + ")", + "my_jpl_rule(", + " name = 'target_1',", + " deps = [':x'],", + ")", + "my_random_rule(", + " name = 'target_2',", + " deps = [':x'],", + ")"); + + ActionGraphContainer actionGraphContainer = + getOutput("//test:all", AqueryActionFilter.emptyInstance()); + + Target protoLibraryTarget = + Iterables.getOnlyElement( + actionGraphContainer.getTargetsList().stream() + .filter(target -> target.getLabel().equals("//test:x")) + .collect(Collectors.toList())); + Action actionFromMyJplAspect = + Iterables.getOnlyElement( + actionGraphContainer.getActionsList().stream() + .filter( + action -> + action.getMnemonic().equals("MyJplAspect") + && action.getTargetId().equals(protoLibraryTarget.getId())) + .collect(Collectors.toList())); + Action actionFromMyRandomAspect = + Iterables.getOnlyElement( + actionGraphContainer.getActionsList().stream() + .filter( + action -> + action.getMnemonic().equals("MyRandomAspect") + && action.getTargetId().equals(protoLibraryTarget.getId())) + .collect(Collectors.toList())); + + // Verify the aspect path of the action contains exactly 1 aspect. + assertThat(actionFromMyJplAspect.getAspectDescriptorIdsCount()).isEqualTo(1); + assertCorrectAspectDescriptor( + actionGraphContainer, + Iterables.getOnlyElement(actionFromMyJplAspect.getAspectDescriptorIdsList()), + "//test:rule.bzl%my_jpl_aspect", + ImmutableList.of()); + assertThat(actionFromMyRandomAspect.getAspectDescriptorIdsCount()).isEqualTo(1); + assertCorrectAspectDescriptor( + actionGraphContainer, + Iterables.getOnlyElement(actionFromMyRandomAspect.getAspectDescriptorIdsList()), + "//test:rule.bzl%my_random_aspect", + ImmutableList.of()); + } + + @Test + public void testIncludeAspects_flagDisabled_NoAspect() throws Exception { + // The flag --include_aspects is set to false by default. + writeFile( + "test/rule.bzl", + "MyProvider = provider(", + " fields = {", + " 'dummy_field': 'dummy field'", + " }", + ")", + "def _my_jpl_aspect_imp(target, ctx):", + " if hasattr(ctx.rule.attr, 'srcs'):", + " out = ctx.actions.declare_file('out_jpl_{}'.format(target))", + " ctx.actions.run(", + " inputs = [f for src in ctx.rule.attr.srcs for f in src.files],", + " outputs = [out],", + " executable = 'dummy',", + " mnemonic = 'MyJplAspect'", + " )", + " return [MyProvider(dummy_field = 1)]", + "my_jpl_aspect = aspect(", + " attr_aspects = ['deps', 'exports'],", + " required_aspect_providers = [['proto_java']],", + " attrs = {", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _my_jpl_aspect_imp,", + ")", + "def _jpl_rule_impl(ctx):", + " return struct()", + "my_jpl_rule = rule(", + " attrs = {", + " 'deps': attr.label_list(aspects = [my_jpl_aspect]),", + " 'srcs': attr.label_list(allow_files = True),", + " 'aspect_param': attr.string(default = 'x', values = ['x', 'y'])", + " },", + " implementation = _jpl_rule_impl", + ")"); + + writeFile( + "test/BUILD", + "load(':rule.bzl', 'my_jpl_rule')", + "proto_library(", + " name = 'x',", + " srcs = [':x.proto']", + ")", + "my_jpl_rule(", + " name = 'my_java_proto',", + " deps = [':x'],", + " aspect_param = 'y'", + ")"); + + ActionGraphContainer actionGraphContainer = + getOutput("deps(//test:my_java_proto)", AqueryActionFilter.emptyInstance()); + + assertThat( + actionGraphContainer.getActionsList().stream() + .filter(action -> !action.getAspectDescriptorIdsList().isEmpty()) + .collect(Collectors.toList())) + .isEmpty(); + } + private AnalysisProtos.ActionGraphContainer getOutput(String queryExpression) throws Exception { return getOutput(queryExpression, /* actionFilters= */ AqueryActionFilter.emptyInstance()); } @@ -570,6 +944,22 @@ private void assertMatchingOnlyAction( assertThat(action.getOutputIdsList()).contains(outputId); } + private void assertCorrectAspectDescriptor( + ActionGraphContainer actionGraphContainer, + String aspectDescriptorId, + String expectedName, + List expectedParameters) { + for (AspectDescriptor aspectDescriptor : actionGraphContainer.getAspectDescriptorsList()) { + if (!aspectDescriptorId.equals(aspectDescriptor.getId())) { + continue; + } + assertThat(aspectDescriptor.getName()).isEqualTo(expectedName); + assertThat(aspectDescriptor.getParametersList()).isEqualTo(expectedParameters); + return; + } + fail("Should have matched at least one AspectDescriptor."); + } + private AqueryActionFilter constructActionFilter(ImmutableMap patternStrings) { AqueryActionFilter.Builder builder = AqueryActionFilter.builder(); for (Entry e : patternStrings.entrySet()) { diff --git a/src/test/shell/integration/aquery_test.sh b/src/test/shell/integration/aquery_test.sh index 86efff9f12aac9..1346f0d065a276 100755 --- a/src/test/shell/integration/aquery_test.sh +++ b/src/test/shell/integration/aquery_test.sh @@ -118,7 +118,7 @@ EOF cat output >> "$TEST_log" assert_contains "action 'Executing genrule //$pkg:bar'" output assert_contains "Mnemonic: Genrule" output - assert_contains "Owner: //$pkg:bar" output + assert_contains "Target: //$pkg:bar" output assert_contains "Configuration: .*-fastbuild" output # Only check that the inputs/outputs/command line/environment exist, but not # their actual contents since that would be too much. @@ -199,7 +199,7 @@ EOF || fail "Expected success" cat output >> "$TEST_log" assert_contains "Mnemonic: SkylarkAction" output - assert_contains "Owner: //$pkg:goo" output + assert_contains "Target: //$pkg:goo" output assert_contains "Environment: \[.*foo=bar" output } @@ -822,4 +822,129 @@ EOF 2> "$TEST_log" || fail "Expected success" } +function test_aquery_aspect_on_aspect() { + local pkg="${FUNCNAME[0]}" + mkdir -p "$pkg" || fail "mkdir -p $pkg" + cat > "$pkg/rule.bzl" <<'EOF' +IntermediateProvider = provider( + fields = { + 'dummy_field': 'dummy field' + } +) + +MyBaseRuleProvider = provider( + fields = { + 'dummy_field': 'dummy field' + } +) + +def _base_rule_impl(ctx): + return [MyBaseRuleProvider(dummy_field = 1)] + +base_rule = rule( + attrs = { + 'srcs': attr.label_list(allow_files = True), + }, + implementation = _base_rule_impl +) + +def _intermediate_aspect_imp(target, ctx): + if hasattr(ctx.rule.attr, 'srcs'): + out = ctx.actions.declare_file('out_jpl_{}'.format(target)) + ctx.actions.run( + inputs = [f for src in ctx.rule.attr.srcs for f in src.files], + outputs = [out], + executable = 'dummy', + mnemonic = 'MyIntermediateAspect' + ) + + return [IntermediateProvider(dummy_field = 1)] + +intermediate_aspect = aspect( + attr_aspects = ['deps', 'exports'], + required_aspect_providers = [[MyBaseRuleProvider]], + provides = [IntermediateProvider], + implementation = _intermediate_aspect_imp, +) + +def _int_rule_impl(ctx): + return struct() + +intermediate_rule = rule( + attrs = { + 'deps': attr.label_list(aspects = [intermediate_aspect]), + 'srcs': attr.label_list(allow_files = True), + }, + implementation = _int_rule_impl +) + +def _aspect_impl(target, ctx): + if hasattr(ctx.rule.attr, 'srcs'): + out = ctx.actions.declare_file('out{}'.format(target)) + ctx.actions.run( + inputs = [f for src in ctx.rule.attr.srcs for f in src.files], + outputs = [out], + executable = 'dummy', + mnemonic = 'MyAspect' + ) + + return [struct()] + +my_aspect = aspect( + attr_aspects = ['deps', 'exports'], + required_aspect_providers = [[IntermediateProvider], [MyBaseRuleProvider]], + attrs = { + 'aspect_param': attr.string(default = 'x', values = ['x', 'y']) + }, + implementation = _aspect_impl, +) + +def _rule_impl(ctx): + return struct() + +my_rule = rule( + attrs = { + 'deps': attr.label_list(aspects = [my_aspect]), + 'srcs': attr.label_list(allow_files = True), + 'aspect_param': attr.string(default = 'x', values = ['x', 'y']) + }, + implementation = _rule_impl +) +EOF + + cat > "$pkg/BUILD" <<'EOF' +load(':rule.bzl', 'my_rule', 'intermediate_rule', 'base_rule') + +base_rule( + name = 'x', + srcs = [':x.java'], +) + +intermediate_rule( + name = 'int_target', + deps = [':x'], +) + +my_rule( + name = 'my_target', + srcs = ['foo.java'], + aspect_param = 'y', + deps = [':int_target'], +) +EOF + + QUERY="deps(//$pkg:my_target)" + + bazel aquery --output=text --include_aspects ${QUERY} > output 2> "$TEST_log" \ + || fail "Expected success" + cat output >> "$TEST_log" + + assert_contains "Mnemonic: MyAspect" output + assert_contains "AspectDescriptors: \[.*:rule.bzl%my_aspect(aspect_param='y')" output + assert_contains "^.*->.*:rule.bzl%intermediate_aspect()\]$" output + + bazel aquery --output=textproto --include_aspects --noinclude_commandline ${QUERY} > output \ + 2> "$TEST_log" || fail "Expected success" +} + run_suite "${PRODUCT_NAME} action graph query tests"