Introduce `InstrumentationOutput` and apply it to profile
`InstrumentationOutput` aims at providing a unified way to handle writing and publishing instrumentation outputs.
PiperOrigin-RevId: 663316138
Change-Id: Ie5d80acd1843a94045307442ed00ffd6a9f98105
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
index fcb2f94..6c89b12 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEvent.java
@@ -13,46 +13,20 @@
// limitations under the License.
package com.google.devtools.build.lib.buildtool.buildevent;
-import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
-import com.google.devtools.build.lib.profiler.Profiler;
-import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.runtime.InstrumentationOutput;
import javax.annotation.Nullable;
/** This event is fired when the profiler is started. */
public class ProfilerStartedEvent implements ExtendedEventHandler.Postable {
- @Nullable private final Path profilePath;
- @Nullable private final UploadContext streamingContext;
- private final Profiler.Format format;
+ @Nullable private final InstrumentationOutput profile;
- public ProfilerStartedEvent(
- @Nullable Path profilePath,
- @Nullable UploadContext streamingContext,
- Profiler.Format format) {
- this.profilePath = profilePath;
- this.streamingContext = streamingContext;
- this.format = format;
+ public ProfilerStartedEvent(@Nullable InstrumentationOutput profile) {
+ this.profile = profile;
}
@Nullable
- public Path getProfilePath() {
- return profilePath;
- }
-
- @Nullable
- public UploadContext getStreamingContext() {
- return streamingContext;
- }
-
- public String getName() {
- switch (format) {
- case JSON_TRACE_FILE_FORMAT -> {
- return "command.profile.json";
- }
- case JSON_TRACE_FILE_COMPRESSED_FORMAT -> {
- return "command.profile.gz";
- }
- }
- throw new UnsupportedOperationException();
+ public InstrumentationOutput getProfile() {
+ return profile;
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index ec89a88..98af82e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -43,9 +43,7 @@
import com.google.devtools.build.lib.bugreport.BugReporter;
import com.google.devtools.build.lib.bugreport.Crash;
import com.google.devtools.build.lib.bugreport.CrashContext;
-import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
-import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildtool.CommandPrecompleteEvent;
import com.google.devtools.build.lib.buildtool.buildevent.ProfilerStartedEvent;
@@ -324,27 +322,38 @@
ImmutableSet.Builder<ProfilerTask> profiledTasksBuilder = ImmutableSet.builder();
Profiler.Format format = Format.JSON_TRACE_FILE_FORMAT;
Path profilePath = null;
- UploadContext streamingContext = null;
+ InstrumentationOutput profile = null;
try {
if (tracerEnabled) {
if (options.profilePath == null) {
+ String profileName = "command.profile.gz";
format = Format.JSON_TRACE_FILE_COMPRESSED_FORMAT;
if (bepOptions != null && bepOptions.streamingLogFileUploads) {
BuildEventArtifactUploader buildEventArtifactUploader =
newUploader(env, bepOptions.buildEventUploadStrategy);
- streamingContext = buildEventArtifactUploader.startUpload(LocalFileType.LOG, null);
- out = streamingContext.getOutputStream();
+ profile =
+ new BuildEventArtifactInstrumentationOutput(
+ profileName, buildEventArtifactUploader);
+ out = profile.createOutputStream();
} else {
- profilePath = workspace.getOutputBase().getRelative("command.profile.gz");
- out = profilePath.getOutputStream();
+ profilePath = workspace.getOutputBase().getRelative(profileName);
+ profile = new LocalInstrumentationOutput(profileName, profilePath);
+ out = profile.createOutputStream();
}
} else {
format =
options.profilePath.toString().endsWith(".gz")
? Format.JSON_TRACE_FILE_COMPRESSED_FORMAT
: Format.JSON_TRACE_FILE_FORMAT;
+ String profileName =
+ (format == Format.JSON_TRACE_FILE_COMPRESSED_FORMAT)
+ ? "command.profile.gz"
+ : "command.profile.json";
profilePath = workspace.getWorkspace().getRelative(options.profilePath);
- out = profilePath.getOutputStream(/* append= */ false, /* internal= */ true);
+ profile = new LocalInstrumentationOutput(profileName, profilePath);
+ out =
+ ((LocalInstrumentationOutput) profile)
+ .createOutputStream(/* append= */ false, /* internal= */ true);
}
for (ProfilerTask profilerTask : ProfilerTask.values()) {
if (!profilerTask.isVfs()
@@ -445,7 +454,7 @@
} catch (IOException e) {
eventHandler.handle(Event.error("Error while creating profile file: " + e.getMessage()));
}
- return new ProfilerStartedEvent(profilePath, streamingContext, format);
+ return new ProfilerStartedEvent(profile);
}
public FileSystem getFileSystem() {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java
new file mode 100644
index 0000000..dad4993
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutput.java
@@ -0,0 +1,51 @@
+// Copyright 2024 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.runtime;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import java.io.OutputStream;
+import javax.annotation.Nullable;
+
+/**
+ * Used when instrumentation output should be treated as a build event artifact so that it will be
+ * uploaded to a specified location.
+ */
+final class BuildEventArtifactInstrumentationOutput implements InstrumentationOutput {
+ private final String name;
+ private final BuildEventArtifactUploader buildEventArtifactUploader;
+ @Nullable private UploadContext uploadContext;
+
+ public BuildEventArtifactInstrumentationOutput(
+ String name, BuildEventArtifactUploader buildEventArtifactUploader) {
+ this.name = checkNotNull(name);
+ this.buildEventArtifactUploader = checkNotNull(buildEventArtifactUploader);
+ }
+
+ @Override
+ public void publish(BuildToolLogCollection buildToolLogCollection) {
+ checkNotNull(uploadContext, "Cannot publish to buildToolLogCollection if upload never starts.");
+ buildToolLogCollection.addUriFuture(name, uploadContext.uriFuture());
+ }
+
+ @Override
+ public OutputStream createOutputStream() {
+ uploadContext = buildEventArtifactUploader.startUpload(LocalFileType.LOG, null);
+ return uploadContext.getOutputStream();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
index 70fe506..8a865f7 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
@@ -179,16 +179,13 @@
}
}
}
- if (profileEvent != null && profileEvent.getProfilePath() != null) {
+ if (profileEvent != null && profileEvent.getProfile() != null) {
// This leads to missing the afterCommand profiles of the other modules in the profile.
// Since the BEP currently shuts down at the BuildCompleteEvent, we cannot just move posting
// the BuildToolLogs to afterCommand of this module.
try {
Profiler.instance().stop();
- event
- .getResult()
- .getBuildToolLogCollection()
- .addLocalFile(profileEvent.getName(), profileEvent.getProfilePath());
+ profileEvent.getProfile().publish(event.getResult().getBuildToolLogCollection());
} catch (IOException e) {
reporter.handle(Event.error("Error while writing profile file: " + e.getMessage()));
}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java
new file mode 100644
index 0000000..f5130e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/InstrumentationOutput.java
@@ -0,0 +1,28 @@
+// Copyright 2024 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.runtime;
+
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Stores and publishes the instrumentation output information. */
+public interface InstrumentationOutput {
+
+ /** Creates the {@link OutputStream} for instrumentation output writes. */
+ OutputStream createOutputStream() throws IOException;
+
+ /** Publishes instrumentation output information to the {@link BuildToolLogCollection}. */
+ void publish(BuildToolLogCollection buildToolLogCollection);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java b/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java
new file mode 100644
index 0000000..aedb96a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutput.java
@@ -0,0 +1,44 @@
+// Copyright 2024 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.runtime;
+
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Used when instrumentation output is written to a local file. */
+final class LocalInstrumentationOutput implements InstrumentationOutput {
+ private final Path path;
+ private final String name;
+
+ LocalInstrumentationOutput(String name, Path path) {
+ this.name = name;
+ this.path = path;
+ }
+
+ @Override
+ public void publish(BuildToolLogCollection buildToolLogCollection) {
+ buildToolLogCollection.addLocalFile(name, path);
+ }
+
+ @Override
+ public OutputStream createOutputStream() throws IOException {
+ return path.getOutputStream();
+ }
+
+ public OutputStream createOutputStream(boolean append, boolean internal) throws IOException {
+ return path.getOutputStream(append, internal);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/BUILD
deleted file mode 100644
index e3a3672..0000000
--- a/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/BUILD
+++ /dev/null
@@ -1,29 +0,0 @@
-load("@rules_java//java:defs.bzl", "java_test")
-
-package(
- default_applicable_licenses = ["//:license"],
- default_testonly = 1,
- default_visibility = ["//src:__subpackages__"],
-)
-
-filegroup(
- name = "srcs",
- testonly = 0,
- srcs = glob(["**"]),
- visibility = ["//src:__subpackages__"],
-)
-
-java_test(
- name = "ProfilerStartedEventTest",
- srcs = ["ProfilerStartedEventTest.java"],
- deps = [
- "//src/main/java/com/google/devtools/build/lib:runtime",
- "//src/main/java/com/google/devtools/build/lib/buildeventstream",
- "//src/main/java/com/google/devtools/build/lib/profiler",
- "//src/main/java/com/google/devtools/build/lib/vfs",
- "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
- "//third_party:junit4",
- "//third_party:mockito",
- "//third_party:truth",
- ],
-)
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEventTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEventTest.java
deleted file mode 100644
index 628fb3b..0000000
--- a/src/test/java/com/google/devtools/build/lib/buildtool/buildevent/ProfilerStartedEventTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2024 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.buildtool.buildevent;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
-
-import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
-import com.google.devtools.build.lib.profiler.Profiler.Format;
-import com.google.devtools.build.lib.vfs.DigestHashFunction;
-import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.Path;
-import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link ProfilerStartedEvent} */
-@RunWith(JUnit4.class)
-public class ProfilerStartedEventTest {
- @Test
- public void testLocalJsonProfiler() {
- FileSystem fs = new InMemoryFileSystem(DigestHashFunction.SHA256);
- Path profilePath = fs.getPath("/tmp/profile");
- ProfilerStartedEvent jsonProfilerStartedEvent =
- new ProfilerStartedEvent(
- profilePath, /* streamingContext= */ null, Format.JSON_TRACE_FILE_FORMAT);
-
- assertThat(jsonProfilerStartedEvent.getProfilePath()).isSameInstanceAs(profilePath);
- assertThat(jsonProfilerStartedEvent.getStreamingContext()).isNull();
- assertThat(jsonProfilerStartedEvent.getName()).isEqualTo("command.profile.json");
- }
-
- @Test
- public void testCompressedProfilerWithBepUploadContext() {
- UploadContext streamingContext = mock(UploadContext.class);
- ProfilerStartedEvent compressedProfilerStartedEvent =
- new ProfilerStartedEvent(
- /* profilePath= */ null, streamingContext, Format.JSON_TRACE_FILE_COMPRESSED_FORMAT);
-
- assertThat(compressedProfilerStartedEvent.getProfilePath()).isNull();
- assertThat(compressedProfilerStartedEvent.getStreamingContext())
- .isSameInstanceAs(streamingContext);
- assertThat(compressedProfilerStartedEvent.getName()).isEqualTo("command.profile.gz");
- }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutputTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutputTest.java
new file mode 100644
index 0000000..a5c7555
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventArtifactInstrumentationOutputTest.java
@@ -0,0 +1,86 @@
+// Copyright 2024 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
+import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BuildEventArtifactInstrumentationOutputTest {
+ @Test
+ public void testBepInstrumentation_cannotPublishIfUploadNeverStarts() {
+ BuildEventArtifactUploader fakeBuildEventArtifactUploader =
+ mock(BuildEventArtifactUploader.class);
+ InstrumentationOutput bepInstrumentationOutput =
+ new BuildEventArtifactInstrumentationOutput("bep", fakeBuildEventArtifactUploader);
+
+ BuildToolLogCollection buildToolLogCollection = new BuildToolLogCollection();
+ assertThrows(
+ NullPointerException.class, () -> bepInstrumentationOutput.publish(buildToolLogCollection));
+ }
+
+ @Test
+ public void testBepInstrumentation_publishNameAndUriFuture()
+ throws ExecutionException, InterruptedException, IOException {
+ UploadContext fakeUploadLoadContext =
+ new UploadContext() {
+ @Override
+ public OutputStream getOutputStream() {
+ return new ByteArrayOutputStream();
+ }
+
+ @Override
+ public ListenableFuture<String> uriFuture() {
+ return immediateFuture("uri/abc12345");
+ }
+ };
+ BuildEventArtifactUploader fakeBuildEventArtifactUploader =
+ mock(BuildEventArtifactUploader.class);
+ when(fakeBuildEventArtifactUploader.startUpload(LocalFileType.LOG, null))
+ .thenReturn(fakeUploadLoadContext);
+
+ InstrumentationOutput bepInstrumentationOutput =
+ new BuildEventArtifactInstrumentationOutput("bep", fakeBuildEventArtifactUploader);
+ // Create the OutputStream will enforce fakeBuildEventArtifactUploader to create the
+ // uploadContext.
+ var unused = bepInstrumentationOutput.createOutputStream();
+ assertThat(bepInstrumentationOutput)
+ .isInstanceOf(BuildEventArtifactInstrumentationOutput.class);
+
+ BuildToolLogCollection buildToolLogCollection = new BuildToolLogCollection();
+ bepInstrumentationOutput.publish(buildToolLogCollection);
+ buildToolLogCollection.freeze();
+
+ assertThat(buildToolLogCollection.toEvent().remoteUploads()).hasSize(1);
+ ListenableFuture<String> soleRemoteUploadUri =
+ buildToolLogCollection.toEvent().remoteUploads().get(0);
+ assertThat(soleRemoteUploadUri.get()).isEqualTo("uri/abc12345");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutputTest.java b/src/test/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutputTest.java
new file mode 100644
index 0000000..e5ae19a8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/LocalInstrumentationOutputTest.java
@@ -0,0 +1,51 @@
+// Copyright 2024 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile;
+import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
+import com.google.devtools.build.lib.buildeventstream.BuildToolLogs.LogFileEntry;
+import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
+import com.google.devtools.build.lib.vfs.DigestHashFunction;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class LocalInstrumentationOutputTest {
+ @Test
+ public void testLocalInstrumentation_publishNameAndPath() {
+ FileSystem fs = new InMemoryFileSystem(DigestHashFunction.SHA256);
+ Path path = fs.getPath("/file");
+ InstrumentationOutput localInstrumentationOutput =
+ new LocalInstrumentationOutput("local", path);
+
+ assertThat(localInstrumentationOutput).isInstanceOf(LocalInstrumentationOutput.class);
+ BuildToolLogCollection buildToolLogCollection = new BuildToolLogCollection();
+ localInstrumentationOutput.publish(buildToolLogCollection);
+ buildToolLogCollection.freeze();
+
+ assertThat(buildToolLogCollection.getLocalFiles())
+ .containsExactly(
+ new LogFileEntry(
+ "local",
+ new LocalFile(
+ path, LocalFileType.LOG, /* artifact= */ null, /* artifactMetadata= */ null)));
+ }
+}