From 79a60e138ce5d63aa5bf0e5b447fbfff23f4b4e7 Mon Sep 17 00:00:00 2001
From: Nikita Tkachenko
<121111529+nikita-tkachenko-datadog@users.noreply.github.com>
Date: Wed, 6 Aug 2025 14:50:14 +0200
Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=8D=92=209318=20-=20Do=20not=20foll?=
=?UTF-8?q?ow=20symlinks=20by=20default=20when=20building=20repository=20i?=
=?UTF-8?q?ndex=20(#9322)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../source/index/RepoIndexBuilder.java | 20 +++++++++++++------
.../trace/api/config/CiVisibilityConfig.java | 2 ++
.../main/java/datadog/trace/api/Config.java | 8 ++++++++
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java
index 805cb4e56c1..23b93ec5580 100644
--- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java
+++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java
@@ -104,6 +104,7 @@ private static final class RepoIndexingFileVisitor implements FileVisitor
private final RepoIndexingStats indexingStats;
private final Path repoRoot;
private final AtomicInteger sourceRootCounter;
+ private final boolean followSymlinks;
private RepoIndexingFileVisitor(
Config config,
@@ -120,16 +121,23 @@ private RepoIndexingFileVisitor(
packageTree = new PackageTree(config);
indexingStats = new RepoIndexingStats();
sourceRootCounter = new AtomicInteger();
+ followSymlinks = config.isCiVisibilityRepoIndexFollowSymlinks();
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
- if (Files.isSymbolicLink(dir) && readSymbolicLink(dir).startsWith(repoRoot)) {
- // The path is a symlink that points inside the repo.
- // We'll visit the folder that it points to anyway,
- // moreover, we don't want two different results for one file
- // (one containing the symlink, the other - the actual folder).
- return FileVisitResult.SKIP_SUBTREE;
+ if (Files.isSymbolicLink(dir)) {
+ if (!followSymlinks) {
+ // Configured to skip symlinks
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ if (readSymbolicLink(dir).startsWith(repoRoot)) {
+ // The path is a symlink that points inside the repo.
+ // We'll visit the folder that it points to anyway,
+ // moreover, we don't want two different results for one file
+ // (one containing the symlink, the other - the actual folder).
+ return FileVisitResult.SKIP_SUBTREE;
+ }
}
return FileVisitResult.CONTINUE;
}
diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java
index ad93aa9dfc7..e39832f1432 100644
--- a/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java
+++ b/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java
@@ -44,6 +44,8 @@ public final class CiVisibilityConfig {
"civisibility.ciprovider.integration.enabled";
public static final String CIVISIBILITY_REPO_INDEX_DUPLICATE_KEY_CHECK_ENABLED =
"civisibility.repo.index.duplicate.key.check.enabled";
+ public static final String CIVISIBILITY_REPO_INDEX_FOLLOW_SYMLINKS =
+ "civisibility.repo.index.follow.symlinks";
public static final String CIVISIBILITY_EXECUTION_SETTINGS_CACHE_SIZE =
"civisibility.execution.settings.cache.size";
public static final String CIVISIBILITY_JVM_INFO_CACHE_SIZE = "civisibility.jvm.info.cache.size";
diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java
index 5ff002d4144..1d257c56542 100644
--- a/internal-api/src/main/java/datadog/trace/api/Config.java
+++ b/internal-api/src/main/java/datadog/trace/api/Config.java
@@ -254,6 +254,7 @@
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_REMOTE_ENV_VARS_PROVIDER_KEY;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_REMOTE_ENV_VARS_PROVIDER_URL;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_REPO_INDEX_DUPLICATE_KEY_CHECK_ENABLED;
+import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_REPO_INDEX_FOLLOW_SYMLINKS;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_RESOURCE_FOLDER_NAMES;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_SCALATEST_FORK_MONITOR_ENABLED;
@@ -1001,6 +1002,7 @@ public static String getHostName() {
private final boolean ciVisibilityTestSkippingEnabled;
private final boolean ciVisibilityCiProviderIntegrationEnabled;
private final boolean ciVisibilityRepoIndexDuplicateKeyCheckEnabled;
+ private final boolean ciVisibilityRepoIndexFollowSymlinks;
private final int ciVisibilityExecutionSettingsCacheSize;
private final int ciVisibilityJvmInfoCacheSize;
private final int ciVisibilityCoverageRootPackagesLimit;
@@ -2261,6 +2263,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
configProvider.getBoolean(CIVISIBILITY_CIPROVIDER_INTEGRATION_ENABLED, true);
ciVisibilityRepoIndexDuplicateKeyCheckEnabled =
configProvider.getBoolean(CIVISIBILITY_REPO_INDEX_DUPLICATE_KEY_CHECK_ENABLED, true);
+ ciVisibilityRepoIndexFollowSymlinks =
+ configProvider.getBoolean(CIVISIBILITY_REPO_INDEX_FOLLOW_SYMLINKS, false);
ciVisibilityExecutionSettingsCacheSize =
configProvider.getInteger(CIVISIBILITY_EXECUTION_SETTINGS_CACHE_SIZE, 16);
ciVisibilityJvmInfoCacheSize = configProvider.getInteger(CIVISIBILITY_JVM_INFO_CACHE_SIZE, 8);
@@ -3822,6 +3826,10 @@ public boolean isCiVisibilityRepoIndexDuplicateKeyCheckEnabled() {
return ciVisibilityRepoIndexDuplicateKeyCheckEnabled;
}
+ public boolean isCiVisibilityRepoIndexFollowSymlinks() {
+ return ciVisibilityRepoIndexFollowSymlinks;
+ }
+
public int getCiVisibilityExecutionSettingsCacheSize() {
return ciVisibilityExecutionSettingsCacheSize;
}
From 7beb0c310e6c02b599e70e4df40e1f678e913c8f Mon Sep 17 00:00:00 2001
From: Lucas Rogerio Caetano Ferreira
Date: Thu, 7 Aug 2025 15:53:53 -0500
Subject: [PATCH 02/11] Update GraalVM config to reflect TempLocationManager's
new package (#9338)
---
.../nativeimage/NativeImageGeneratorRunnerInstrumentation.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java
index fa3a0bc92c8..382a8266c78 100644
--- a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java
+++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java
@@ -70,7 +70,6 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[
+ "com.datadog.profiling.controller.openjdk.events.TimelineEvent:build_time,"
+ "com.datadog.profiling.controller.openjdk.events.SmapEntryEvent:build_time,"
+ "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory$SmapParseErrorEvent:build_time,"
- + "com.datadog.profiling.controller.TempLocationManager$SingletonHolder:run_time,"
+ "com.datadog.profiling.ddprof.JavaProfilerLoader:run_time,"
+ "datadog.environment.JavaVirtualMachine:rerun,"
+ "datadog.trace.agent.tooling.WeakMaps$Adapter:build_time,"
@@ -154,6 +153,7 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[
+ "datadog.trace.logging.LogReporter:build_time,"
+ "datadog.trace.logging.PrintStreamWrapper:build_time,"
+ "datadog.trace.util.CollectionUtils:build_time,"
+ + "datadog.trace.util.TempLocationManager$SingletonHolder:run_time,"
+ "datadog.slf4j.helpers.NOPLoggerFactory:build_time,"
+ "datadog.slf4j.helpers.SubstituteLoggerFactory:build_time,"
+ "datadog.slf4j.impl.StaticLoggerBinder:build_time,"
From 579165c0462c571e07651be204e2861b2a672d09 Mon Sep 17 00:00:00 2001
From: Andrea Marziali
Date: Fri, 8 Aug 2025 15:29:33 +0200
Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=8D=92=209184=20-=20Make=20rum=20in?=
=?UTF-8?q?jector=20stream/writer=20more=20resilient=20to=20errors=20(#934?=
=?UTF-8?q?0)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Make rum injector stream/writer more resilient to errors
(cherry picked from commit a298c600bc8210c05f671e76ade2e7f356793320)
* fix tests
(cherry picked from commit 62acf7c61ea89e19c1cc42a7ccb06a5d0851bd86)
* fix tests
(cherry picked from commit 5a32d5116a442039465ac72fb5d64684247116df)
* Update dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java
Co-authored-by: Stuart McCulloch
(cherry picked from commit 87d15fbde3d22f829859cee530903ff94c132363)
* apply suggestions
(cherry picked from commit 21afeb2cac550b9fcfeca804b785ecd362c270b4)
---------
Co-authored-by: Stuart McCulloch
---
.../buffer/InjectingPipeOutputStream.java | 74 ++++++++++------
.../buffer/InjectingPipeWriter.java | 85 ++++++++++++-------
.../InjectingPipeOutputStreamTest.groovy | 67 +++++++++++++++
.../buffer/InjectingPipeWriterTest.groovy | 67 +++++++++++++++
4 files changed, 240 insertions(+), 53 deletions(-)
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java
index 633db0c8d4c..8fa6e115c4d 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java
@@ -5,16 +5,19 @@
/**
* An OutputStream containing a circular buffer with a lookbehind buffer of n bytes. The first time
- * that the latest n bytes matches the marker, a content is injected before.
+ * that the latest n bytes matches the marker, a content is injected before. In case of IOException
+ * thrown by the downstream, the buffer will be lost unless the error occurred when draining it. In
+ * this case the draining will be resumed.
*/
public class InjectingPipeOutputStream extends OutputStream {
private final byte[] lookbehind;
private int pos;
- private boolean bufferFilled;
+ private int count;
private final byte[] marker;
private final byte[] contentToInject;
- private boolean found = false;
- private int matchingPos = 0;
+ private boolean filter;
+ private boolean wasDraining;
+ private int matchingPos;
private final Runnable onContentInjected;
private final int bulkWriteThreshold;
private final OutputStream downstream;
@@ -34,6 +37,11 @@ public InjectingPipeOutputStream(
this.marker = marker;
this.lookbehind = new byte[marker.length];
this.pos = 0;
+ this.count = 0;
+ this.matchingPos = 0;
+ this.wasDraining = false;
+ // should filter the stream to potentially inject into it.
+ this.filter = true;
this.contentToInject = contentToInject;
this.onContentInjected = onContentInjected;
this.bulkWriteThreshold = marker.length * 2 - 2;
@@ -41,25 +49,27 @@ public InjectingPipeOutputStream(
@Override
public void write(int b) throws IOException {
- if (found) {
+ if (!filter) {
+ if (wasDraining) {
+ // continue draining
+ drain();
+ }
downstream.write(b);
return;
}
- if (bufferFilled) {
+ if (count == lookbehind.length) {
downstream.write(lookbehind[pos]);
+ } else {
+ count++;
}
lookbehind[pos] = (byte) b;
pos = (pos + 1) % lookbehind.length;
- if (!bufferFilled) {
- bufferFilled = pos == 0;
- }
-
if (marker[matchingPos++] == b) {
if (matchingPos == marker.length) {
- found = true;
+ filter = false;
downstream.write(contentToInject);
if (onContentInjected != null) {
onContentInjected.run();
@@ -73,10 +83,15 @@ public void write(int b) throws IOException {
@Override
public void write(byte[] array, int off, int len) throws IOException {
- if (found) {
+ if (!filter) {
+ if (wasDraining) {
+ // needs drain
+ drain();
+ }
downstream.write(array, off, len);
return;
}
+
if (len > bulkWriteThreshold) {
// if the content is large enough, we can bulk write everything but the N trail and tail.
// This because the buffer can already contain some byte from a previous single write.
@@ -84,7 +99,7 @@ public void write(byte[] array, int off, int len) throws IOException {
int idx = arrayContains(array, off, len, marker);
if (idx >= 0) {
// we have a full match. just write everything
- found = true;
+ filter = false;
drain();
downstream.write(array, off, idx);
downstream.write(contentToInject);
@@ -99,7 +114,12 @@ public void write(byte[] array, int off, int len) throws IOException {
write(array[i]);
}
drain();
+ boolean wasFiltering = filter;
+
+ // will be reset if no errors after the following write
+ filter = false;
downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold);
+ filter = wasFiltering;
for (int i = len - marker.length + 1; i < len; i++) {
write(array[i]);
}
@@ -133,16 +153,19 @@ private int arrayContains(byte[] array, int off, int len, byte[] search) {
}
private void drain() throws IOException {
- if (bufferFilled) {
- for (int i = 0; i < lookbehind.length; i++) {
- downstream.write(lookbehind[(pos + i) % lookbehind.length]);
+ if (count > 0) {
+ boolean wasFiltering = filter;
+ filter = false;
+ wasDraining = true;
+ int start = (pos - count + lookbehind.length) % lookbehind.length;
+ int cnt = count;
+ for (int i = 0; i < cnt; i++) {
+ downstream.write(lookbehind[(start + i) % lookbehind.length]);
+ count--;
}
- } else {
- downstream.write(this.lookbehind, 0, pos);
+ filter = wasFiltering;
+ wasDraining = false;
}
- pos = 0;
- matchingPos = 0;
- bufferFilled = false;
}
@Override
@@ -152,9 +175,12 @@ public void flush() throws IOException {
@Override
public void close() throws IOException {
- if (!found) {
- drain();
+ try {
+ if (filter || wasDraining) {
+ drain();
+ }
+ } finally {
+ downstream.close();
}
- downstream.close();
}
}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java
index f012c04cae4..d7128e9b385 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java
@@ -5,16 +5,19 @@
/**
* A Writer containing a circular buffer with a lookbehind buffer of n bytes. The first time that
- * the latest n bytes matches the marker, a content is injected before.
+ * the latest n bytes matches the marker, a content is injected before. In case of IOException
+ * thrown by the downstream, the buffer will be lost unless the error occurred when draining it. In
+ * this case the draining will be resumed.
*/
public class InjectingPipeWriter extends Writer {
private final char[] lookbehind;
private int pos;
- private boolean bufferFilled;
+ private int count;
private final char[] marker;
private final char[] contentToInject;
- private boolean found = false;
- private int matchingPos = 0;
+ private boolean filter;
+ private boolean wasDraining;
+ private int matchingPos;
private final Runnable onContentInjected;
private final int bulkWriteThreshold;
private final Writer downstream;
@@ -34,6 +37,11 @@ public InjectingPipeWriter(
this.marker = marker;
this.lookbehind = new char[marker.length];
this.pos = 0;
+ this.count = 0;
+ this.matchingPos = 0;
+ this.wasDraining = false;
+ // should filter the stream to potentially inject into it.
+ this.filter = true;
this.contentToInject = contentToInject;
this.onContentInjected = onContentInjected;
this.bulkWriteThreshold = marker.length * 2 - 2;
@@ -41,25 +49,27 @@ public InjectingPipeWriter(
@Override
public void write(int c) throws IOException {
- if (found) {
+ if (!filter) {
+ if (wasDraining) {
+ // continue draining
+ drain();
+ }
downstream.write(c);
return;
}
- if (bufferFilled) {
+ if (count == lookbehind.length) {
downstream.write(lookbehind[pos]);
+ } else {
+ count++;
}
lookbehind[pos] = (char) c;
pos = (pos + 1) % lookbehind.length;
- if (!bufferFilled) {
- bufferFilled = pos == 0;
- }
-
if (marker[matchingPos++] == c) {
if (matchingPos == marker.length) {
- found = true;
+ filter = false;
downstream.write(contentToInject);
if (onContentInjected != null) {
onContentInjected.run();
@@ -71,17 +81,17 @@ public void write(int c) throws IOException {
}
}
- @Override
- public void flush() throws IOException {
- downstream.flush();
- }
-
@Override
public void write(char[] array, int off, int len) throws IOException {
- if (found) {
+ if (!filter) {
+ if (wasDraining) {
+ // needs drain
+ drain();
+ }
downstream.write(array, off, len);
return;
}
+
if (len > bulkWriteThreshold) {
// if the content is large enough, we can bulk write everything but the N trail and tail.
// This because the buffer can already contain some byte from a previous single write.
@@ -89,7 +99,7 @@ public void write(char[] array, int off, int len) throws IOException {
int idx = arrayContains(array, off, len, marker);
if (idx >= 0) {
// we have a full match. just write everything
- found = true;
+ filter = false;
drain();
downstream.write(array, off, idx);
downstream.write(contentToInject);
@@ -104,7 +114,13 @@ public void write(char[] array, int off, int len) throws IOException {
write(array[i]);
}
drain();
+ boolean wasFiltering = filter;
+
+ // will be reset if no errors after the following write
+ filter = false;
downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold);
+ filter = wasFiltering;
+
for (int i = len - marker.length + 1; i < len; i++) {
write(array[i]);
}
@@ -138,23 +154,34 @@ private int arrayContains(char[] array, int off, int len, char[] search) {
}
private void drain() throws IOException {
- if (bufferFilled) {
- for (int i = 0; i < lookbehind.length; i++) {
- downstream.write(lookbehind[(pos + i) % lookbehind.length]);
+ if (count > 0) {
+ boolean wasFiltering = filter;
+ filter = false;
+ wasDraining = true;
+ int start = (pos - count + lookbehind.length) % lookbehind.length;
+ int cnt = count;
+ for (int i = 0; i < cnt; i++) {
+ downstream.write(lookbehind[(start + i) % lookbehind.length]);
+ count--;
}
- } else {
- downstream.write(this.lookbehind, 0, pos);
+ filter = wasFiltering;
+ wasDraining = false;
}
- pos = 0;
- matchingPos = 0;
- bufferFilled = false;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ downstream.flush();
}
@Override
public void close() throws IOException {
- if (!found) {
- drain();
+ try {
+ if (filter || wasDraining) {
+ drain();
+ }
+ } finally {
+ downstream.close();
}
- downstream.close();
}
}
diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy
index 457b26577ba..9b04234ad3d 100644
--- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy
+++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy
@@ -3,6 +3,36 @@ package datadog.trace.bootstrap.instrumentation.buffer
import datadog.trace.test.util.DDSpecification
class InjectingPipeOutputStreamTest extends DDSpecification {
+ static class GlitchedOutputStream extends FilterOutputStream {
+ int glitchesPos
+ int count
+ final OutputStream out
+
+ GlitchedOutputStream(OutputStream out, int glitchesPos) {
+ super(out)
+ this.out = out
+ this.glitchesPos = glitchesPos
+ }
+
+ @Override
+ void write(byte[] b, int off, int len) throws IOException {
+ count += len
+ if (count >= glitchesPos) {
+ glitchesPos = Integer.MAX_VALUE
+ throw new IOException("Glitched after $count bytes")
+ }
+ out.write(b, off, len)
+ }
+
+ @Override
+ void write(int b) throws IOException {
+ if (++count == glitchesPos) {
+ throw new IOException("Glitched after $glitchesPos bytes")
+ }
+ out.write(b)
+ }
+ }
+
def 'should filter a buffer and inject if found #found'() {
setup:
def downstream = new ByteArrayOutputStream()
@@ -20,4 +50,41 @@ class InjectingPipeOutputStreamTest extends DDSpecification {
"" | "" | "" | false | ""
"" | "" | "" | false | ""
}
+
+ def 'should be resilient to exceptions when writing #body'() {
+ setup:
+ def baos = new ByteArrayOutputStream()
+ def downstream = new GlitchedOutputStream(baos, glichesAt)
+ def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null)
+ when:
+ try {
+ for (String line : body) {
+ final bytes = line.getBytes("UTF-8")
+ try {
+ piped.write(bytes)
+ } catch (IOException ioe) {
+ ioe.printStackTrace()
+ piped.write(bytes)
+ }
+ }
+ } finally {
+ // it can throw when draining at close
+ try {
+ piped.close()
+ } catch (IOException ignored) {
+ }
+ }
+ then:
+ assert baos.toByteArray() == expected.getBytes("UTF-8")
+ where:
+ body | marker | contentToInject | glichesAt | expected
+ // write fails after the content has been injected
+ ["", "", "", "", "", ""] | "" | "" | 60 | ""
+ // write fails before the content has been injected
+ ["", "", "", "", "", ""] | "" | "" | 20 | ""
+ // write fails after having filled the buffer. The last line is written twice
+ ["", "", ""] | "" | "" | 10 | "