diff --git a/jvm/src/main/scala/com/nawforce/apexlink/org/OrgInfo.scala b/jvm/src/main/scala/com/nawforce/apexlink/org/OrgInfo.scala index b0ac8fb05..a9ce92b8e 100644 --- a/jvm/src/main/scala/com/nawforce/apexlink/org/OrgInfo.scala +++ b/jvm/src/main/scala/com/nawforce/apexlink/org/OrgInfo.scala @@ -14,9 +14,18 @@ package com.nawforce.apexlink.org import com.nawforce.apexlink.org.OPM.OrgImpl -import com.nawforce.pkgforce.diagnostics.{Diagnostic, ERROR_CATEGORY, Issue, MISSING_CATEGORY} -import com.nawforce.pkgforce.path.PathLocation +import com.nawforce.pkgforce.diagnostics.{ + Diagnostic, + ERROR_CATEGORY, + Issue, + LoggerOps, + MISSING_CATEGORY +} +import com.nawforce.pkgforce.path.{Location, PathLike, PathLocation} +import java.io.{File, PrintWriter, StringWriter} +import java.nio.file.Files +import scala.collection.compat.immutable.ArraySeq import scala.util.DynamicVariable /** Access to the 'current' org, this should be deprecated now we have the OPM hierarchy. @@ -42,4 +51,37 @@ object OrgInfo { log(new Issue(pathLocation.path, Diagnostic(MISSING_CATEGORY, pathLocation.location, message))) } + /** Log an exception during processing. If at least one path is provided this logs the + * exception against the first. The files are copied to a temporary directory to aid debugging. + */ + private[nawforce] def logException(ex: Throwable, paths: ArraySeq[PathLike]): Unit = { + if (paths.isEmpty) { + LoggerOps.info("Exception reported against no paths", ex) + return + } + + try { + val writer = new StringWriter + writer.append("Validation failed: ") + val tempDir = Files.createTempDirectory("apex-ls-log") + paths.foreach(path => { + val from = new File(path.toString).toPath + if (Files.exists(from)) { + Files.copy( + from, + tempDir.resolve(from.getFileName), + java.nio.file.StandardCopyOption.REPLACE_EXISTING + ) + } + }) + writer.append("log directory ") + writer.append(tempDir.toString) + writer.append('\n') + ex.printStackTrace(new PrintWriter(writer)) + log(Issue(ERROR_CATEGORY, PathLocation(paths.head, Location.empty), writer.toString)) + } catch { + case ex: Throwable => + LoggerOps.info("Failed to log an exception", ex) + } + } } diff --git a/jvm/src/main/scala/com/nawforce/apexlink/types/core/TypeDeclaration.scala b/jvm/src/main/scala/com/nawforce/apexlink/types/core/TypeDeclaration.scala index 56b780faa..b210dc2ca 100644 --- a/jvm/src/main/scala/com/nawforce/apexlink/types/core/TypeDeclaration.scala +++ b/jvm/src/main/scala/com/nawforce/apexlink/types/core/TypeDeclaration.scala @@ -38,6 +38,7 @@ import com.nawforce.pkgforce.parsers.{CLASS_NATURE, INTERFACE_NATURE, Nature} import com.nawforce.pkgforce.path.{Location, PathLike, PathLocation, UnsafeLocatable} import java.io.{PrintWriter, StringWriter} +import java.nio.file.Files import scala.collection.immutable.ArraySeq import scala.collection.mutable @@ -431,14 +432,7 @@ trait TypeDeclaration extends AbstractTypeDeclaration with Dependent with PreReV try { validate() } catch { - case ex: Throwable => - val writer = new StringWriter - writer.append("Validation failed") - writer.append(": ") - ex.printStackTrace(new PrintWriter(writer)) - OrgInfo.log( - Issue(ERROR_CATEGORY, PathLocation(paths.head, Location.empty), writer.toString) - ) + case ex: Throwable => OrgInfo.logException(ex, paths) } } diff --git a/jvm/src/test/scala/com/nawforce/apexlink/org/OrgInfoTest.scala b/jvm/src/test/scala/com/nawforce/apexlink/org/OrgInfoTest.scala new file mode 100644 index 000000000..8f7930b94 --- /dev/null +++ b/jvm/src/test/scala/com/nawforce/apexlink/org/OrgInfoTest.scala @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Certinia Inc. All rights reserved. + */ +package com.nawforce.apexlink.org + +import com.nawforce.apexlink.TestHelper +import com.nawforce.pkgforce.diagnostics.LoggerOps.{INFO_LOGGING, NO_LOGGING} +import com.nawforce.pkgforce.diagnostics.{Logger, LoggerOps} +import com.nawforce.pkgforce.path.PathLike +import com.nawforce.runtime.FileSystemHelper +import org.scalatest.funsuite.AnyFunSuite + +import java.io.{File, StringWriter} +import scala.collection.immutable.ArraySeq + +class OrgInfoTest extends AnyFunSuite with TestHelper { + + final class CaptureLogger extends Logger { + val writer: StringWriter = new StringWriter() + + override def info(message: String): Unit = { + writer.append(s"INFO: $message\n") + } + + override def debug(message: String): Unit = { + writer.append(s"DEBUG: $message\n") + } + + override def trace(message: String): Unit = { + writer.append(s"TRACE: $message\n") + } + } + + test("Log exception without files creates info message") { + FileSystemHelper.run(Map()) { root: PathLike => + createOrg(root) + + val captureLogger = new CaptureLogger + val oldLogger = LoggerOps.setLogger(captureLogger) + LoggerOps.setLoggingLevel(INFO_LOGGING) + + OrgInfo.logException(new Exception("Hello"), ArraySeq()) + + assert(captureLogger.writer.toString.startsWith("INFO: Exception reported against no paths")) + assert(captureLogger.writer.toString.contains("INFO: java.lang.Exception: Hello")) + + LoggerOps.setLoggingLevel(NO_LOGGING) + LoggerOps.setLogger(oldLogger) + + } + } + + test("Log exception captures files") { + FileSystemHelper.runTempDir( + Map("a.txt" -> "a.txt", "dir1/b.txt" -> "b.txt", "dir1/dir2/c.txt" -> "c.txt") + ) { root: PathLike => + createOrg(root) + + val captureLogger = new CaptureLogger + val oldLogger = LoggerOps.setLogger(captureLogger) + LoggerOps.setLoggingLevel(INFO_LOGGING) + + try { + + withOrg { _ => + OrgInfo.logException( + new Exception("Hello"), + ArraySeq(root.join("a.txt"), root.join("dir1/b.txt"), root.join("dir1/dir2/c.txt")) + ) + } + + assert(captureLogger.writer.toString.isEmpty) + assert(getMessages().contains("/a.txt: Error: line 1: Validation failed: log directory")) + assert(getMessages().contains("java.lang.Exception: Hello")) + + val tmpDir = getMessages().split("\n").head.split("directory").last.trim + val tmpDirPath = new File(tmpDir) + assert(tmpDirPath.isDirectory) + assert(tmpDirPath.listFiles().map(_.getName).toSet == Set("a.txt", "b.txt", "c.txt")) + + } finally { + LoggerOps.setLoggingLevel(NO_LOGGING) + LoggerOps.setLogger(oldLogger) + } + } + } + +}