Add assert-related testing functions, findFirst/findFirstOrNull methods, and expand documentation.
This commit is contained in:
parent
99f883cfc7
commit
3ac7fd7ceb
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
94
docs/Testing.md
Normal 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.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package lyng.stdlib
|
package lyng.stdlib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Wrap a builder into a zero-argument thunk that computes once and caches the result.
|
Wrap a builder into a zero-argument thunk that computes once and caches the result.
|
||||||
The first call invokes builder() and stores the value; subsequent calls return the cached value.
|
The first call invokes builder() and stores the value; subsequent calls return the cached value.
|
||||||
*/
|
*/
|
||||||
fun cached(builder) {
|
fun cached(builder) {
|
||||||
var calculated = false
|
var calculated = false
|
||||||
@ -37,9 +37,33 @@ fun Iterable.drop(n) {
|
|||||||
fun Iterable.first() {
|
fun Iterable.first() {
|
||||||
val i = iterator()
|
val i = iterator()
|
||||||
if( !i.hasNext() ) throw NoSuchElementException()
|
if( !i.hasNext() ) throw NoSuchElementException()
|
||||||
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
|
||||||
@ -49,7 +73,7 @@ fun Iterable.last() {
|
|||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
if( !found ) throw NoSuchElementException()
|
if( !found ) throw NoSuchElementException()
|
||||||
element
|
element
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Emit all but the last N elements of this iterable. */
|
/* Emit all but the last N elements of this iterable. */
|
||||||
@ -172,10 +196,10 @@ fun List.sort() {
|
|||||||
|
|
||||||
/* Represents a single stack trace element. */
|
/* Represents a single stack trace element. */
|
||||||
class StackTraceEntry(
|
class StackTraceEntry(
|
||||||
val sourceName: String,
|
val sourceName: String,
|
||||||
val line: Int,
|
val line: Int,
|
||||||
val column: Int,
|
val column: Int,
|
||||||
val sourceString: String
|
val sourceString: String
|
||||||
) {
|
) {
|
||||||
/* Formatted representation: source:line:column: text. */
|
/* Formatted representation: source:line:column: text. */
|
||||||
fun toString() {
|
fun toString() {
|
||||||
@ -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) }
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user