diff --git a/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala b/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala index 7ecd5f951..733c85cb9 100644 --- a/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala +++ b/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala @@ -61,4 +61,6 @@ trait BaseProps { def BrightCoveVideoUri(accountId: String, videoId: String): Uri = uri"https://cms.api.brightcove.com/v1/accounts/$accountId/videos/$videoId/sources" + def DisableLicense: Boolean = booleanPropOrElse("DISABLE_LICENSE", default = false) + } diff --git a/common/src/main/scala/no/ndla/common/errors/ExceptionLogHandler.scala b/common/src/main/scala/no/ndla/common/errors/ExceptionLogHandler.scala index dda568ba2..6bb171ab3 100644 --- a/common/src/main/scala/no/ndla/common/errors/ExceptionLogHandler.scala +++ b/common/src/main/scala/no/ndla/common/errors/ExceptionLogHandler.scala @@ -9,16 +9,24 @@ package no.ndla.common.errors import org.log4s.Logger +import scala.util.{Failure, Try} + object ExceptionLogHandler { val logger: Logger = org.log4s.getLogger - def default(f: => Unit): Unit = { + + private def handleException(e: Throwable): Unit = { + logger.error(e)(s"Uncaught exception, quitting...") + System.exit(1) + } + + def default(f: => Try[Unit]): Unit = { try { - f + f match { + case Failure(ex) => handleException(ex) + case _ => + } } catch { - case e: Throwable => - logger.error(e)(s"Uncaught exception, quitting...") - System.exit(1) + case ex: Throwable => handleException(ex) } } - } diff --git a/draft-api/src/test/scala/no/ndla/draftapi/integration/TaxonomyApiClientTest.scala b/draft-api/src/test/scala/no/ndla/draftapi/integration/TaxonomyApiClientTest.scala index ca012c64e..0e8bd363f 100644 --- a/draft-api/src/test/scala/no/ndla/draftapi/integration/TaxonomyApiClientTest.scala +++ b/draft-api/src/test/scala/no/ndla/draftapi/integration/TaxonomyApiClientTest.scala @@ -22,7 +22,7 @@ class TaxonomyApiClientTest extends UnitSuite with TestEnvironment { override val taxonomyApiClient: TaxonomyApiClient = spy(new TaxonomyApiClient) - override protected def beforeEach(): Unit = { + override def beforeEach(): Unit = { // Since we use spy, we reset the mock before each test allowing verify to be accurate reset(taxonomyApiClient) } diff --git a/log4j2-test.yaml b/log4j2-test.yaml index e3fe3f912..c35c426e3 100644 --- a/log4j2-test.yaml +++ b/log4j2-test.yaml @@ -1,5 +1,8 @@ Configuration: status: warn Loggers: + Logger: + name: "no.ndla" + level: debug Root: level: warn diff --git a/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/CloneFolderTest.scala b/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/CloneFolderTest.scala index 115d20b65..7d08e81a2 100644 --- a/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/CloneFolderTest.scala +++ b/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/CloneFolderTest.scala @@ -87,6 +87,7 @@ class CloneFolderTest val myndlaApiFolderUrl: String = s"$myndlaApiBaseUrl/myndla-api/v1/folders" override def beforeAll(): Unit = { + super.beforeAll() implicit val ec: ExecutionContextExecutorService = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor) Future { myndlaApi.run() }: Unit diff --git a/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/FolderTest.scala b/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/FolderTest.scala index 8e9273ff6..09e53016b 100644 --- a/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/FolderTest.scala +++ b/myndla-api/src/test/scala/no/ndla/myndlaapi/e2e/FolderTest.scala @@ -82,6 +82,7 @@ class FolderTest val myndlaApiFolderUrl: String = s"$myndlaApiBaseUrl/myndla-api/v1/folders" override def beforeAll(): Unit = { + super.beforeAll() implicit val ec = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor) Future { myndlaApi.run() }: Unit Thread.sleep(4000) diff --git a/network/src/main/scala/no/ndla/network/tapir/NdlaTapirMain.scala b/network/src/main/scala/no/ndla/network/tapir/NdlaTapirMain.scala index 8880190a0..562708694 100644 --- a/network/src/main/scala/no/ndla/network/tapir/NdlaTapirMain.scala +++ b/network/src/main/scala/no/ndla/network/tapir/NdlaTapirMain.scala @@ -10,8 +10,10 @@ package no.ndla.network.tapir import no.ndla.common.Environment.setPropsFromEnv import no.ndla.common.configuration.BaseProps import org.log4s.{Logger, getLogger} + import scala.concurrent.Future import scala.io.Source +import scala.util.Try trait NdlaTapirMain { val logger: Logger = getLogger @@ -22,7 +24,9 @@ trait NdlaTapirMain { def beforeStart(): Unit private def logCopyrightHeader(): Unit = { - logger.info(Source.fromInputStream(getClass.getResourceAsStream("/log-license.txt")).mkString) + if (!props.DisableLicense) { + logger.info(Source.fromInputStream(getClass.getResourceAsStream("/log-license.txt")).mkString) + } } private def performWarmup(): Unit = if (!props.disableWarmup) { @@ -40,13 +44,15 @@ trait NdlaTapirMain { }: Unit } - def run(): Unit = { + def run(): Try[Unit] = { setPropsFromEnv() logCopyrightHeader() - startServer(props.ApplicationName, props.ApplicationPort) { + Try(startServer(props.ApplicationName, props.ApplicationPort) { beforeStart() performWarmup() + }).recover { ex => + logger.error(ex)("Failed to start server, exiting...") } } } diff --git a/project/scalatestsuitelib.scala b/project/scalatestsuitelib.scala index b0eca9304..74338f40b 100644 --- a/project/scalatestsuitelib.scala +++ b/project/scalatestsuitelib.scala @@ -6,12 +6,17 @@ import sbt.Keys.* object scalatestsuitelib extends Module { override val moduleName: String = "scalatestsuite" override val enableReleases: Boolean = false - lazy val dependencies: Seq[ModuleID] = Seq( - "org.scalatest" %% "scalatest" % ScalaTestV, - "org.testcontainers" % "elasticsearch" % TestContainersV, - "org.testcontainers" % "testcontainers" % TestContainersV, - "org.testcontainers" % "postgresql" % TestContainersV - ) ++ database ++ vulnerabilityOverrides ++ mockito + lazy val dependencies: Seq[ModuleID] = withLogging( + Seq( + "org.scalatest" %% "scalatest" % ScalaTestV, + "org.testcontainers" % "elasticsearch" % TestContainersV, + "org.testcontainers" % "testcontainers" % TestContainersV, + "org.testcontainers" % "postgresql" % TestContainersV + ), + database, + vulnerabilityOverrides, + mockito + ) override lazy val settings: Seq[Def.Setting[?]] = Seq( libraryDependencies ++= dependencies diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/BufferedLogAppender.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/BufferedLogAppender.scala new file mode 100644 index 000000000..cccb735b4 --- /dev/null +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/BufferedLogAppender.scala @@ -0,0 +1,39 @@ +/* + * Part of NDLA scalatestsuite + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.scalatestsuite + +import org.apache.logging.log4j.core.LogEvent +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.layout.PatternLayout + +import scala.collection.mutable.ListBuffer + +object Layout { + lazy val prebuiltLayout: PatternLayout = PatternLayout + .newBuilder() + .withPattern("[%level] %C.%M#%L: %msg%n") + .build() +} + +class BufferedLogAppender + extends AbstractAppender("BufferedAppender", null, Layout.prebuiltLayout, false, Array.empty) { + private val logQueue = ListBuffer.empty[String] + def clear(): Unit = logQueue.clear() + def printLogs(): Unit = logQueue.foreach(print) + def getAndClearLogs: List[String] = { + val toReturn = logQueue.toList + logQueue.clear() + toReturn + } + + override def append(event: LogEvent): Unit = { + val logString = getLayout.toSerializable(event).toString + logQueue.append(logString) + } +} diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/ColoredText.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/ColoredText.scala new file mode 100644 index 000000000..612ad2d6b --- /dev/null +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/ColoredText.scala @@ -0,0 +1,27 @@ +/* + * Part of NDLA scalatestsuite + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.scalatestsuite + +object ColoredText { + val RED = "\u001b[31m" + val GREEN = "\u001b[32m" + val YELLOW = "\u001b[33m" + val BLUE = "\u001b[34m" + val RESET = "\u001b[0m" + + def print(color: Colors, text: String) = { + color match { + case Red => println(s"$RED$text$RESET") + case Green => println(s"$GREEN$text$RESET") + case Yellow => println(s"$YELLOW$text$RESET") + case Blue => println(s"$BLUE$text$RESET") + } + } + +} diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/Colors.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/Colors.scala new file mode 100644 index 000000000..b747dbd9c --- /dev/null +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/Colors.scala @@ -0,0 +1,15 @@ +/* + * Part of NDLA scalatestsuite + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.scalatestsuite + +sealed trait Colors +case object Red extends Colors +case object Green extends Colors +case object Yellow extends Colors +case object Blue extends Colors diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/IntegrationSuite.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/IntegrationSuite.scala index 5b0f9af76..b85371bdd 100644 --- a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/IntegrationSuite.scala +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/IntegrationSuite.scala @@ -149,8 +149,13 @@ abstract class IntegrationSuite( }) } - override def beforeAll(): Unit = setDatabaseEnvironment() + override def beforeAll(): Unit = { + super.beforeAll() + setDatabaseEnvironment() + } + override def afterAll(): Unit = { + super.afterAll() setPropEnv(previousDatabaseEnv) elasticSearchContainer.foreach(c => c.stop()) postgresContainer.foreach(c => c.stop()) diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/TestSuiteLoggingSetup.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/TestSuiteLoggingSetup.scala new file mode 100644 index 000000000..71ab21b20 --- /dev/null +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/TestSuiteLoggingSetup.scala @@ -0,0 +1,66 @@ +/* + * Part of NDLA scalatestsuite + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.scalatestsuite + +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.{Level, LogManager} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome} + +import scala.collection.mutable.ListBuffer + +/** Sets up test environment to keep logs and print them if the test fails */ +trait TestSuiteLoggingSetup extends AnyFunSuite with BeforeAndAfterEach with BeforeAndAfterAll { + private val appender = new BufferedLogAppender() + private def getNDLALogger = LogManager.getContext(false).asInstanceOf[LoggerContext].getLogger("no.ndla") + private val beforeAllLog = ListBuffer.empty[String] + + private def setupLogger(): Unit = { + if (!appender.isStarted) appender.start() + val ndlaLogger = getNDLALogger + ndlaLogger.setLevel(Level.DEBUG) + ndlaLogger.addAppender(appender) + } + + override def beforeEach(): Unit = { + val logs = appender.getAndClearLogs + beforeAllLog.addAll(logs) + setupLogger() + super.beforeEach() + } + + override def beforeAll(): Unit = { + setupLogger() + super.beforeAll() + } + + override def afterEach(): Unit = { + val ndlaLogger = getNDLALogger + ndlaLogger.removeAppender(appender) + appender.clear() + super.afterEach() + } + + override def withFixture(test: NoArgTest): Outcome = { + val result = super.withFixture(test) + if (!result.isSucceeded) { + // If test fails, print the buffered logs + val testName = s"${this.suiteName}$$'${test.name}'" + ColoredText.print(Red, s"\n---- Captured Logs for test: $testName ----") + if (beforeAllLog.nonEmpty) { + ColoredText.print(Red, s">>> Captured Logs from $suiteName$$beforeAll: >>>") + beforeAllLog.foreach(print) + ColoredText.print(Red, s"<<< End of Captured Logs from $suiteName$$beforeAll <<<") + } + appender.printLogs() + ColoredText.print(Red, s"---- End of Captured Logs for test: $testName ----\n") + } + result + } +} diff --git a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/UnitTestSuite.scala b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/UnitTestSuite.scala index e02ce5944..37b8f9aea 100644 --- a/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/UnitTestSuite.scala +++ b/scalatestsuite/src/main/scala/no/ndla/scalatestsuite/UnitTestSuite.scala @@ -7,15 +7,15 @@ package no.ndla.scalatestsuite -import org.scalatestplus.mockito.MockitoSugar -import org.scalatest._ +import org.scalatest.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import org.scalatestplus.mockito.MockitoSugar import java.io.IOException import java.net.ServerSocket import scala.util.Properties.{propOrNone, setProp} -import scala.util.{Try, Success, Failure} +import scala.util.{Failure, Success, Try} abstract class UnitTestSuite extends AnyFunSuite @@ -25,7 +25,10 @@ abstract class UnitTestSuite with Inspectors with MockitoSugar with BeforeAndAfterEach - with BeforeAndAfterAll { + with BeforeAndAfterAll + with TestSuiteLoggingSetup { + + setPropEnv("DISABLE_LICENSE", "true"): Unit def setPropEnv(key: String, value: String): String = setProp(key, value)