| // Copyright 2022 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.testutil; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.eventbus.AllowConcurrentEvents; |
| import com.google.common.eventbus.Subscribe; |
| import com.google.devtools.build.lib.actions.ActionCompletionEvent; |
| import com.google.devtools.build.lib.actions.ActionExecutedEvent; |
| import com.google.devtools.build.lib.actions.ActionExecutionMetadata; |
| import com.google.devtools.build.lib.actions.ActionMiddlemanEvent; |
| import com.google.devtools.build.lib.actions.ActionResultReceivedEvent; |
| import com.google.devtools.build.lib.actions.ActionStartedEvent; |
| import com.google.devtools.build.lib.actions.CachedActionEvent; |
| import com.google.devtools.build.lib.skyframe.proto.ActionRewind.ActionRewindEvent; |
| import com.google.devtools.build.lib.skyframe.rewinding.ActionRewindingStats; |
| import com.google.devtools.build.lib.skyframe.rewinding.ActionRewoundEvent; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| |
| /** Records various action-related events for tests. */ |
| public final class ActionEventRecorder { |
| |
| private final List<ActionStartedEvent> actionStartedEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionCompletionEvent> actionCompletionEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionExecutedEvent> actionExecutedEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionResultReceivedEvent> actionResultReceivedEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionMiddlemanEvent> actionMiddlemanEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<CachedActionEvent> cachedActionEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionRewoundEvent> actionRewoundEvents = |
| Collections.synchronizedList(new ArrayList<>()); |
| private final List<ActionRewindingStats> actionRewindingStatsPosts = |
| Collections.synchronizedList(new ArrayList<>()); |
| |
| private Consumer<ActionRewoundEvent> actionRewoundEventSubscriber = e -> {}; |
| |
| public void setActionRewoundEventSubscriber(Consumer<ActionRewoundEvent> subscriber) { |
| actionRewoundEventSubscriber = subscriber; |
| } |
| |
| public List<ActionStartedEvent> getActionStartedEvents() { |
| return actionStartedEvents; |
| } |
| |
| public List<ActionCompletionEvent> getActionCompletionEvents() { |
| return actionCompletionEvents; |
| } |
| |
| public List<ActionExecutedEvent> getActionExecutedEvents() { |
| return actionExecutedEvents; |
| } |
| |
| public List<ActionResultReceivedEvent> getActionResultReceivedEvents() { |
| return actionResultReceivedEvents; |
| } |
| |
| public List<ActionRewoundEvent> getActionRewoundEvents() { |
| return actionRewoundEvents; |
| } |
| |
| public List<ActionRewindingStats> getActionRewindingStatsPosts() { |
| return actionRewindingStatsPosts; |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionStarted(ActionStartedEvent event) { |
| actionStartedEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionCompleted(ActionCompletionEvent event) { |
| actionCompletionEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionExecuted(ActionExecutedEvent event) { |
| actionExecutedEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionResultReceived(ActionResultReceivedEvent event) { |
| actionResultReceivedEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionMiddleman(ActionMiddlemanEvent event) { |
| actionMiddlemanEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void cachedAction(CachedActionEvent event) { |
| cachedActionEvents.add(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionRewound(ActionRewoundEvent event) { |
| actionRewoundEvents.add(event); |
| actionRewoundEventSubscriber.accept(event); |
| } |
| |
| @SuppressWarnings("unused") |
| @Subscribe |
| @AllowConcurrentEvents |
| void actionRewindingStats(ActionRewindingStats actionRewindingStats) { |
| actionRewindingStatsPosts.add(actionRewindingStats); |
| } |
| |
| public void clear() { |
| actionStartedEvents.clear(); |
| actionCompletionEvents.clear(); |
| actionExecutedEvents.clear(); |
| actionResultReceivedEvents.clear(); |
| actionMiddlemanEvents.clear(); |
| cachedActionEvents.clear(); |
| actionRewoundEvents.clear(); |
| actionRewindingStatsPosts.clear(); |
| } |
| |
| /** |
| * Check how many of each type of event was emitted during a successful build. |
| * |
| * @param runOnce Actions which ran and are not rewound |
| * @param completedRewound Actions which ran and then are rewound by a later failed action |
| * @param failedRewound Actions which fail because of lost inputs and which rewind themselves and |
| * the actions that generate those lost inputs |
| * @param exactlyOneMiddlemanEventChecks A list of predicates which should be satisfied exactly |
| * once by the sequence of middleman events emitted |
| */ |
| public void assertEvents( |
| ImmutableList<String> runOnce, |
| ImmutableList<String> completedRewound, |
| ImmutableList<String> failedRewound, |
| ImmutableList<Predicate<ActionMiddlemanEvent>> exactlyOneMiddlemanEventChecks, |
| ImmutableList<Integer> actionRewindingPostLostInputCounts) { |
| assertEvents( |
| runOnce, |
| completedRewound, |
| failedRewound, |
| exactlyOneMiddlemanEventChecks, |
| /*expectResultReceivedForFailedRewound=*/ true, |
| actionRewindingPostLostInputCounts, |
| /*lostInputAndActionsPosts=*/ ImmutableList.of()); |
| } |
| |
| /** |
| * Like {@link #assertEvents(ImmutableList, ImmutableList, ImmutableList, ImmutableList, |
| * ImmutableList)}. The {@code expectResultReceivedForFailedRewound} should be true iff the failed |
| * rewound actions ever successfully complete. |
| * |
| * @param expectResultReceivedForFailedRewound whether the failed rewound actions ever |
| * successfully complete, because no {@link ActionResultReceivedEvent} is emitted for a failed |
| * action |
| */ |
| public void assertEvents( |
| ImmutableList<String> runOnce, |
| ImmutableList<String> completedRewound, |
| ImmutableList<String> failedRewound, |
| ImmutableList<Predicate<ActionMiddlemanEvent>> exactlyOneMiddlemanEventChecks, |
| boolean expectResultReceivedForFailedRewound, |
| ImmutableList<Integer> actionRewindingPostLostInputCounts, |
| ImmutableList<ImmutableList<ActionRewindEventAsserter>> lostInputAndActionsPosts) { |
| EventCountAsserter eventCountAsserter = |
| new EventCountAsserter(runOnce, completedRewound, failedRewound); |
| |
| eventCountAsserter.assertEventCounts( |
| /*events=*/ actionStartedEvents, |
| /*eventsName=*/ "actionStartedEvents", |
| /*converter=*/ e -> progressMessageOrPrettyPrint(e.getAction()), |
| /*expectedRunOnceEventCount=*/ 1, |
| /*expectedCompletedRewoundEventCount=*/ 1, |
| /*expectedFailedRewoundEventCount=*/ 2); |
| |
| eventCountAsserter.assertEventCounts( |
| /*events=*/ actionCompletionEvents, |
| /*eventsName=*/ "actionCompletionEvents", |
| /*converter=*/ e -> progressMessageOrPrettyPrint(e.getAction()), |
| /*expectedRunOnceEventCount=*/ 1, |
| /*expectedCompletedRewoundEventCount=*/ 1, |
| /*expectedFailedRewoundEventCount=*/ 1); |
| |
| eventCountAsserter.assertEventCounts( |
| /*events=*/ actionExecutedEvents, |
| /*eventsName=*/ "actionExecutedEvents", |
| /*converter=*/ e -> progressMessageOrPrettyPrint(e.getAction()), |
| /*expectedRunOnceEventCount=*/ 1, |
| /*expectedCompletedRewoundEventCount=*/ 1, |
| /*expectedFailedRewoundEventCount=*/ 1); |
| |
| eventCountAsserter.assertEventCounts( |
| /*events=*/ actionResultReceivedEvents, |
| /*eventsName=*/ "actionResultReceivedEvents", |
| /*converter=*/ e -> progressMessageOrPrettyPrint(e.getAction()), |
| /*expectedRunOnceEventCount=*/ 1, |
| /*expectedCompletedRewoundEventCount=*/ 1, |
| /*expectedFailedRewoundEventCount=*/ expectResultReceivedForFailedRewound ? 1 : 0); |
| |
| eventCountAsserter.assertEventCounts( |
| /*events=*/ actionRewoundEvents, |
| /*eventsName=*/ "actionRewoundEvents", |
| /*converter=*/ e -> progressMessageOrPrettyPrint(e.getFailedRewoundAction()), |
| /*expectedRunOnceEventCount=*/ 0, |
| /*expectedCompletedRewoundEventCount=*/ 0, |
| /*expectedFailedRewoundEventCount=*/ 1); |
| |
| assertRewindActionStats(actionRewindingPostLostInputCounts, lostInputAndActionsPosts); |
| |
| // No middlemen, no worries |
| // for (Predicate<ActionMiddlemanEvent> check : exactlyOneMiddlemanEventChecks) { |
| // assertThat(actionMiddlemanEvents.stream().filter(check).count()).isEqualTo(1); |
| // } |
| assertThat(cachedActionEvents).isEmpty(); |
| } |
| |
| /** |
| * Assert that the contents of the {@link ActionRewindingStats} event matches expected results. |
| * |
| * @param totalLostInputCounts - The list of the counts of all lost inputs logged with each |
| * ActionRewindingStats post. |
| * @param lostInputAndActionsPosts - The list of all expected values in all ActionRewindingStats |
| * posts. The outer list represents the ActionRewindingStats posts, the inner list contains |
| * {@link ActionRewindEventAsserter} that has the expected values of each action rewind event |
| * within each corresponding post. |
| */ |
| public void assertRewindActionStats( |
| ImmutableList<Integer> totalLostInputCounts, |
| ImmutableList<ImmutableList<ActionRewindEventAsserter>> lostInputAndActionsPosts) { |
| assertThat(actionRewindingStatsPosts).hasSize(totalLostInputCounts.size()); |
| for (int postIndex = 0; postIndex < lostInputAndActionsPosts.size(); postIndex++) { |
| ActionRewindingStats actionRewindingStats = actionRewindingStatsPosts.get(postIndex); |
| assertThat(actionRewindingStats.lostInputsCount()) |
| .isEqualTo(totalLostInputCounts.get(postIndex)); |
| ImmutableList<ActionRewindEventAsserter> expectedLostInputPost = |
| lostInputAndActionsPosts.get(postIndex); |
| |
| for (int r = 0; r < actionRewindingStats.actionRewindEvents().size(); r++) { |
| ActionRewindEvent actualActionRewind = actionRewindingStats.actionRewindEvents().get(r); |
| expectedLostInputPost.get(r).assertActionRewindEvent(actualActionRewind); |
| } |
| } |
| } |
| |
| @AutoValue |
| public abstract static class ActionRewindEventAsserter { |
| |
| public static ActionRewindEventAsserter create( |
| String expectedActionMnemonic, |
| String expectedActionOwnerLabel, |
| int expectedLostInputsCount, |
| int expectedInvalidatedNodesCount) { |
| return new AutoValue_ActionEventRecorder_ActionRewindEventAsserter( |
| expectedActionMnemonic, |
| expectedActionOwnerLabel, |
| expectedLostInputsCount, |
| expectedInvalidatedNodesCount); |
| } |
| |
| ActionRewindEventAsserter() {} |
| |
| private void assertActionRewindEvent(ActionRewindEvent actualEvent) { |
| assertThat(actualEvent.getActionDescription().getType()).isEqualTo(expectedActionMnemonic()); |
| assertThat(actualEvent.getActionDescription().getRuleLabel()) |
| .isEqualTo(expectedActionOwnerLabel()); |
| assertThat(actualEvent.getTotalLostInputsCount()).isEqualTo(expectedLostInputsCount()); |
| assertThat(actualEvent.getInvalidatedNodesCount()).isEqualTo(expectedInvalidatedNodesCount()); |
| } |
| |
| abstract String expectedActionMnemonic(); |
| |
| abstract String expectedActionOwnerLabel(); |
| |
| abstract int expectedLostInputsCount(); |
| |
| abstract int expectedInvalidatedNodesCount(); |
| } |
| |
| private static final class EventCountAsserter { |
| private final ImmutableList<String> runOnce; |
| private final ImmutableList<String> completedRewound; |
| private final ImmutableList<String> failedRewound; |
| |
| private EventCountAsserter( |
| ImmutableList<String> runOnce, |
| ImmutableList<String> completedRewound, |
| ImmutableList<String> failedRewound) { |
| this.runOnce = runOnce; |
| this.completedRewound = completedRewound; |
| this.failedRewound = failedRewound; |
| } |
| |
| private <T> void assertEventCounts( |
| List<T> events, |
| String eventsName, |
| Function<T, String> converter, |
| int expectedRunOnceEventCount, |
| int expectedCompletedRewoundEventCount, |
| int expectedFailedRewoundEventCount) { |
| ImmutableList<String> eventDescriptions = |
| events.stream().map(converter).collect(toImmutableList()); |
| for (String runOnceAction : runOnce) { |
| assertWithMessage("Run-once action \"%s\" in %s", runOnceAction, eventsName) |
| .that(eventDescriptions.stream().filter(d -> d.equals(runOnceAction)).count()) |
| .isEqualTo(expectedRunOnceEventCount); |
| } |
| for (String rewoundAction : completedRewound) { |
| assertWithMessage("Completed rewound action \"%s\" in %s", rewoundAction, eventsName) |
| .that(eventDescriptions.stream().filter(d -> d.equals(rewoundAction)).count()) |
| .isEqualTo(expectedCompletedRewoundEventCount); |
| } |
| for (String failedRewoundAction : failedRewound) { |
| assertWithMessage("Failed rewound action \"%s\" in %s", failedRewoundAction, eventsName) |
| .that(eventDescriptions.stream().filter(d -> d.equals(failedRewoundAction)).count()) |
| .isEqualTo(expectedFailedRewoundEventCount); |
| } |
| } |
| } |
| |
| public static String progressMessageOrPrettyPrint(ActionExecutionMetadata action) { |
| String progressMessage = action.getProgressMessage(); |
| return progressMessage != null ? progressMessage : action.prettyPrint(); |
| } |
| } |