-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
log scalafix --check
diffs as errors via a sbt logger
#106
Changes from all commits
1f2d0a2
6892311
13efb64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package scalafix.internal.sbt | ||
|
||
import java.io.{ByteArrayOutputStream, OutputStream} | ||
|
||
import sbt.{Level, Logger} | ||
|
||
/** Split an OutputStream into messages and feed them to a given logger at a specified level. Not thread-safe. */ | ||
class LoggingOutputStream( | ||
logger: Logger, | ||
level: Level.Value, | ||
separator: String | ||
) extends OutputStream { | ||
|
||
private val baos = new ByteArrayOutputStream { | ||
def maybeStripSuffix(suffix: Array[Byte]): Option[String] = { | ||
def endsWithSuffix: Boolean = | ||
count >= suffix.length && suffix.zipWithIndex.forall { | ||
case (b: Byte, i: Int) => | ||
b == buf(count - separatorBytes.length + i) | ||
} | ||
|
||
if (endsWithSuffix) | ||
Some(new String(buf, 0, count - separatorBytes.length)) | ||
else None | ||
} | ||
} | ||
|
||
private val separatorBytes = separator.getBytes | ||
require(separatorBytes.length > 0) | ||
|
||
override def write(b: Int): Unit = { | ||
baos.write(b) | ||
baos.maybeStripSuffix(separatorBytes).foreach { message => | ||
logger.log(level, message) | ||
baos.reset() | ||
} | ||
} | ||
} | ||
|
||
object LoggingOutputStream { | ||
def apply(logger: Logger, level: Level.Value): OutputStream = | ||
new LoggingOutputStream(logger, level, System.lineSeparator) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
package scalafix.sbt | ||
|
||
import java.io.PrintStream | ||
import java.nio.file.{Path, Paths} | ||
|
||
import com.geirsson.coursiersmall.Repository | ||
|
@@ -168,7 +169,8 @@ object ScalafixPlugin extends AutoPlugin { | |
loadedRules = () => scalafixInterface().availableRules(), | ||
terminalWidth = Some(JLineAccess.terminalWidth) | ||
).parser.parsed | ||
|
||
val errorLogger = | ||
new PrintStream(LoggingOutputStream(streams.value.log, Level.Error)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about deferring the logging until the point we know the status code, to adjust the level accordingly, but looking at the usage of that |
||
val projectDepsInternal = products.in(ScalafixConfig).value ++ | ||
internalDependencyClasspath.in(ScalafixConfig).value.map(_.data) | ||
val projectDepsExternal = | ||
|
@@ -192,6 +194,7 @@ object ScalafixPlugin extends AutoPlugin { | |
val mainInterface = mainInterface0 | ||
.withArgs(maybeNoCache: _*) | ||
.withArgs( | ||
Arg.PrintStream(errorLogger), | ||
Arg.Config(scalafixConf), | ||
Arg.Rules(shell.rules), | ||
Arg.ParsedArgs(shell.extra) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
-> example/scalafix --test | ||
$ sleep 2000 | ||
> checkLogs | ||
> example/scalafix | ||
> example/scalafix --test | ||
> tests/test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package scalafix.internal.sbt | ||
|
||
import java.io.PrintStream | ||
|
||
import org.scalatest.funsuite.AnyFunSuite | ||
import org.scalatest.matchers.must.Matchers | ||
import sbt.{Level, Logger} | ||
|
||
import scala.collection.mutable | ||
|
||
class LoggingOutputStreamSuite extends AnyFunSuite with Matchers { | ||
|
||
val sep = System.lineSeparator | ||
val CRLF = "\r\n" | ||
|
||
def withStubLogger( | ||
level: Level.Value, | ||
separator: String = sep | ||
)( | ||
testFun: ( | ||
LoggingOutputStream, | ||
mutable.Seq[(Level.Value, String)] | ||
) => Unit | ||
): Unit = { | ||
val logs = mutable.ListBuffer[(Level.Value, String)]() | ||
|
||
val logger = new Logger { | ||
override def log(level: Level.Value, message: => String): Unit = | ||
logs += ((level, message)) | ||
|
||
override def success(message: => String): Unit = ??? | ||
override def trace(t: => Throwable): Unit = ??? | ||
} | ||
|
||
testFun(new LoggingOutputStream(logger, level, separator), logs) | ||
} | ||
|
||
test("capture objects printed through PrintStream.println") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val data = 1234 | ||
new PrintStream(outputStream).println(data) | ||
logs must be(mutable.Seq((Level.Warn, String.valueOf(data)))) | ||
} | ||
} | ||
|
||
test("capture messages of increasing length") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val word = "foo" | ||
val printStream = new PrintStream(outputStream) | ||
printStream.println(word) | ||
printStream.println(word * 2) | ||
printStream.println(word * 3) | ||
logs.map(_._2) must be(mutable.Seq(word, word * 2, word * 3)) | ||
} | ||
} | ||
|
||
test("capture messages of decreasing length") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val word = "foo" | ||
val printStream = new PrintStream(outputStream) | ||
printStream.println(word * 3) | ||
printStream.println(word * 2) | ||
printStream.println(word) | ||
logs.map(_._2) must be(mutable.Seq(word * 3, word * 2, word)) | ||
} | ||
} | ||
|
||
test("capture messages of non-monotonic length") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val word = "foo" | ||
val printStream = new PrintStream(outputStream) | ||
printStream.println(word * 3) | ||
printStream.println(word) | ||
printStream.println(word * 2) | ||
logs.map(_._2) must be(mutable.Seq(word * 3, word, word * 2)) | ||
} | ||
} | ||
|
||
test("capture heading empty message") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val message = "hello world!" | ||
outputStream.write(s"$sep$message$sep".getBytes) | ||
logs.map(_._2) must be(mutable.Seq("", message)) | ||
} | ||
} | ||
|
||
test("capture in-between empty message") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val message1 = "hello world!" | ||
val message2 = "here we are" | ||
outputStream.write(s"$message1$sep$sep$message2$sep".stripMargin.getBytes) | ||
logs.map(_._2) must be(mutable.Seq(message1, "", message2)) | ||
} | ||
} | ||
|
||
test("capture trailing empty message") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val message = "hello world!" | ||
outputStream.write(s"$message$sep$sep".getBytes) | ||
logs.map(_._2) must be(mutable.Seq(message, "")) | ||
} | ||
} | ||
|
||
test("capture multi-byte characters") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val messageWithNonAsciiChar = "il était un petit navire" | ||
messageWithNonAsciiChar.getBytes.length must be > messageWithNonAsciiChar.length //assert test is correct | ||
outputStream.write(s"$messageWithNonAsciiChar$sep".getBytes) | ||
logs.map(_._2) must be(mutable.Seq(messageWithNonAsciiChar)) | ||
} | ||
} | ||
|
||
test("handle multi-character separator") { | ||
withStubLogger(Level.Warn, CRLF) { (outputStream, logs) => | ||
val message1 = "hello world!" | ||
val message2 = "here we are" | ||
outputStream.write(s"$message1$CRLF$CRLF$message2$CRLF".getBytes) | ||
logs.map(_._2) must be(mutable.Seq(message1, "", message2)) | ||
} | ||
} | ||
|
||
test("capture very long messages") { | ||
withStubLogger(Level.Warn) { (outputStream, logs) => | ||
val veryLongMessage = "a" * 1000000 // this would exhaust memory on quadratic implementations | ||
outputStream.write(s"$veryLongMessage$sep".getBytes) | ||
logs.map(_._2) must be(mutable.Seq(veryLongMessage)) | ||
} | ||
} | ||
|
||
test( | ||
"capture very long messages containing a subset of the line separator" | ||
) { | ||
withStubLogger(Level.Warn, CRLF) { (outputStream, logs) => | ||
val veryLongMessage = CRLF.head.toString * 1000000 | ||
outputStream.write(s"$veryLongMessage$CRLF".getBytes) | ||
logs.map(_._2) must be(mutable.Seq(veryLongMessage)) | ||
} | ||
} | ||
|
||
test("fail verbosely for invalid separator") { | ||
an[IllegalArgumentException] should be thrownBy new LoggingOutputStream( | ||
Logger.Null, | ||
Level.Warn, | ||
separator = "" | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surprisingly, I couldn't find any helper class in sbt... The best I found was https://github.com/sbt/util/blob/d31b9c509384ecaf37f2ffaec8731250e3c60532/internal/util-logging/src/main/scala/sbt/internal/util/LoggerWriter.scala, which is internal and doesn't match the types we need in that case.