Add assert-related testing functions, findFirst/findFirstOrNull methods, and expand documentation.

This commit is contained in:
Sergey Chernov 2025-12-21 19:11:37 +01:00
parent 99f883cfc7
commit 3ac7fd7ceb
8 changed files with 193 additions and 16 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ xcuserdata
/sample_texts/1.txt.gz /sample_texts/1.txt.gz
/kotlin-js-store/wasm/yarn.lock /kotlin-js-store/wasm/yarn.lock
/distributables /distributables
/.output.txt

View File

@ -39,6 +39,7 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
- [Language home](https://lynglang.com) - [Language home](https://lynglang.com)
- [introduction and tutorial](docs/tutorial.md) - start here please - [introduction and tutorial](docs/tutorial.md) - start here please
- [Testing and Assertions](docs/Testing.md)
- [Samples directory](docs/samples) - [Samples directory](docs/samples)
- [Formatter (core + CLI + IDE)](docs/formatter.md) - [Formatter (core + CLI + IDE)](docs/formatter.md)
- [Books directory](docs) - [Books directory](docs)

View File

@ -81,6 +81,21 @@ Used to transform either the whole iterable stream or also skipping som elements
>>> void >>> void
## findFirst and findFirstOrNull
Search for the first element that satisfies the given predicate:
val source = [1, 2, 3, 4]
assertEquals( 2, source.findFirst { it % 2 == 0 } )
assertEquals( 2, source.findFirstOrNull { it % 2 == 0 } )
// findFirst throws if not found:
assertThrows( NoSuchElementException ) { source.findFirst { it > 10 } }
// findFirstOrNull returns null if not found:
assertEquals( null, source.findFirstOrNull { it > 10 } )
>>> void
## Instance methods: ## Instance methods:
@ -96,6 +111,8 @@ Used to transform either the whole iterable stream or also skipping som elements
| map(f) | create a list of values returned by `f` called for each element of the iterable | | map(f) | create a list of values returned by `f` called for each element of the iterable |
| indexOf(i) | return index if the first encounter of i or a negative value if not found | | indexOf(i) | return index if the first encounter of i or a negative value if not found |
| associateBy(kf) | create a map where keys are returned by kf that will be called for each element | | associateBy(kf) | create a map where keys are returned by kf that will be called for each element |
| findFirst(p) | return first element matching predicate `p` or throw (1) |
| findFirstOrNull(p) | return first element matching predicate `p` or `null` |
| first | first element (1) | | first | first element (1) |
| last | last element (1) | | last | last element (1) |
| take(n) | return [Iterable] of up to n first elements | | take(n) | return [Iterable] of up to n first elements |
@ -129,6 +146,8 @@ optional function applied to each item that must return result string for an ite
fun Iterable.forEach(block: (Any?)->Void ): Void fun Iterable.forEach(block: (Any?)->Void ): Void
fun Iterable.map(block: (Any?)->Void ): List fun Iterable.map(block: (Any?)->Void ): List
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
fun Iterable.findFirst( predicate: (Any?)->Bool): Any
fun Iterable.findFirstOrNull( predicate: (Any?)->Bool): Any?
## Abstract methods: ## Abstract methods:

94
docs/Testing.md Normal file
View File

@ -0,0 +1,94 @@
# Testing and Assertions
Lyng provides several built-in functions for testing and verifying code behavior. These are available in all scripts.
## Basic Assertions
### `assert`
Assert that a condition is true.
assert(condition, message=null)
- `condition`: A boolean expression.
- `message` (optional): A string message to include in the exception if the assertion fails.
If the condition is false, it throws an `AssertionFailedException`.
```lyng
assert(1 + 1 == 2)
assert(true, "This should be true")
```
### `assertEquals` and `assertEqual`
Assert that two values are equal. `assertEqual` is an alias for `assertEquals`.
assertEquals(expected, actual)
assertEqual(expected, actual)
If `expected != actual`, it throws an `AssertionFailedException` with a message showing both values.
```lyng
assertEquals(4, 2 * 2)
assertEqual("hello", "hel" + "lo")
```
### `assertNotEquals`
Assert that two values are not equal.
assertNotEquals(unexpected, actual)
If `unexpected == actual`, it throws an `AssertionFailedException`.
```lyng
assertNotEquals(5, 2 * 2)
```
## Exception Testing
### `assertThrows`
Assert that a block of code throws an exception.
assertThrows(code)
assertThrows(expectedExceptionClass, code)
- `expectedExceptionClass` (optional): The class of the exception that is expected to be thrown.
- `code`: A lambda block or statement to execute.
If the code does not throw an exception, an `AssertionFailedException` is raised.
If an `expectedExceptionClass` is provided, the thrown exception must be of that class (or its subclass), otherwise an error is raised.
`assertThrows` returns the caught exception object if successful.
```lyng
// Just assert that something is thrown
assertThrows { 1 / 0 }
// Assert that a specific exception class is thrown
assertThrows(NoSuchElementException) {
[1, 2, 3].findFirst { it > 10 }
}
// You can use the returned exception
val ex = assertThrows { throw Exception("custom error") }
assertEquals("custom error", ex.message)
```
## Other Validation Functions
While not strictly for testing, these functions help in defensive programming:
### `require`
require(condition, message="requirement not met")
Throws an `IllegalArgumentException` if the condition is false. Use this for validating function arguments.
### `check`
check(condition, message="check failed")
Throws an `IllegalStateException` if the condition is false. Use this for validating internal state.

View File

@ -9,6 +9,7 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.md) - [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md) - [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md), [the `when` statement](when.md) - [math in Lyng](math.md), [the `when` statement](when.md)
- [Testing and Assertions](Testing.md)
- [time](time.md) and [parallelism](parallelism.md) - [time](time.md) and [parallelism](parallelism.md)
- [parallelism] - multithreaded code, coroutines, etc. - [parallelism] - multithreaded code, coroutines, etc.
- Some class - Some class
@ -329,7 +330,7 @@ will be thrown:
// WRONG! Exception will be thrown at next line: // WRONG! Exception will be thrown at next line:
foo + "bar" foo + "bar"
Correct pattern is: The correct pattern is:
foo = "foo" foo = "foo"
// now is OK: // now is OK:
@ -478,6 +479,8 @@ one could be with ellipsis that means "the rest pf arguments as List":
### Using lambda as the parameter ### Using lambda as the parameter
See also: [Testing and Assertions](Testing.md)
// note that fun returns its last calculated value, // note that fun returns its last calculated value,
// in our case, result after in-place addition: // in our case, result after in-place addition:
fun mapValues(iterable, transform) { fun mapValues(iterable, transform) {
@ -1466,6 +1469,8 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
[List]: List.md [List]: List.md
[Testing]: Testing.md
[Iterable]: Iterable.md [Iterable]: Iterable.md
[Iterator]: Iterator.md [Iterator]: Iterator.md

View File

@ -204,7 +204,19 @@ class Script(
) )
} }
addFn("assertThrows") { addFn("assertThrows") {
val code = requireOnlyArg<Statement>() val code: Statement
val expectedClass: ObjClass?
when(args.size) {
1 -> {
code = requiredArg<Statement>(0)
expectedClass = null
}
2 -> {
code = requiredArg<Statement>(1)
expectedClass = requiredArg<ObjClass>(0)
}
else -> raiseIllegalArgument("Expected 1 or 2 arguments, got ${args.size}")
}
val result = try { val result = try {
code.execute(this) code.execute(this)
null null
@ -213,7 +225,15 @@ class Script(
} catch (_: ScriptError) { } catch (_: ScriptError) {
ObjNull ObjNull
} }
result ?: raiseError(ObjAssertionFailedException(this, "Expected exception but nothing was thrown")) if( result == null ) raiseError(ObjAssertionFailedException(this, "Expected exception but nothing was thrown"))
expectedClass?.let {
if( result !is ObjException)
raiseError("Expected $expectedClass, got $result")
if (result.exceptionClass != expectedClass) {
raiseError("Expected $expectedClass, got ${result.exceptionClass}")
}
}
result
} }
addFn("dynamic") { addFn("dynamic") {

View File

@ -4270,4 +4270,18 @@ class ScriptTest {
assertEquals(51, r.toInt()) assertEquals(51, r.toInt())
} }
@Test
fun testFirstInEnum() = runTest {
eval("""
enum E {
one, two, three
}
println(E.entries)
assertEquals( E.two, E.entries.findFirst {
println(it.name)
it.name in ["aaa", "two"]
} )
""".trimIndent())
}
} }

View File

@ -40,6 +40,30 @@ fun Iterable.first() {
i.next().also { i.cancelIteration() } i.next().also { i.cancelIteration() }
} }
/*
Return the first element that matches the predicate or throws
NuSuchElementException
*/
fun Iterable.findFirst(predicate) {
for( x in this ) {
if( predicate(x) )
break x
}
else throw NoSuchElementException()
}
/*
return the first element matching the predicate or null
*/
fun Iterable.findFirstOrNull(predicate) {
for( x in this ) {
if( predicate(x) )
break x
}
else null
}
/* Return the last element or throw if the iterable is empty. */ /* Return the last element or throw if the iterable is empty. */
fun Iterable.last() { fun Iterable.last() {
var found = false var found = false
@ -194,4 +218,3 @@ fun Exception.printStackTrace() {
/* Compile this string into a regular expression. */ /* Compile this string into a regular expression. */
fun String.re(): Regex { Regex(this) } fun String.re(): Regex { Regex(this) }