Skip to content

Commit

Permalink
feature: reports
Browse files Browse the repository at this point in the history
A framework for creating error reports in metals.
  • Loading branch information
kasiaMarek committed Feb 14, 2023
1 parent aa943d5 commit 8199993
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1086,4 +1086,11 @@ object MetalsEnrichments
}
}

implicit class XtensionAny[T](v: T) {
def withExec[R](toExec: T => R): T = {
toExec(v)
v
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ class MetalsLspService(

val tables: Tables = register(new Tables(workspace, time))

private val reports = new Reports(workspace)

private val buildTools: BuildTools = new BuildTools(
workspace,
bspGlobalDirectories,
Expand Down Expand Up @@ -288,7 +290,7 @@ class MetalsLspService(
)

private val timerProvider: TimerProvider = new TimerProvider(time)
private val trees = new Trees(buffers, scalaVersionSelector, workspace)
private val trees = new Trees(buffers, scalaVersionSelector, reports)

private val documentSymbolProvider = new DocumentSymbolProvider(
trees,
Expand Down Expand Up @@ -1050,7 +1052,7 @@ class MetalsLspService(
tables.fingerprints.save(fingerprints.getAllFingerprints())
cancel()
} catch {
case NonFatal(e) =>
case NonFatal(e) => // report?
scribe.error("cancellation error", e)
} finally {
promise.success(())
Expand Down Expand Up @@ -2209,7 +2211,7 @@ class MetalsLspService(
case Failed(exit) =>
exit match {
case Left(exitCode) =>
scribe.error(
scribe.error( // report
s"Create of .bsp failed with exit code: $exitCode"
)
languageClient.showMessage(
Expand Down Expand Up @@ -2383,7 +2385,7 @@ class MetalsLspService(
if (ammoniteChanges.nonEmpty)
ammonite.importBuild().onComplete {
case Success(()) =>
case Failure(exception) =>
case Failure(exception) => // report?
scribe.error("Error re-importing Ammonite build", exception)
}

Expand Down Expand Up @@ -2448,7 +2450,7 @@ class MetalsLspService(
val details = " See logs for more details."
languageClient.showMessage(
new MessageParams(MessageType.Error, message + details)
)
) // report
scribe.error(message, e)
BuildChange.Failed
}
Expand Down Expand Up @@ -2726,7 +2728,7 @@ class MetalsLspService(

private def newSymbolIndex(): OnDemandSymbolIndex = {
OnDemandSymbolIndex.empty(
onError = {
onError = { // report?
case e @ (_: ParseException | _: TokenizeException) =>
scribe.error(e.toString)
case e: IndexingExceptions.InvalidJarException =>
Expand Down
123 changes: 123 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/ReportsProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package scala.meta.internal.metals

import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath
import scala.meta.io.RelativePath

class Reports(workspace: AbsolutePath) {
private lazy val reportsDir =
workspace.resolve(Directories.reports).withExec { d =>
Files.createDirectories(d.toNIO)
}
val unsanitized =
new ReportsProvider(workspace, Directories.reports.resolve("metals-full"))
val incognito =
new ReportsProvider(workspace, Directories.reports.resolve("metals"))
val bloop =
new ReportsProvider(workspace, Directories.reports.resolve("bloop"))

def all: List[ReportsProvider] = List(unsanitized, incognito, bloop)
def allToZip: List[ReportsProvider] = List(incognito, bloop)

def zipReports(): Path = {
val path = reportsDir.resolve(Reports.ZIP_FILE_NAME).toNIO
val zipOut = new ZipOutputStream(Files.newOutputStream(path))

for {
reportsProvider <- allToZip
report <- reportsProvider.getReports
} {
val zipEntry = new ZipEntry(report.name)
zipOut.putNextEntry(zipEntry)
zipOut.write(Files.readAllBytes(report.toPath))
}
zipOut.close()

path
}

def cleanUpOldReports(
maxReportsNumber: Int = Reports.MAX_NUMBER_OF_REPORTS
): Unit = {
all.foreach(_.cleanUpOldReports(maxReportsNumber))
}

def deleteAll(): Unit = {
all.foreach(_.deleteAll())
Files.delete(reportsDir.resolve(Reports.ZIP_FILE_NAME).toNIO)
}
}

class ReportsProvider(workspace: AbsolutePath, pathToReports: RelativePath) {
private lazy val reportsDir =
workspace.resolve(pathToReports).withExec { d =>
Files.createDirectories(d.toNIO)
}

private lazy val userHome = Option(System.getProperty("user.home"))

def createReport(name: String, text: String): AbsolutePath =
reportsDir
.resolve(s"r_${name}_${System.currentTimeMillis()}")
.withExec(_.writeText(sanitize(text)))

private def sanitize(text: String) = {
val textAfterWokspaceReplace =
text.replaceAll(workspace.toString(), Reports.WORKSPACE_STR)
userHome
.map(textAfterWokspaceReplace.replaceAll(_, Reports.HOME_STR))
.getOrElse(textAfterWokspaceReplace)
}

def cleanUpOldReports(
maxReportsNumber: Int = Reports.MAX_NUMBER_OF_REPORTS
): List[Report] = {
val reports = getReports
if (reports.length > maxReportsNumber) {
val filesToDelete = reports
.sortBy(_.timestamp)
.slice(0, reports.length - maxReportsNumber)
filesToDelete.foreach { f => Files.delete(f.toPath) }
filesToDelete
} else List()
}

def getReports: List[Report] = {
val reportsDir = workspace.resolve(pathToReports)
if (reportsDir.exists && reportsDir.isDirectory) {
reportsDir.toFile.listFiles().toList.map(Report.fromFile(_)).collect {
case Some(l) => l
}
} else List()
}

def deleteAll(): Unit = getReports.foreach(r => Files.delete(r.toPath))
}

object Reports {
val MAX_NUMBER_OF_REPORTS = 30
val WORKSPACE_STR = "<WORKSPACE>"
val HOME_STR = "<HOME>"
val ZIP_FILE_NAME = "reports.zip"
}

case class Report(file: File, timestamp: Long) {
def toPath: Path = file.toPath()
def name: String = file.getName()
}

object Report {
def fromFile(file: File): Option[Report] = {
val reportRegex = "r_.*_([-+]?[0-9]+)".r
file.getName() match {
case reportRegex(time) => Some(Report(file, time.toLong))
case _: String => None
}
}
}
17 changes: 6 additions & 11 deletions metals/src/main/scala/scala/meta/internal/parsing/Trees.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package scala.meta.internal.parsing

import java.nio.file.Files

import scala.collection.concurrent.TrieMap
import scala.reflect.ClassTag

import scala.meta._
import scala.meta.inputs.Position
import scala.meta.internal.metals.Buffers
import scala.meta.internal.metals.Directories
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.Reports
import scala.meta.internal.metals.ScalaVersionSelector
import scala.meta.io.AbsolutePath
import scala.meta.parsers.Parse
Expand All @@ -30,7 +28,7 @@ import org.eclipse.{lsp4j => l}
final class Trees(
buffers: Buffers,
scalaVersionSelector: ScalaVersionSelector,
workspace: AbsolutePath,
reports: Reports,
) {

private val trees = TrieMap.empty[AbsolutePath, Tree]
Expand Down Expand Up @@ -141,13 +139,10 @@ final class Trees(
} catch {
// if the parsers breaks we should not throw the exception further
case _: StackOverflowError =>
val reportsDir = workspace.resolve(Directories.reports)
val newPathCopy =
reportsDir.resolve(
"stackoverflow_" + System.currentTimeMillis() + "_" + path.filename
)
Files.createDirectories(reportsDir.toNIO)
newPathCopy.writeText(text)
val newPathCopy = reports.unsanitized.createReport(
s"stackoverflow_${path.filename}",
text,
)
val message =
s"Could not parse $path, saved the current snapshot to ${newPathCopy}"
scribe.warn(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.MetalsLspService
import scala.meta.internal.metals.MetalsServerInputs
import scala.meta.internal.metals.MutableCancelable
import scala.meta.internal.metals.Reports
import scala.meta.internal.metals.ThreadPools
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.internal.metals.clients.language.NoopLanguageClient
Expand Down Expand Up @@ -166,6 +167,8 @@ class MetalsLanguageServer(
serverState.set(ServerState.Initialized(service))
metalsService.underlying = service

new Reports(workspace).cleanUpOldReports()

service.initialize()
}
}
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/src/main/scala/tests/TestingServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import scala.meta.internal.metals.MtagsResolver
import scala.meta.internal.metals.ParametrizedCommand
import scala.meta.internal.metals.PositionSyntax._
import scala.meta.internal.metals.ProgressTicks
import scala.meta.internal.metals.Reports
import scala.meta.internal.metals.ScalaVersionSelector
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.TextEdits
Expand Down Expand Up @@ -162,13 +163,15 @@ final case class TestingServer(

lazy val server = languageServer.getOldMetalsLanguageServer

val reports = new Reports(workspace)

private lazy val trees = new Trees(
buffers,
new ScalaVersionSelector(
() => initialUserConfig,
server.buildTargets,
),
workspace,
reports,
)

private val virtualDocSources = TrieMap.empty[String, AbsolutePath]
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/src/main/scala/tests/TreeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.nio.file.Paths

import scala.meta.internal.metals.Buffers
import scala.meta.internal.metals.BuildTargets
import scala.meta.internal.metals.Reports
import scala.meta.internal.metals.ScalaVersionSelector
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.parsing.Trees
Expand All @@ -21,7 +22,8 @@ object TreeUtils {
() => UserConfiguration(fallbackScalaVersion = scalaVersion),
buildTargets,
)
val trees = new Trees(buffers, selector, AbsolutePath(Paths.get(".")))
val trees =
new Trees(buffers, selector, new Reports(AbsolutePath(Paths.get("."))))
(buffers, trees)
}
}
60 changes: 60 additions & 0 deletions tests/unit/src/test/scala/tests/ReportsSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tests

import java.nio.file.Files
import java.nio.file.Paths

import scala.meta.internal.metals.Report
import scala.meta.internal.metals.Reports
import scala.meta.io.AbsolutePath

class ReportsSuite extends BaseSuite {
val workspace: AbsolutePath = AbsolutePath(Paths.get("."))
val reportsProvider = new Reports(workspace)

def exampleText(workspaceStr: String = workspace.toString()): String =
s"""|An error happend in the file:
|${workspaceStr}/WrongFile.scala
|""".stripMargin

override def afterEach(context: AfterEach): Unit = {
reportsProvider.deleteAll()
super.afterEach(context)
}

test("create-report") {
val path =
reportsProvider.incognito.createReport("test_error", exampleText())
val obtained = Files.readString(path.toNIO)
assertEquals(exampleText(Reports.WORKSPACE_STR), obtained)
assert(Report.fromFile(path.toFile).nonEmpty)
}

test("delete-old-reports") {
reportsProvider.incognito.createReport("some_test_error_old", exampleText())
reportsProvider.incognito.createReport(
"some_different_test_error_old",
exampleText(),
)
reportsProvider.incognito.createReport("some_test_error_new", exampleText())
reportsProvider.incognito.createReport(
"some_different_test_error_new",
exampleText(),
)
val deleted = reportsProvider.incognito.cleanUpOldReports(2)
assertEquals(deleted.length, 2)
deleted.foreach(f => assert(f.name.contains("old")))
val reports = reportsProvider.incognito.getReports
assertEquals(reports.length, 2)
reports.foreach(f => assert(f.name.contains("new")))
}

test("zip-reports") {
reportsProvider.incognito.createReport("test_error", exampleText())
reportsProvider.incognito.createReport(
"different_test_error",
exampleText(),
)
val pathToZip = reportsProvider.zipReports()
assertEquals(pathToZip.toFile.getName(), Reports.ZIP_FILE_NAME)
}
}

0 comments on commit 8199993

Please sign in to comment.