Skip to content

Commit

Permalink
Merge pull request #577 from smeup/bugfix/err_event_unknown_sourceid
Browse files Browse the repository at this point in the history
Bugfix/ErrorEvent with unknown sourceid
  • Loading branch information
lanarimarco authored Jul 23, 2024
2 parents d003991 + b77c8be commit 378bb1f
Show file tree
Hide file tree
Showing 9 changed files with 8,147 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,13 @@ open class BaseCompileTimeInterpreter(
if (it.directive().dir_api() != null) {
val apiDirective = it.directive().dir_api()
val apiId = apiDirective.toApiId(conf)
val api = MainExecutionContext.getSystemInterface()?.findApi(apiId)
api?.let {
it.compilationUnit.dataDefinitions.firstOrNull { def ->
def.name.equals(declName, ignoreCase = true)
}
}?.let { return it.type.size }
return apiId.loadAndUse { api ->
api.let {
it.compilationUnit.dataDefinitions.firstOrNull { def ->
def.name.equals(declName, ignoreCase = true)
}
}?.let { it.type.size }
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,51 @@ internal fun List<RpgParser.StatementContext>.toApiDescriptors(conf: ToAstConfig

private fun CompilationUnit.includeApi(apiId: ApiId): CompilationUnit {
return apiId.runNode {
val parentPgmName = MainExecutionContext.getExecutionProgramName()
MainExecutionContext.setExecutionProgramName(apiId.toString())
val api = MainExecutionContext.getSystemInterface()!!.findApi(apiId).apply {
MainExecutionContext.getConfiguration().jarikoCallback.onApiInclusion(apiId, this)
}.validate()
this.copy(
fileDefinitions = this.fileDefinitions.include(api.compilationUnit.fileDefinitions),
dataDefinitions = this.dataDefinitions.include(api.compilationUnit.dataDefinitions),
subroutines = this.subroutines.include(api.compilationUnit.subroutines),
compileTimeArrays = this.compileTimeArrays.include(api.compilationUnit.compileTimeArrays),
directives = this.directives.include(api.compilationUnit.directives),
position = this.position,
apiDescriptors = api.compilationUnit.apiDescriptors?.let {
this.apiDescriptors?.plus(it)
} ?: this.apiDescriptors,
procedures = this.procedures.let { it ?: listOf() }.includeProceduresWithoutDuplicates(api.compilationUnit.procedures.let { it ?: listOf() })
).apply {
MainExecutionContext.setExecutionProgramName(parentPgmName)
apiId.loadAndUse { api ->
this.copy(
fileDefinitions = this.fileDefinitions.include(api.compilationUnit.fileDefinitions),
dataDefinitions = this.dataDefinitions.include(api.compilationUnit.dataDefinitions),
subroutines = this.subroutines.include(api.compilationUnit.subroutines),
compileTimeArrays = this.compileTimeArrays.include(api.compilationUnit.compileTimeArrays),
directives = this.directives.include(api.compilationUnit.directives),
position = this.position,
apiDescriptors = api.compilationUnit.apiDescriptors?.let {
this.apiDescriptors?.plus(it)
} ?: this.apiDescriptors,
procedures = this.procedures.let { it ?: listOf() }.includeProceduresWithoutDuplicates(api.compilationUnit.procedures.let { it ?: listOf() })
)
}
}
}

/**
* Uses an API by loading it and applying the provided logic function.
*
* This function encapsulates the process of setting the current parsing program name to the API's ID,
* loading the API, invoking a callback to signal the API's inclusion, and then applying a user-defined
* logic function to the loaded API.
* After the logic function is applied, the original parsing program name is restored.
* This ensures that the API processing is isolated and does not affect the global
* execution context outside of this function's scope.
*
* @param T The return type of the logic function applied to the API.
* @param logic A higher-order function that takes an [Api] instance and returns a value of type [T].
* This function is applied to the API after it is loaded and validated.
* @return Returns the result of the logic function applied to the loaded API.
*/
internal fun <T> ApiId.loadAndUse(logic: (api: Api) -> T): T {
val parentPgmName = MainExecutionContext.getExecutionProgramName()
val apiId = this
MainExecutionContext.setExecutionProgramName(this.toString())
val api = MainExecutionContext.getSystemInterface()!!.findApi(apiId).apply {
MainExecutionContext.getConfiguration().jarikoCallback.onApiInclusion(apiId, this)
}.validate()
logic.invoke(api).let { result ->
MainExecutionContext.setExecutionProgramName(parentPgmName)
return result
}
}

internal fun CompilationUnit.postProcess(): CompilationUnit {
var compilationUnit = this
apiDescriptors?.let { apiDescriptors ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ import com.smeup.rpgparser.execution.*
import com.smeup.rpgparser.interpreter.*
import com.smeup.rpgparser.jvminterop.JavaSystemInterface
import com.smeup.rpgparser.parsing.ast.CompilationUnit
import com.smeup.rpgparser.parsing.facade.SourceReferenceType
import com.smeup.rpgparser.parsing.parsetreetoast.ParseTreeToAstError
import com.smeup.rpgparser.rpginterop.DirRpgProgramFinder
import com.smeup.rpgparser.rpginterop.RpgProgramFinder
import com.smeup.rpgparser.rpginterop.SourceProgramFinder
import org.apache.logging.log4j.LogManager
import org.junit.Assert
import java.io.File
import java.io.PrintStream
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.fail

/**
* This class must be extended from all test classes in order to automatically manage tests using both version
Expand Down Expand Up @@ -205,7 +209,8 @@ abstract class AbstractTest {
programName: String,
params: CommandLineParms = CommandLineParms(emptyList()),
configuration: Configuration = Configuration(),
systemInterface: SystemInterface = JavaSystemInterface()
systemInterface: SystemInterface = JavaSystemInterface(),
additionalProgramFinders: List<RpgProgramFinder> = emptyList<RpgProgramFinder>()
): CommandLineParms? {
val resourceName = if (programName.endsWith(".rpgle")) {
programName
Expand All @@ -223,6 +228,7 @@ abstract class AbstractTest {
if (resource != null) programFinders.add(DirRpgProgramFinder(directory = File(resource.path).parentFile))
programFinders.add(DirRpgProgramFinder(directory = File("src/test/resources/")))
if (inlinePgm) programFinders.add(SourceProgramFinder())
programFinders.addAll(additionalProgramFinders)
val jariko = getProgram(
nameOrSource = if (inlinePgm) programName else programName.substringAfterLast("/", programName),
systemInterface = systemInterface,
Expand Down Expand Up @@ -357,6 +363,166 @@ abstract class AbstractTest {
}

open fun useCompiledVersion() = false

/**
* This function is used to test the execution of a program and validate the error handling mechanism.
* It expects the program to fail and checks if the error events are correctly captured.
*
* @param pgm The name of the program to be executed.
* @param sourceReferenceType The expected type of the source reference (Program or Copy) where the error is expected to occur.
* @param sourceId The expected identifier of the source where the error is expected to occur.
* @param lines The list of line numbers where the errors are expected.
* @param reloadConfig The reload configuration to be used for the execution of the program. Default is null.
*/
protected fun executePgmCallBackTest(
pgm: String,
sourceReferenceType: SourceReferenceType,
sourceId: String,
lines: List<Int>,
reloadConfig: ReloadConfig? = null
) {
val errorEvents = mutableListOf<ErrorEvent>()
runCatching {
val configuration = Configuration().apply {
jarikoCallback.onError = { errorEvent ->
println(errorEvent)
errorEvents.add(errorEvent)
}
options = Options(debuggingInformation = true)
this.reloadConfig = reloadConfig
}
executePgm(pgm, configuration = configuration)
}.onSuccess {
Assert.fail("Program must exit with error")
}.onFailure {
println(it.stackTraceToString())
errorEvents.throwErrorIfUnknownSourceId()
Assert.assertEquals(sourceReferenceType, errorEvents[0].sourceReference!!.sourceReferenceType)
Assert.assertEquals(sourceId, errorEvents[0].sourceReference!!.sourceId)
Assert.assertEquals(lines.sorted(), errorEvents.map { errorEvent -> errorEvent.sourceReference!!.relativeLine }.sorted())
}
}

/**
* This function is used to test the execution of a program and validate the error handling mechanism.
* It expects the program to fail and checks if the error events are correctly captured.
*
* @param pgm The name of the program to be executed.
* @param sourceReferenceType The expected type of the source reference (Program or Copy) where the error is expected to occur.
* @param sourceId The expected identifier of the source where the error is expected to occur.
* @param lines The map of lines, number and message, expected.
* @param reloadConfig The reload configuration to be used for the execution of the program. Default is null.
*/
protected fun executePgmCallBackTest(
pgm: String,
sourceReferenceType: SourceReferenceType,
sourceId: String,
lines: Map<Int, String>,
reloadConfig: ReloadConfig? = null
) {
val errorEvents = mutableListOf<ErrorEvent>()
runCatching {
val configuration = Configuration().apply {
jarikoCallback.onError = { errorEvent ->
println(errorEvent)
errorEvents.add(errorEvent)
}
options = Options(debuggingInformation = true)
this.reloadConfig = reloadConfig
}
executePgm(pgm, configuration = configuration)
}.onSuccess {
Assert.fail("Program must exit with error")
}.onFailure {
println(it.stackTraceToString())
errorEvents.throwErrorIfUnknownSourceId()
Assert.assertEquals(sourceReferenceType, errorEvents[0].sourceReference!!.sourceReferenceType)
Assert.assertEquals(sourceId, errorEvents[0].sourceReference!!.sourceId)
val found = errorEvents
.associate { errorEvent ->
errorEvent.sourceReference!!.relativeLine to (errorEvent.error as ParseTreeToAstError).message!!
}
.map {
Pair(it.value, it.contains(lines))
}
Assert.assertTrue(
"Errors doesn't correspond:\n" + found.joinToString(separator = "\n") { it.first },
found.size == found.filter { it.second }.size && found.size == lines.size
)
}
}

private fun Map.Entry<Int, String>.contains(list: Map<Int, String>): Boolean {
list.forEach {
if (this.value.contains(it.value) && this.key == it.key) {
return true
}
}
return false
}

/**
* Verify that the sourceLine is properly set in case of error.
* ErrorEvent must contain a reference to an absolute line of the source code
* @param pgm The name of the program to be executed.
* @param throwableConsumer A consumer to handle the throwable, default is empty.
* @param additionalProgramFinders A list of additional program finders to be used during the execution, default is empty.
* @param reloadConfig The reload configuration to be used during the execution, default is null.
* */
protected fun executeSourceLineTest(
pgm: String,
throwableConsumer: (Throwable) -> Unit = {},
additionalProgramFinders: List<RpgProgramFinder> = emptyList(),
reloadConfig: ReloadConfig? = null
) {
val sourceIdToLines = mutableMapOf<String, List<String>>()
val errorEvents = mutableListOf<ErrorEvent>()
val configuration = Configuration().apply {
jarikoCallback.beforeParsing = { it ->
sourceIdToLines[MainExecutionContext.getParsingProgramStack().peek().name] = it.lines()
it
}
jarikoCallback.onError = {
errorEvents.add(it)
}
// I set dumpSourceOnExecutionError because I want test also the sourceLine presence in case
// of runtime error
options = Options(debuggingInformation = true, dumpSourceOnExecutionError = true)
this.reloadConfig = reloadConfig
}
kotlin.runCatching {
executePgm(pgm, configuration = configuration, additionalProgramFinders = additionalProgramFinders)
}.onSuccess {
Assert.fail("$pgm must exit with error")
}.onFailure {
throwableConsumer(it)
errorEvents.throwErrorIfUnknownSourceId()
errorEvents.forEach { errorEvent ->
// copy is not parsed so, if sourceReference is a copy, I will use the program name
val sourceId = if (errorEvent.sourceReference!!.sourceReferenceType == SourceReferenceType.Copy) {
pgm
} else {
errorEvent.sourceReference!!.sourceId
}
val lines = sourceIdToLines[sourceId]!!
if (lines[errorEvent.absoluteLine!! - 1] != errorEvent.fragment) {
System.err.println("ErrorEvent: $errorEvent")
System.err.println("Jariko arose an error at this line: ${errorEvent.absoluteLine!!}, fragment: ${errorEvent.fragment}")
System.err.println("But the source code at line: ${errorEvent.absoluteLine!!} is: ${lines[errorEvent.absoluteLine!! - 1]}")
fail()
}
}
}
}
}

private fun List<ErrorEvent>.throwErrorIfUnknownSourceId() {
if (this.any { errorEvent -> errorEvent.sourceReference!!.sourceId == "UNKNOWN" }) {
val errorEvent = this.first { errorEvent -> errorEvent.sourceReference!!.sourceId == "UNKNOWN" }
System.err.println("ErrorEvent.error:")
errorEvent.error.printStackTrace()
error("errorEvent: $errorEvent\nwith sourceId: UNKNOWN is not allowed")
}
}

fun Configuration.adaptForTestCase(testCase: AbstractTest): Configuration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ open class SmeupInterpreterTest : AbstractTest() {
jarikoCallback.onError =
{ errorEvent -> firstError = if (firstError == null) errorEvent.error else firstError }
}
javaClass.getResource("/ERROR28.rpgle").also { resource ->
javaClass.getResource("/smeup/ERROR28.rpgle").also { resource ->
require(resource != null) { "Resource not found: /ERROR28.rpgle" }
val path = File(resource.path).parentFile
val smeupPath = File(javaClass.getResource("/smeup")!!.path)
Expand All @@ -719,4 +719,22 @@ open class SmeupInterpreterTest : AbstractTest() {
assertNotNull(compilationError)
assertSame(firstError, compilationError)
}

@Test
fun executeERROR28SourceLineTest() {
executeSourceLineTest(
pgm = "smeup/ERROR28",
throwableConsumer = { throwable ->
System.err.println(throwable.message)
}
)
}

@Test
fun executeERROR29SourceLineTest() {
executeSourceLineTest(
pgm = "smeup/ERROR29",
reloadConfig = smeupConfig.reloadConfig
)
}
}
Loading

0 comments on commit 378bb1f

Please sign in to comment.