published to our maven

This commit is contained in:
sergeych committed 2025-06-13 19:11:28 +04:00
1 parent 185aa4e0cf
commit 8c6a1979ed
39 files changed
+89 -104

No files matched your search

+276
View File
@@ -0,0 +1,276 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Context
import net.sergeych.lyng.ObjVoid
import java.nio.file.Files
import java.nio.file.Files.readAllLines
import java.nio.file.Paths
import kotlin.io.path.absolutePathString
import kotlin.io.path.extension
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
fun leftMargin(s: String): Int {
var cnt = 0
for (c in s) {
when (c) {
' ' -> cnt++
'\t' -> cnt = (cnt / 4.0 + 0.9).toInt() * 4
else -> break
}
}
return cnt
}
data class DocTest(
val fileName: String,
val line: Int,
val code: String,
val expectedOutput: String,
val expectedResult: String,
val expectedError: String? = null,
val bookMode: Boolean = false
) {
val sourceLines by lazy { code.lines() }
val fileNamePart by lazy {
Paths.get(fileName).fileName.toString()
}
override fun toString(): String {
val absPath = Paths.get(fileName).absolutePathString()
return "DocTest: $absPath:${line + 1}"
}
val detailedString by lazy {
val codeWithLines = sourceLines.withIndex().map { (i, s) -> "${i + line + 1}: $s" }.joinToString("\n")
var result = "$this\n$codeWithLines\n"
if( !bookMode) {
if (expectedOutput.isNotBlank())
result += "--------expected output--------\n$expectedOutput\n"
"$result-----expected return value-----\n$expectedResult"
}
else result
}
}
fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> = flow {
val book = readAllLines(Paths.get(fileName))
var startOffset = 0
val block = mutableListOf<String>()
var startIndex = 0
for ((index, l) in book.withIndex()) {
val off = leftMargin(l)
when {
off < startOffset && startOffset != 0 -> {
if (l.isBlank()) {
continue
}
// end of block or just text:
if (block.isNotEmpty()) {
// check/create block
// 2 lines min
if (block.size > 1) {
// remove prefix
for ((i, s) in block.withIndex()) {
var x = s
// could be tabs :(
val initial = leftMargin(x)
do {
x = x.drop(1)
} while (initial - leftMargin(x) != startOffset)
block[i] = x
}
if (bookMode) {
emit(
DocTest(
fileName, startIndex,
block.joinToString("\n"),
"",
"",
null,
bookMode = true
)
)
}
// println(block.joinToString("\n") { "${startIndex + ii++}: $it" })
val outStart = block.indexOfFirst { it.startsWith(">>>") }
if (outStart < 0) {
// println("No output at block from line ${startIndex+1}")
} else {
var isValid = true
val result = mutableListOf<String>()
while (block.size > outStart) {
val line = block.removeAt(outStart)
if (!line.startsWith(">>> ")) {
println("invalid output line, must start with '>>> ', block from ${startIndex + 1}: $line")
isValid = false
break
}
result.add(line.drop(4))
}
if (isValid) {
emit(
DocTest(
fileName, startIndex,
block.joinToString("\n"),
if (result.size > 1)
result.dropLast(1).joinToString("") { it + "\n" }
else "",
result.last()
)
)
}
}
// last line '>>>'
}
block.clear()
startOffset = 0
}
}
off != 0 && startOffset == 0 -> {
// start
block.clear()
startIndex = index
block.add(l)
startOffset = off
}
off != 0 -> {
block.add(l)
}
off == 0 && startOffset == 0 -> {
// skip
}
else -> {
throw RuntimeException("Unexpected line: ($off/$startOffset) $l")
}
}
}
}
.flowOn(Dispatchers.IO)
suspend fun DocTest.test(context: Context = Context()) {
val collectedOutput = StringBuilder()
val currentTest = this
context.apply {
addFn("println") {
if( bookMode ) {
println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}")
}
else {
for ((i, a) in args.withIndex()) {
if (i > 0) collectedOutput.append(' '); collectedOutput.append(a.asStr.value)
collectedOutput.append('\n')
}
}
ObjVoid
}
}
var error: Throwable? = null
val result = try {
context.eval(code)
} catch (e: Throwable) {
error = e
null
}?.inspect()?.replace(Regex("@\\d+"), "@...")
if (bookMode) {
if (error != null) {
println("Sample failed: ${this.detailedString}")
fail("book sample failed", error)
}
} else {
if (error != null || expectedOutput != collectedOutput.toString() ||
expectedResult != result
) {
System.err.println("\nfailed: ${this.detailedString}")
}
error?.let {
fail(it.message, it)
}
assertEquals(expectedOutput, collectedOutput.toString(), "script output do not match")
assertEquals(expectedResult, result.toString(), "script result does not match")
// println("OK: $this")
}
}
suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
val bookContext = Context()
var count = 0
parseDocTests(fileName, bookMode).collect { dt ->
if (bookMode) dt.test(bookContext)
else dt.test()
count++
}
print("completed mdtest: $fileName; ")
if (bookMode)
println("fragments processed: $count")
else
println("tests passed: $count")
}
class BookTest {
@Test
fun testsFromTutorial() = runTest {
runDocTests("../docs/tutorial.md")
}
@Test
fun testsFromMath() = runTest {
runDocTests("../docs/math.md")
}
@Test
fun testsFromAdvanced() = runTest {
runDocTests("../docs/advanced_topics.md")
}
@Test
fun testsFromOOP() = runTest {
runDocTests("../docs/OOP.md")
}
@Test
fun testFromReal() = runTest {
runDocTests("../docs/Real.md")
}
@Test
fun testFromList() = runTest {
runDocTests("../docs/List.md")
}
@Test
fun testFromRange() = runTest {
runDocTests("../docs/Range.md")
}
@Test
fun testSampleBooks() = runTest {
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
if (bt.extension == "md") {
runDocTests(bt.toString(), bookMode = true)
}
}
}
@Test
fun testArgumentBooks() = runTest {
runDocTests("../docs/declaring_arguments.md")
}
@Test
fun testExceptionsBooks() = runTest {
runDocTests("../docs/exceptions_handling.md")
}
}
+34
View File
@@ -0,0 +1,34 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import net.sergeych.lyng.Context
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.extension
import kotlin.test.Test
suspend fun executeSampleTests(fileName: String) {
val sample = withContext(Dispatchers.IO) {
Files.readString(Paths.get(fileName))
}
runBlocking {
val c = Context()
val start = Clock.System.now()
c.eval(sample)
val time = Clock.System.now() - start
println("$time: $fileName")
// delay(100)
// }
}
}
class SamplesTest {
@Test
fun testSamples() = runBlocking {
for (s in Files.list(Paths.get("../docs/samples"))) {
if (s.extension == "lyng") executeSampleTests(s.toString())
}
}
}