Repository: scala-steward-org/scala-steward
Pull request #2940 (2023-01-22 16:14:46)

Throw if slurp reads more lines than `max-buffer-size`
What has been done?

This changes `slurp` such that it throws an `ProcessBufferOverflowException` if a process outputs more lines than the max buffer size of the internal lines buffer. Throwing that exception can be prevented via the new `SlurpOption.IgnoreBufferOverflow`. `slurp` then behaves as before this change.

Some `exec` calls now use `SlurpOption.IgnoreBufferOverflow` in cases where we ignore the output of a process or a partial output of a process is sufficient.

Why?

This allows us to decide in `ProcessAlg#exec` calls if we need the complete output of a process or if it is okay to only get a fraction of it (for example, if we completely ignore the output). In some situations it is important that we receive the complete output and this allows us to fail early instead of getting hard to debug errors later that are a consequence of the incomplete output. The message of `ProcessBufferOverflowException` tells the user what went wrong and how the max buffer size can be increased.

History and design constraints:

- `ProcessAlg#exec` returns a list of outputted lines, so that other parts of Scala Steward can easily work with the output of external processes.
- External processes can run indefinitely and output an indefinite number of bytes. This is why timeouts (#776) and a bounded lines buffer (#1123) have been added to `slurp`.
- The output of processes is valuable for reporting errors, even if that output is not used by the rest of the program.

Alternative to: #2717
Closes: #2718

codecov[bot]: # [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) Report
Base: **90.65**% // Head: **90.70**% // Increases project coverage by **`+0.05%`** :tada:
> Coverage data is based on head [(`5b9dae5`)](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) compared to base [(`f515759`)](https://codecov.io/gh/scala-steward-org/scala-steward/commit/f5157594dfd29b5b5993dc61ad94f661da912ce8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).
> Patch coverage: 98.86% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2940      +/-   ##
==========================================
+ Coverage   90.65%   90.70%   +0.05%     
==========================================
  Files         155      155              
  Lines        3070     3089      +19     
  Branches      216      234      +18     
==========================================
+ Hits         2783     2802      +19     
  Misses        287      287              
```


| [Impacted Files](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) | Coverage Δ | |
|---|---|---|
| [...n/scala/org/scalasteward/core/git/FileGitAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9naXQvRmlsZUdpdEFsZy5zY2FsYQ==) | `98.59% <96.77%> (+0.02%)` | :arrow_up: |
| [.../scala/org/scalasteward/core/application/Cli.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9DbGkuc2NhbGE=) | `100.00% <100.00%> (ø)` | |
| [...a/org/scalasteward/core/buildtool/sbt/SbtAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9idWlsZHRvb2wvc2J0L1NidEFsZy5zY2FsYQ==) | `98.30% <100.00%> (+0.05%)` | :arrow_up: |
| [...asteward/core/buildtool/scalacli/ScalaCliAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9idWlsZHRvb2wvc2NhbGFjbGkvU2NhbGFDbGlBbGcuc2NhbGE=) | `100.00% <100.00%> (ø)` | |
| [...rg/scalasteward/core/edit/hooks/HookExecutor.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L2hvb2tzL0hvb2tFeGVjdXRvci5zY2FsYQ==) | `94.52% <100.00%> (+0.15%)` | :arrow_up: |
| [.../scalasteward/core/edit/scalafix/ScalafixCli.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L3NjYWxhZml4L1NjYWxhZml4Q2xpLnNjYWxh) | `100.00% <100.00%> (ø)` | |
| [...in/scala/org/scalasteward/core/io/ProcessAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9pby9Qcm9jZXNzQWxnLnNjYWxh) | `100.00% <100.00%> (ø)` | |
| [.../main/scala/org/scalasteward/core/io/process.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9pby9wcm9jZXNzLnNjYWxh) | `100.00% <100.00%> (ø)` | |
| [...a/org/scalasteward/core/scalafmt/ScalafmtAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9zY2FsYWZtdC9TY2FsYWZtdEFsZy5zY2FsYQ==) | `95.23% <100.00%> (+0.50%)` | :arrow_up: |
| [...ain/scala/org/scalasteward/core/util/package.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS91dGlsL3BhY2thZ2Uuc2NhbGE=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2940?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).

fthomas: /cc @IdiosApps @missingfaktor

Patch:
diff --git a/docs/help.md b/docs/help.md
index a246c9bf..543f60de 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -57,7 +57,7 @@ Options and flags:
     --disable-sandbox
         Whether to not use the sandbox
     --max-buffer-size <integer>
-        Size of the buffer for the output of an external process in lines; default: 8192
+        Size of the buffer for the output of an external process in lines; default: 16384
     --repo-config <uri>
         Additional repo config file (can be used multiple times)
     --disable-default-repo-config
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
index 859e2640..b11e43c4 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
@@ -26,7 +26,7 @@ import org.http4s.syntax.literals._
 import org.scalasteward.core.application.Config._
 import org.scalasteward.core.data.Resolver
 import org.scalasteward.core.forge.ForgeType
-import org.scalasteward.core.forge.ForgeType.GitHub
+import org.scalasteward.core.forge.ForgeType.{AzureRepos, GitHub}
 import org.scalasteward.core.forge.github.GitHubApp
 import org.scalasteward.core.git.Author
 import org.scalasteward.core.util.Nel
@@ -36,6 +36,14 @@ import scala.concurrent.duration._
 object Cli {
   final case class EnvVar(name: String, value: String)
 
+  object name {
+    val forgeApiHost = "forge-api-host"
+    val forgeLogin = "forge-login"
+    val forgeType = "forge-type"
+    val maxBufferSize = "max-buffer-size"
+    val processTimeout = "process-timeout"
+  }
+
   implicit val envVarArgument: Argument[EnvVar] =
     Argument.from("name=value") { s =>
       s.trim.split('=').toList match {
@@ -58,7 +66,7 @@ object Cli {
     }
 
   implicit val forgeTypeArgument: Argument[ForgeType] =
-    Argument.from("forge-type") { s =>
+    Argument.from(name.forgeType) { s =>
       Validated.fromEither(ForgeType.parse(s)).toValidatedNel
     }
 
@@ -97,37 +105,37 @@ object Cli {
   private val vcsType =
     option[ForgeType](
       "vcs-type",
-      "deprecated in favor of --forge-type",
+      s"deprecated in favor of --${name.forgeType}",
       visibility = Visibility.Partial
     )
 
   private val forgeType = {
     val help = ForgeType.all.map(_.asString).mkString("One of ", ", ", "") +
       s"; default: ${GitHub.asString}"
-    option[ForgeType]("forge-type", help).orElse(vcsType).withDefault(GitHub)
+    option[ForgeType](name.forgeType, help).orElse(vcsType).withDefault(GitHub)
   }
 
   private val vcsApiHost =
     option[Uri](
       "vcs-api-host",
-      "deprecated in favor of --forge-api-host",
+      s"deprecated in favor of --${name.forgeApiHost}",
       visibility = Visibility.Partial
     )
 
   private val forgeApiHost: Opts[Uri] =
-    option[Uri]("forge-api-host", s"API URL of the forge; default: ${GitHub.publicApiBaseUrl}")
+    option[Uri](name.forgeApiHost, s"API URL of the forge; default: ${GitHub.publicApiBaseUrl}")
       .orElse(vcsApiHost)
       .withDefault(GitHub.publicApiBaseUrl)
 
   private val vcsLogin =
     option[String](
       "vcs-login",
-      "deprecated in favor of --forge-login",
+      s"deprecated in favor of --${name.forgeLogin}",
       visibility = Visibility.Partial
     )
 
   private val forgeLogin: Opts[String] =
-    option[String]("forge-login", "The user name for the forge").orElse(vcsLogin)
+    option[String](name.forgeLogin, "The user name for the forge").orElse(vcsLogin)
 
   private val doNotFork: Opts[Boolean] =
     flag("do-not-fork", "Whether to not push the update branches to a fork; default: false").orFalse
@@ -163,7 +171,7 @@ object Cli {
     val default = 10.minutes
     val help =
       s"Timeout for external process invocations; default: ${renderFiniteDuration(default)}"
-    option[FiniteDuration]("process-timeout", help).withDefault(default)
+    option[FiniteDuration](name.processTimeout, help).withDefault(default)
   }
 
   private val whitelist: Opts[List[String]] =
@@ -188,10 +196,10 @@ object Cli {
     (whitelist, readOnly, enableSandbox).mapN(SandboxCfg.apply)
 
   private val maxBufferSize: Opts[Int] = {
-    val default = 8192
+    val default = 16384
     val help =
       s"Size of the buffer for the output of an external process in lines; default: $default"
-    option[Int]("max-buffer-size", help).withDefault(default)
+    option[Int](name.maxBufferSize, help).withDefault(default)
   }
 
   private val processCfg: Opts[ProcessCfg] =
@@ -287,7 +295,7 @@ object Cli {
   private val azureReposOrganization: Opts[Option[String]] =
     option[String](
       "azure-repos-organization",
-      "The Azure organization (required when --forge-type is azure-repos)"
+      s"The Azure organization (required when --${name.forgeType} is ${AzureRepos.asString})"
     ).orNone
 
   private val azureReposCfg: Opts[AzureReposCfg] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/sbt/SbtAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/sbt/SbtAlg.scala
index 7672ead8..c36e25b7 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/buildtool/sbt/SbtAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/sbt/SbtAlg.scala
@@ -26,6 +26,7 @@ import org.scalasteward.core.buildtool.{BuildRoot, BuildToolAlg}
 import org.scalasteward.core.coursier.VersionsCache
 import org.scalasteward.core.data.{Dependency, Scope, Version}
 import org.scalasteward.core.edit.scalafix.{ScalafixCli, ScalafixMigration}
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{FileAlg, FileData, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.util.Nel
 
@@ -100,8 +101,11 @@ final class SbtAlg[F[_]](config: Config)(implicit
             val options = scalaStewardScalafixOptions(opts.toList)
             fileAlg.createTemporarily(buildRootDir, options)
           }
-          val scalafixCmds = migration.rewriteRules.map(rule => s"$scalafixAll $rule").toList
-          withScalacOptions.surround(sbt(Nel(scalafixEnable, scalafixCmds), buildRootDir).void)
+          withScalacOptions.surround {
+            val scalafixCmds = migration.rewriteRules.map(rule => s"$scalafixAll $rule").toList
+            val slurpOptions = SlurpOptions.ignoreBufferOverflow
+            sbt(Nel(scalafixEnable, scalafixCmds), buildRootDir, slurpOptions).void
+          }
         }
       }
     }
@@ -124,7 +128,11 @@ final class SbtAlg[F[_]](config: Config)(implicit
       }
     } yield ()
 
-  private def sbt(sbtCommands: Nel[String], repoDir: File): F[List[String]] =
+  private def sbt(
+      sbtCommands: Nel[String],
+      repoDir: File,
+      slurpOptions: SlurpOptions = Set.empty
+  ): F[List[String]] =
     maybeIgnoreOptsFiles(repoDir).surround {
       val command =
         Nel.of(
@@ -134,7 +142,7 @@ final class SbtAlg[F[_]](config: Config)(implicit
           "-Dsbt.supershell=false",
           sbtCommands.mkString_(";", ";", "")
         )
-      processAlg.execSandboxed(command, repoDir)
+      processAlg.execSandboxed(command, repoDir, slurpOptions = slurpOptions)
     }
 
   private def maybeIgnoreOptsFiles(dir: File): Resource[F, Unit] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/scalacli/ScalaCliAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/scalacli/ScalaCliAlg.scala
index 6b55e40c..63e9370a 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/buildtool/scalacli/ScalaCliAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/scalacli/ScalaCliAlg.scala
@@ -23,6 +23,7 @@ import org.scalasteward.core.buildtool.{BuildRoot, BuildToolAlg}
 import org.scalasteward.core.data.Scope
 import org.scalasteward.core.edit.scalafix.ScalafixMigration
 import org.scalasteward.core.git.GitAlg
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.util.Nel
 import org.typelevel.log4cats.Logger
@@ -51,7 +52,8 @@ final class ScalaCliAlg[F[_]](implicit
       exportDir = "tmp-sbt-build-for-scala-steward"
       exportCmd =
         Nel.of("scala-cli", "export", "--sbt", "--output", exportDir, buildRootDir.pathAsString)
-      _ <- processAlg.execSandboxed(exportCmd, buildRootDir)
+      slurpOptions = SlurpOptions.ignoreBufferOverflow
+      _ <- processAlg.execSandboxed(exportCmd, buildRootDir, slurpOptions = slurpOptions)
       exportBuildRoot = buildRoot.copy(relativePath = buildRoot.relativePath + s"/$exportDir")
       dependencies <- sbtAlg.getDependencies(exportBuildRoot)
       _ <- fileAlg.deleteForce(buildRootDir / exportDir)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/hooks/HookExecutor.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/hooks/HookExecutor.scala
index 10a589a9..e1fdfcc8 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/hooks/HookExecutor.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/hooks/HookExecutor.scala
@@ -29,6 +29,7 @@ import org.scalasteward.core.data._
 import org.scalasteward.core.edit.EditAttempt
 import org.scalasteward.core.edit.EditAttempt.HookEdit
 import org.scalasteward.core.git.{gitBlameIgnoreRevsName, Commit, CommitMsg, GitAlg}
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.repocache.RepoCache
 import org.scalasteward.core.scalafmt.{scalafmtArtifactId, scalafmtGroupId, ScalafmtAlg}
@@ -66,7 +67,10 @@ final class HookExecutor[F[_]](implicit
       )
       repoDir <- workspaceAlg.repoDir(repo)
       result <- logger.attemptWarn.log("Post-update hook failed") {
-        processAlg.execMaybeSandboxed(hook.useSandbox)(hook.command, repoDir)
+        val slurpOptions = SlurpOptions.ignoreBufferOverflow
+        processAlg
+          .execMaybeSandboxed(hook.useSandbox)(hook.command, repoDir, slurpOptions = slurpOptions)
+          .void
       }
       commitMessage = hook
         .commitMessage(update)
@@ -74,7 +78,7 @@ final class HookExecutor[F[_]](implicit
       maybeHookCommit <- gitAlg.commitAllIfDirty(repo, commitMessage)
       maybeBlameIgnoreCommit <-
         maybeHookCommit.flatTraverse(addToGitBlameIgnoreRevs(repo, repoDir, hook, _, commitMessage))
-    } yield HookEdit(hook, result.void, maybeHookCommit.toList ++ maybeBlameIgnoreCommit.toList)
+    } yield HookEdit(hook, result, maybeHookCommit.toList ++ maybeBlameIgnoreCommit.toList)
 
   private def addToGitBlameIgnoreRevs(
       repo: Repo,
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixCli.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixCli.scala
index 06ab37fa..8a18e03f 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixCli.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixCli.scala
@@ -20,6 +20,7 @@ import better.files.File
 import cats.Monad
 import cats.syntax.all._
 import org.scalasteward.core.edit.scalafix.ScalafixCli.scalafixBinary
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.util.Nel
 
@@ -31,7 +32,7 @@ final class ScalafixCli[F[_]](implicit
   def runMigration(workingDir: File, files: Nel[File], migration: ScalafixMigration): F[Unit] = {
     val rules = migration.rewriteRules.map("--rules=" + _)
     val cmd = scalafixBinary :: rules ::: files.map(_.pathAsString)
-    processAlg.exec(cmd, workingDir).void
+    processAlg.exec(cmd, workingDir, slurpOptions = SlurpOptions.ignoreBufferOverflow).void
   }
 
   def version: F[String] = {
diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
index 9ed09e28..e2d8f502 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala
@@ -22,6 +22,7 @@ import cats.syntax.all._
 import org.http4s.Uri
 import org.scalasteward.core.application.Config.GitCfg
 import org.scalasteward.core.git.FileGitAlg.{dotdot, gitCmd}
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.util.Nel
 
@@ -32,24 +33,24 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
     F: MonadCancelThrow[F]
 ) extends GenGitAlg[F, File] {
   override def add(repo: File, file: String): F[Unit] =
-    git("add", file)(repo).void
+    git_("add", file)(repo).void
 
   override def branchAuthors(repo: File, branch: Branch, base: Branch): F[List[String]] =
     git("log", "--pretty=format:'%an'", dotdot(base, branch))(repo).map(_.distinct)
 
   override def branchExists(repo: File, branch: Branch): F[Boolean] =
-    git("branch", "--list", "--no-color", "--all", branch.name)(repo).map(_.mkString.trim.nonEmpty)
+    git_("branch", "--list", "--no-color", "--all", branch.name)(repo).map(_.mkString.trim.nonEmpty)
 
   override def branchesDiffer(repo: File, b1: Branch, b2: Branch): F[Boolean] =
-    git("diff", "--name-only", b1.name, b2.name, "--")(repo).map(_.nonEmpty)
+    git_("diff", "--name-only", b1.name, b2.name, "--")(repo).map(_.nonEmpty)
 
   override def checkoutBranch(repo: File, branch: Branch): F[Unit] =
-    git("checkout", branch.name)(repo).void
+    git_("checkout", branch.name)(repo).void
 
   override def clone(repo: File, url: Uri): F[Unit] =
     for {
       rootDir <- workspaceAlg.rootDir
-      _ <- git("clone", url.toString, repo.pathAsString)(rootDir)
+      _ <- git_("clone", url.toString, repo.pathAsString)(rootDir)
     } yield ()
 
   override def cloneExists(repo: File): F[Boolean] =
@@ -57,29 +58,29 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
 
   override def commitAll(repo: File, message: CommitMsg): F[Commit] = {
     val messages = message.toNel.foldMap(m => List("-m", m))
-    git("commit" :: "--all" :: sign :: messages: _*)(repo) >>
+    git_("commit" :: "--all" :: sign :: messages: _*)(repo) >>
       latestSha1(repo, Branch.head).map(Commit.apply)
   }
 
   override def containsChanges(repo: File): F[Boolean] =
-    git("status", "--porcelain", "--untracked-files=no", "--ignore-submodules")(repo)
+    git_("status", "--porcelain", "--untracked-files=no", "--ignore-submodules")(repo)
       .map(_.nonEmpty)
 
   override def createBranch(repo: File, branch: Branch): F[Unit] =
-    git("checkout", "-b", branch.name)(repo).void
+    git_("checkout", "-b", branch.name)(repo).void
 
   override def currentBranch(repo: File): F[Branch] =
     git("rev-parse", "--abbrev-ref", Branch.head.name)(repo)
       .map(lines => Branch(lines.mkString.trim))
 
   override def deleteLocalBranch(repo: File, branch: Branch): F[Unit] =
-    git("branch", "--delete", "--force", branch.name)(repo).void
+    git_("branch", "--delete", "--force", branch.name)(repo).void
 
   override def deleteRemoteBranch(repo: File, branch: Branch): F[Unit] =
-    git("push", "origin", "--delete", branch.name)(repo).void
+    git_("push", "origin", "--delete", branch.name)(repo).void
 
   override def discardChanges(repo: File): F[Unit] =
-    git("checkout", "--", ".")(repo).void
+    git_("checkout", "--", ".")(repo).void
 
   override def findFilesContaining(repo: File, string: String): F[List[String]] =
     git("grep", "-I", "--fixed-strings", "--files-with-matches", string)(repo)
@@ -87,8 +88,8 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
       .map(_.filter(_.nonEmpty))
 
   override def hasConflicts(repo: File, branch: Branch, base: Branch): F[Boolean] = {
-    val tryMerge = git("merge", "--no-commit", "--no-ff", branch.name)(repo)
-    val abortMerge = git("merge", "--abort")(repo).void
+    val tryMerge = git_("merge", "--no-commit", "--no-ff", branch.name)(repo)
+    val abortMerge = git_("merge", "--abort")(repo).void
 
     returnToCurrentBranch(repo) {
       checkoutBranch(repo, base) >> F.guarantee(tryMerge, abortMerge).attempt.map(_.isLeft)
@@ -96,10 +97,10 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
   }
 
   override def initSubmodules(repo: File): F[Unit] =
-    git("submodule", "update", "--init", "--recursive")(repo).void
+    git_("submodule", "update", "--init", "--recursive")(repo).void
 
   override def isMerged(repo: File, branch: Branch, base: Branch): F[Boolean] =
-    git("log", "--pretty=format:%h", dotdot(base, branch))(repo).map(_.isEmpty)
+    git_("log", "--pretty=format:%h", dotdot(base, branch))(repo).map(_.isEmpty)
 
   override def latestSha1(repo: File, branch: Branch): F[Sha1] =
     git("rev-parse", "--verify", branch.name)(repo)
@@ -108,22 +109,22 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
   override def mergeTheirs(repo: File, branch: Branch): F[Option[Commit]] =
     for {
       before <- latestSha1(repo, Branch.head)
-      _ <- git("merge", "--strategy-option=theirs", sign, branch.name)(repo).void
+      _ <- git_("merge", "--strategy-option=theirs", sign, branch.name)(repo).void
         .handleErrorWith { throwable =>
           // Resolve CONFLICT (modify/delete) by deleting unmerged files:
           for {
             unmergedFiles <- git("diff", "--name-only", "--diff-filter=U")(repo)
             _ <- Nel
               .fromList(unmergedFiles.filter(_.nonEmpty))
-              .fold(F.raiseError[Unit](throwable))(_.traverse_(file => git("rm", file)(repo)))
-            _ <- git("commit", "--all", "--no-edit", sign)(repo)
+              .fold(F.raiseError[Unit](throwable))(_.traverse_(file => git_("rm", file)(repo)))
+            _ <- git_("commit", "--all", "--no-edit", sign)(repo)
           } yield ()
         }
       after <- latestSha1(repo, Branch.head)
     } yield Option.when(before =!= after)(Commit(after))
 
   override def push(repo: File, branch: Branch): F[Unit] =
-    git("push", "--force", "--set-upstream", "origin", branch.name)(repo).void
+    git_("push", "--force", "--set-upstream", "origin", branch.name)(repo).void
 
   override def removeClone(repo: File): F[Unit] =
     fileAlg.deleteForce(repo)
@@ -136,16 +137,16 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
       if (commits.isEmpty) F.pure(None)
       else {
         val msg = CommitMsg(s"Revert commit(s) " + commits.mkString(", "))
-        git("revert" :: "--no-commit" :: commits: _*)(repo) >> commitAllIfDirty(repo, msg)
+        git_("revert" :: "--no-commit" :: commits: _*)(repo) >> commitAllIfDirty(repo, msg)
       }
     }
   }
 
   override def setAuthor(repo: File, author: Author): F[Unit] =
     for {
-      _ <- git("config", "user.email", author.email)(repo)
-      _ <- git("config", "user.name", author.name)(repo)
-      _ <- author.signingKey.traverse_(key => git("config", "user.signingKey", key)(repo))
+      _ <- git_("config", "user.email", author.email)(repo)
+      _ <- git_("config", "user.name", author.name)(repo)
+      _ <- author.signingKey.traverse_(key => git_("config", "user.signingKey", key)(repo))
     } yield ()
 
   override def syncFork(repo: File, upstreamUrl: Uri, defaultBranch: Branch): F[Unit] =
@@ -154,21 +155,27 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit
       remote = "upstream"
       branch = defaultBranch.name
       remoteBranch = s"$remote/$branch"
-      _ <- git("remote", "add", remote, upstreamUrl.toString)(repo)
-      _ <- git("fetch", "--force", "--tags", remote, branch)(repo)
-      _ <- git("checkout", "-B", branch, "--track", remoteBranch)(repo)
-      _ <- git("merge", remoteBranch)(repo)
+      _ <- git_("remote", "add", remote, upstreamUrl.toString)(repo)
+      _ <- git_("fetch", "--force", "--tags", remote, branch)(repo)
+      _ <- git_("checkout", "-B", branch, "--track", remoteBranch)(repo)
+      _ <- git_("merge", remoteBranch)(repo)
       _ <- push(repo, defaultBranch)
     } yield ()
 
   override def version: F[String] =
-    workspaceAlg.rootDir.flatMap(git("--version")).map(_.mkString.trim)
-
-  private def git(args: String*)(repo: File): F[List[String]] = {
-    val extraEnv = "GIT_ASKPASS" -> config.gitAskPass.pathAsString
-    processAlg.exec(gitCmd ++ args.toList, repo, extraEnv)
+    workspaceAlg.rootDir.flatMap(git("--version")(_)).map(_.mkString.trim)
+
+  private def git(args: String*)(
+      repo: File,
+      slurpOptions: SlurpOptions = Set.empty
+  ): F[List[String]] = {
+    val extraEnv = List("GIT_ASKPASS" -> config.gitAskPass.pathAsString)
+    processAlg.exec(gitCmd ++ args.toList, repo, extraEnv, slurpOptions)
   }
 
+  private def git_(args: String*)(repo: File): F[List[String]] =
+    git(args: _*)(repo, SlurpOptions.ignoreBufferOverflow)
+
   private val sign: String =
     if (config.signCommits) "--gpg-sign" else "--no-gpg-sign"
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/io/ProcessAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/io/ProcessAlg.scala
index 8433ddcc..5fdd7606 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/io/ProcessAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/io/ProcessAlg.scala
@@ -20,7 +20,7 @@ import better.files.File
 import cats.effect.Async
 import cats.syntax.all._
 import org.scalasteward.core.application.Config.ProcessCfg
-import org.scalasteward.core.io.process.Args
+import org.scalasteward.core.io.process.{Args, SlurpOption, SlurpOptions}
 import org.scalasteward.core.util.Nel
 import org.typelevel.log4cats.Logger
 
@@ -28,23 +28,26 @@ trait ProcessAlg[F[_]] {
   def exec(
       command: Nel[String],
       workingDirectory: File,
-      extraEnv: (String, String)*
+      extraEnv: List[(String, String)] = Nil,
+      slurpOptions: SlurpOptions = Set.empty
   ): F[List[String]]
 
   def execSandboxed(
       command: Nel[String],
       workingDirectory: File,
-      extraEnv: (String, String)*
+      extraEnv: List[(String, String)] = Nil,
+      slurpOptions: SlurpOptions = Set.empty
   ): F[List[String]] =
-    exec(command, workingDirectory, extraEnv: _*)
+    exec(command, workingDirectory, extraEnv, slurpOptions)
 
   final def execMaybeSandboxed(sandboxed: Boolean)(
       command: Nel[String],
       workingDirectory: File,
-      extraEnv: (String, String)*
+      extraEnv: List[(String, String)] = Nil,
+      slurpOptions: SlurpOptions = Set.empty
   ): F[List[String]] =
-    if (sandboxed) execSandboxed(command, workingDirectory, extraEnv: _*)
-    else exec(command, workingDirectory, extraEnv: _*)
+    if (sandboxed) execSandboxed(command, workingDirectory, extraEnv, slurpOptions)
+    else exec(command, workingDirectory, extraEnv, slurpOptions)
 }
 
 object ProcessAlg {
@@ -55,9 +58,10 @@ object ProcessAlg {
     override def exec(
         command: Nel[String],
         workingDirectory: File,
-        extraEnv: (String, String)*
+        extraEnv: List[(String, String)],
+        slurpOptions: SlurpOptions
     ): F[List[String]] =
-      execImpl(Args(command, Some(workingDirectory), extraEnv.toList ++ configEnv))
+      execImpl(Args(command, Some(workingDirectory), extraEnv ++ configEnv, slurpOptions))
   }
 
   private class WithFirejail[F[_]](config: ProcessCfg)(execImpl: Args => F[List[String]])
@@ -65,7 +69,8 @@ object ProcessAlg {
     override def execSandboxed(
         command: Nel[String],
         workingDirectory: File,
-        extraEnv: (String, String)*
+        extraEnv: List[(String, String)],
+        slurpOptions: SlurpOptions
     ): F[List[String]] = {
       val whitelisted = (workingDirectory.toString :: config.sandboxCfg.whitelistedDirectories)
         .map(dir => s"--whitelist=$dir")
@@ -74,7 +79,12 @@ object ProcessAlg {
       val envVars = (extraEnv ++ configEnv)
         .map { case (k, v) => s"--env=$k=$v" }
       val firejail = Nel("firejail", "--quiet" :: whitelisted ++ readOnly ++ envVars) ::: command
-      execImpl(Args(firejail, Some(workingDirectory), clearEnv = true))
+      val args = Args(
+        command = firejail,
+        workingDirectory = Some(workingDirectory),
+        slurpOptions = slurpOptions ++ Set(SlurpOption.ClearEnvironment)
+      )
+      execImpl(args)
     }
   }
 
@@ -90,6 +100,6 @@ object ProcessAlg {
   ): ProcessAlg[F] =
     fromExecImpl(config) { args =>
       logger.debug(s"Execute ${process.showCmd(args)}") >>
-        process.slurp[F](args, config.processTimeout, config.maxBufferSize, logger.trace(_))
+        process.slurp(args, config.processTimeout, config.maxBufferSize, logger.trace(_))
     }
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/io/process.scala b/modules/core/src/main/scala/org/scalasteward/core/io/process.scala
index 718028f3..2c9b8c10 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/io/process.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/io/process.scala
@@ -21,6 +21,7 @@ import cats.effect._
 import cats.syntax.all._
 import fs2.Stream
 import java.io.{IOException, InputStream}
+import org.scalasteward.core.application.Cli
 import org.scalasteward.core.util._
 import scala.collection.mutable.ListBuffer
 import scala.concurrent.TimeoutException
@@ -31,9 +32,27 @@ object process {
       command: Nel[String],
       workingDirectory: Option[File] = None,
       extraEnv: List[(String, String)] = Nil,
-      clearEnv: Boolean = false
+      slurpOptions: SlurpOptions = Set.empty
   )
 
+  sealed trait SlurpOption extends Product with Serializable
+  object SlurpOption {
+    case object ClearEnvironment extends SlurpOption
+
+    /** [[slurp]] uses an internal buffer with a fixed maximum size for the output of a process. If
+      * a process outputs more lines than the maximum buffer size, [[slurp]] raises an
+      * [[ProcessBufferOverflowException]]. If this option is given, the buffer overflow will be
+      * ignored and no exception is raised. Note that in these cases only the last lines up until
+      * the maximum buffer size are returned.
+      */
+    case object IgnoreBufferOverflow extends SlurpOption
+  }
+
+  type SlurpOptions = Set[SlurpOption]
+  object SlurpOptions {
+    val ignoreBufferOverflow: SlurpOptions = Set(SlurpOption.IgnoreBufferOverflow)
+  }
+
   def slurp[F[_]](
       args: Args,
       timeout: FiniteDuration,
@@ -42,41 +61,44 @@ object process {
   )(implicit F: Async[F]): F[List[String]] =
     createProcess(args).flatMap { process =>
       F.delay(new ListBuffer[String]).flatMap { buffer =>
-        val readOut = readInputStream[F](process.getInputStream)
-          .evalMap(line => F.delay(appendBounded(buffer, line, maxBufferSize)) >> log(line))
-          .compile
-          .drain
-
-        val result = readOut >> F.blocking(process.waitFor()) >>= { exitValue =>
-          if (exitValue === 0) F.pure(buffer.toList)
-          else {
-            val msg = s"'${showCmd(args)}' exited with code $exitValue"
-            F.raiseError[List[String]](new IOException(makeMessage(msg, buffer.toList)))
+        val raiseError = F.raiseError[List[String]] _
+
+        val result =
+          readLinesIntoBuffer(process.getInputStream, buffer, maxBufferSize, log).flatMap {
+            maxSizeExceeded =>
+              F.blocking(process.waitFor()).flatMap { exitValue =>
+                if (maxSizeExceeded && !args.slurpOptions(SlurpOption.IgnoreBufferOverflow))
+                  raiseError(new ProcessBufferOverflowException(args, buffer, maxBufferSize))
+                else if (exitValue === 0)
+                  F.pure(buffer.toList)
+                else
+                  raiseError(new ProcessFailedException(args, buffer, exitValue))
+              }
           }
-        }
 
-        val fallback = F.blocking(process.destroyForcibly()) >> {
-          val msg = s"'${showCmd(args)}' timed out after ${timeout.toString}"
-          F.raiseError[List[String]](new TimeoutException(makeMessage(msg, buffer.toList)))
-        }
+        val onTimeout = F.blocking(process.destroyForcibly()) >>
+          raiseError(new ProcessTimedOutException(args, buffer, timeout))
 
-        F.timeoutAndForget(result, timeout).recoverWith { case _: TimeoutException => fallback }
+        F.timeoutAndForget(result, timeout).recoverWith { case _: TimeoutException => onTimeout }
       }
     }
 
   def showCmd(args: Args): String =
     (args.extraEnv.map { case (k, v) => s"$k=$v" } ++ args.command.toList).mkString_(" ")
 
-  private def createProcess[F[_]](args: Args)(implicit F: Sync[F]): F[Process] =
+  private def createProcessBuilder[F[_]](args: Args)(implicit F: Sync[F]): F[ProcessBuilder] =
     F.blocking {
       val pb = new ProcessBuilder(args.command.toList: _*)
       args.workingDirectory.foreach(file => pb.directory(file.toJava))
       val env = pb.environment()
-      if (args.clearEnv) env.clear()
+      if (args.slurpOptions(SlurpOption.ClearEnvironment)) env.clear()
       args.extraEnv.foreach { case (key, value) => env.put(key, value) }
       pb.redirectErrorStream(true)
       pb
-    }.flatMap { pb =>
+    }
+
+  private def createProcess[F[_]](args: Args)(implicit F: Sync[F]): F[Process] =
+    createProcessBuilder(args).flatMap { pb =>
       F.blocking {
         val p = pb.start()
         // Close standard input so that the process never waits for input.
@@ -85,12 +107,60 @@ object process {
       }
     }
 
-  private def readInputStream[F[_]](is: InputStream)(implicit F: Sync[F]): Stream[F, String] =
+  /** Reads lines from `is` into `buffer` such that `buffer` contains no more than `maxBufferSize`
+    * lines.
+    *
+    * @return
+    *   `true` if more than `maxBufferSize` lines were added to the buffer, `false` otherwise
+    */
+  private def readLinesIntoBuffer[F[_]](
+      is: InputStream,
+      buffer: ListBuffer[String],
+      maxBufferSize: Int,
+      log: String => F[Unit]
+  )(implicit F: Sync[F]): F[Boolean] =
+    readUtf8Lines[F](is, maxBufferSize)
+      .evalMap(line => log(line).as(appendBounded(buffer, line, maxBufferSize)))
+      .compile
+      .fold(false)(_ || _)
+
+  private def readUtf8Lines[F[_]](
+      is: InputStream,
+      maxBufferSize: Int
+  )(implicit F: Sync[F]): Stream[F, String] =
     fs2.io
-      .readInputStream(F.pure(is), chunkSize = 4096)
+      .readInputStream(F.pure(is), chunkSize = 8192)
       .through(fs2.text.utf8.decode)
-      .through(fs2.text.lines)
+      .through(fs2.text.linesLimited(maxBufferSize))
+
+  final class ProcessBufferOverflowException(
+      args: Args,
+      buffer: ListBuffer[String],
+      maxBufferSize: Int
+  ) extends IOException(makeMessage(args, buffer) {
+        s"outputted more than $maxBufferSize lines. " +
+          s"If the process executed normally and the buffer size is just too small, you can " +
+          s"increase it with the --${Cli.name.maxBufferSize} command-line option and/or open " +
+          s"a pull request in ${org.scalasteward.core.BuildInfo.gitHubUrl} that increases the " +
+          s"default buffer size."
+      })
+
+  final class ProcessFailedException(
+      args: Args,
+      buffer: ListBuffer[String],
+      exitValue: Int
+  ) extends IOException(makeMessage(args, buffer)(s"exited with code $exitValue."))
+
+  final class ProcessTimedOutException(
+      args: Args,
+      buffer: ListBuffer[String],
+      timeout: FiniteDuration
+  ) extends TimeoutException(makeMessage(args, buffer) {
+        s"timed out after ${timeout.toString}. " +
+          s"If the process executed normally but just takes a long time, you can increase the " +
+          s"timeout with the --${Cli.name.processTimeout} command-line option."
+      })
 
-  private def makeMessage(prefix: String, output: List[String]): String =
-    (prefix :: output).mkString("\n")
+  private def makeMessage(args: Args, buffer: ListBuffer[String])(description: String): String =
+    buffer.prepend(s"'${showCmd(args)}' $description").mkString("\n")
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/scalafmt/ScalafmtAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/scalafmt/ScalafmtAlg.scala
index 096b6a1f..3f724c07 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/scalafmt/ScalafmtAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/scalafmt/ScalafmtAlg.scala
@@ -23,6 +23,7 @@ import io.circe.ParsingFailure
 import org.scalasteward.core.application.Config
 import org.scalasteward.core.buildtool.BuildRoot
 import org.scalasteward.core.data.{Repo, Scope, Version}
+import org.scalasteward.core.io.process.SlurpOptions
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.scalafmt.ScalafmtAlg.{opts, parseScalafmtConf}
 import org.scalasteward.core.util.Nel
@@ -51,10 +52,12 @@ final class ScalafmtAlg[F[_]](config: Config)(implicit
       .map(version => Scope(List(scalafmtDependency(version)), List(config.defaultResolver)))
       .value
 
-  def reformatChanged(repo: Repo): F[Unit] = {
-    val cmd = Nel.of(scalafmtBinary, opts.nonInteractive) ++ opts.modeChanged
-    workspaceAlg.repoDir(repo).flatMap(processAlg.exec(cmd, _)).void
-  }
+  def reformatChanged(repo: Repo): F[Unit] =
+    for {
+      repoDir <- workspaceAlg.repoDir(repo)
+      cmd = Nel.of(scalafmtBinary, opts.nonInteractive) ++ opts.modeChanged
+      _ <- processAlg.exec(cmd, repoDir, slurpOptions = SlurpOptions.ignoreBufferOverflow)
+    } yield ()
 
   def version: F[String] = {
     val cmd = Nel.of(scalafmtBinary, opts.version)
@@ -64,7 +67,7 @@ final class ScalafmtAlg[F[_]](config: Config)(implicit
 
 object ScalafmtAlg {
   object opts {
-    val modeChanged = List("--mode", "changed")
+    val modeChanged: List[String] = List("--mode", "changed")
     val nonInteractive = "--non-interactive"
     val version = "--version"
   }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/util/package.scala b/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
index d3ca06db..0a41d65a 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
@@ -26,9 +26,11 @@ package object util {
   final val Nel = cats.data.NonEmptyList
 
   /** Appends `elem` to `buffer` such that its size does not exceed `maxSize`. */
-  def appendBounded[A](buffer: ListBuffer[A], elem: A, maxSize: Int): Unit = {
-    if (buffer.size >= maxSize) buffer.remove(0, maxSize / 2)
+  def appendBounded[A](buffer: ListBuffer[A], elem: A, maxSize: Int): Boolean = {
+    val maxSizeExceeded = buffer.size >= maxSize
+    if (maxSizeExceeded) buffer.remove(0, maxSize / 2)
     buffer.append(elem)
+    maxSizeExceeded
   }
 
   /** Like `Semigroup[Option[A]].combine` but allows to specify how `A`s are combined. */


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/io/processTest.scala b/modules/core/src/test/scala/org/scalasteward/core/io/processTest.scala
index 1274c694..4b589f83 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/io/processTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/io/processTest.scala
@@ -2,49 +2,74 @@ package org.scalasteward.core.io
 
 import cats.effect.IO
 import cats.effect.unsafe.implicits.global
+import fs2.text.LineTooLongException
 import munit.FunSuite
-import org.scalasteward.core.io.process.Args
+import org.scalasteward.core.io.process._
 import org.scalasteward.core.util.{DateTimeAlg, Nel}
-import scala.concurrent.TimeoutException
 import scala.concurrent.duration._
 
 class processTest extends FunSuite {
   def slurp1(cmd: Nel[String]): IO[List[String]] =
-    process.slurp[IO](Args(cmd), 1.minute, 8192, _ => IO.unit)
+    slurp[IO](Args(cmd), 1.minute, 8192, _ => IO.unit)
 
   def slurp2(cmd: Nel[String], timeout: FiniteDuration): IO[List[String]] =
-    process.slurp[IO](Args(cmd), timeout, 8192, _ => IO.unit)
+    slurp[IO](Args(cmd), timeout, 8192, _ => IO.unit)
 
-  test("echo hello") {
+  def slurp3(
+      cmd: Nel[String],
+      maxBufferSize: Int,
+      slurpOptions: SlurpOptions
+  ): IO[List[String]] =
+    slurp[IO](Args(cmd, slurpOptions = slurpOptions), 1.minute, maxBufferSize, _ => IO.unit)
+
+  test("echo: ok, one line") {
     assertEquals(slurp1(Nel.of("echo", "-n", "hello")).unsafeRunSync(), List("hello"))
   }
 
-  test("echo hello world") {
-    assertEquals(
-      slurp1(Nel.of("echo", "-n", "hello\nworld")).unsafeRunSync(),
-      List("hello", "world")
-    )
+  test("echo: ok, two lines") {
+    val obtained = slurp1(Nel.of("echo", "-n", "hello\nworld")).unsafeRunSync()
+    assertEquals(obtained, List("hello", "world"))
+  }
+
+  test("echo: ok, buffer size exceeded") {
+    val obtained =
+      slurp3(Nel.of("echo", "-n", "1\n2\n3\n4\n5\n6"), 4, SlurpOptions.ignoreBufferOverflow)
+        .unsafeRunSync()
+    assertEquals(obtained, List("3", "4", "5", "6"))
+  }
+
+  test("echo: fail, buffer size exceeded") {
+    val Left(t) =
+      slurp3(Nel.of("echo", "-n", "1\n2\n3\n4\n5\n6"), 4, Set.empty).attempt.unsafeRunSync()
+    assert(clue(t).isInstanceOf[ProcessBufferOverflowException])
+  }
+
+  test("echo: fail, line length > buffer size") {
+    val Left(t) =
+      slurp3(Nel.of("echo", "-n", "123456"), 4, Set.empty).attempt.unsafeRunSync()
+    assert(clue(t).isInstanceOf[LineTooLongException])
   }
 
-  test("ls") {
+  test("ls: ok") {
     assert(slurp1(Nel.of("ls")).attempt.unsafeRunSync().isRight)
   }
 
-  test("ls --foo") {
-    assert(slurp1(Nel.of("ls", "--foo")).attempt.unsafeRunSync().isLeft)
+  test("ls: fail, non-zero exit code") {
+    val Left(t) = slurp1(Nel.of("ls", "--foo")).attempt.unsafeRunSync()
+    assert(clue(t).isInstanceOf[ProcessFailedException])
   }
 
   test("sleep 1: ok") {
     assertEquals(slurp2(Nel.of("sleep", "1"), 2.seconds).unsafeRunSync(), List())
   }
 
-  test("sleep 1: fail") {
+  test("sleep 1: fail, timeout") {
     val timeout = 500.milliseconds
     val sleep = timeout * 2
     val p = slurp2(Nel.of("sleep", sleep.toSeconds.toInt.toString), timeout).attempt
     val (Left(t), fd) = DateTimeAlg.create[IO].timed(p).unsafeRunSync()
 
-    assert(clue(t).isInstanceOf[TimeoutException])
+    assert(clue(t).isInstanceOf[ProcessTimedOutException])
     assert(clue(fd) > timeout)
     assert(clue(fd) < sleep)
   }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/util/utilTest.scala b/modules/core/src/test/scala/org/scalasteward/core/util/utilTest.scala
index c6ea0734..042de3d3 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/util/utilTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/util/utilTest.scala
@@ -12,22 +12,22 @@ class utilTest extends ScalaCheckSuite {
     val lb = new ListBuffer[Int]
     lb.appendAll(List(1, 2, 3))
 
-    appendBounded(lb, 4, 4)
+    assert(!appendBounded(lb, 4, 4))
     assertEquals(lb.toList, List(1, 2, 3, 4))
 
-    appendBounded(lb, 5, 4)
+    assert(appendBounded(lb, 5, 4))
     assertEquals(lb.toList, List(3, 4, 5))
 
-    appendBounded(lb, 6, 4)
+    assert(!appendBounded(lb, 6, 4))
     assertEquals(lb.toList, List(3, 4, 5, 6))
 
-    appendBounded(lb, 7, 6)
+    assert(!appendBounded(lb, 7, 6))
     assertEquals(lb.toList, List(3, 4, 5, 6, 7))
 
-    appendBounded(lb, 8, 6)
+    assert(!appendBounded(lb, 8, 6))
     assertEquals(lb.toList, List(3, 4, 5, 6, 7, 8))
 
-    appendBounded(lb, 9, 6)
+    assert(appendBounded(lb, 9, 6))
     assertEquals(lb.toList, List(6, 7, 8, 9))
   }