Tests for std::process and remove receive_if

This adds tests for `std::process`, and removes
`std::process.receive_if`. The `receive_if` method has always been a bit
of a weird method. It was originally meant to allow some form of pattern
matching on messages, but this turned out to be less useful than hoped
due to the return type still being `Dynamic`.

The `timeout` argument was also not used properly, and solving this
would not be easily doable.  This is because there was no single
operation to apply the timeout to, instead the implementation of
`receive_if` contained two `receive` calls, and some other logic.
Handling all this would require us to keep track of the elapsed time
ourselves, which is rather tricky should the process be stuck in a
blocking receive.

Instead of trying to come up with all kinds of crazy ideas, I have opted
to simply remove `receive_if` for the time being. This makes some test
runner code a bit more fragile (in theory), but I doubt this will pose
any real problems any time soon.
parent 8747098f
Pipeline #39515082 passed with stages
in 12 minutes and 47 seconds
......@@ -1185,6 +1185,8 @@ module Inkoc
def on_raw_process_terminate_current(node, body)
body.instruct(:ProcessTerminateCurrent, node.location)
get_nil(body, node.location)
end
def on_raw_remove_attribute(node, body)
......
......@@ -96,6 +96,9 @@ object Sender!(T) {
object Receiver!(T) {
## Receives a message of type `T` sent to the current process.
##
## The returned value will be `Nil` if the supplied timeout expired before a
## message was received.
##
## # Examples
##
## Receiving a message using a `Sender` and `Receiver`:
......@@ -107,8 +110,8 @@ object Receiver!(T) {
##
## sender.send(10)
## receiver.receive # => 10
def receive(timeout: ToFloat = 0.0) -> T {
ThisModule.receive(timeout.to_float) as T
def receive(timeout: ToFloat = 0.0) -> ?T {
ThisModule.receive(timeout.to_float) as ?T
}
}
......@@ -171,76 +174,6 @@ def receive(timeout: ToFloat = 0.0) {
_INKOC.process_receive_message(timeout.to_float)
}
## Receives a message that matches a condition.
##
## The `condition` argument is a `Block` that takes a single argument, which is
## the message to test. If the `Block` returns `True`, then the message is
## returned by this method. If the `Block` returns `False`, the message is
## put back into the mailbox.
##
## If no messages match the condition, this method will suspend the current
## process for a brief period of time. The time the process will be suspended
## for can be controlled using the `recheck_interval` argument. This argument
## specifies the minimum suspension time in seconds.
##
## # Examples
##
## Receiving a message that matches our condition:
##
## import std::process
##
## process.send(pid: process.current, message: 20)
## process.send(pid: process.current, message: 10)
##
## let message = process.receive_if do (message) {
## message == 10
## }
##
## message # => 10
##
## Receiving a message with a timeout:
##
## import std::process
##
## process.send(pid: process.current, message: 10)
##
## let message = process.receive_if(timeout: 10, condition: do (message) {
## message == 10
## })
##
## message # => 10
##
## When supplying both the `timeout` and `condition` arguments, it is preferred
## to specify the `timeout` argument first.
def receive_if(
condition: do (Dynamic) -> Dynamic,
timeout: ToFloat = 0.0,
recheck_interval = 50,
) {
let first_message = receive(timeout)
let mut message = first_message
{
condition.call(message).if_true {
return message
}
send(pid: current, message: message)
message = receive(timeout)
# It's possible none of the messages in our mailbox match the given
# condition. This could lead to this process being suspended and resumed
# many times, using a lot of CPU time.
#
# To prevent this from happening we'll manually suspend ourselves for a
# brief period of time once we have checked all messages in the mailbox.
message.equal?(first_message).if_true {
suspend(recheck_interval)
}
}.loop
}
## Spawns a new process that will execute the given lambda.
##
## Processes are completely isolated and as such "self" in the lambda will refer
......@@ -375,7 +308,7 @@ def blocking!(R)(block: do -> R) -> R {
## Suspends the current process until it is rescheduled.
##
## The argument of this method can be used to set a minimum suspension time (in
## seconds). If no timeout is specified the process may be rescheduled at
## seconds). If no minimum time is specified the process may be rescheduled at
## any time.
##
## # Examples
......@@ -391,8 +324,8 @@ def blocking!(R)(block: do -> R) -> R {
## import std::process
##
## process.suspend(5) # => Nil
def suspend(timeout: ToFloat = 0.0) -> Nil {
_INKOC.process_suspend_current(timeout.to_float)
def suspend(minimum: ToFloat = 0.0) -> Nil {
_INKOC.process_suspend_current(minimum.to_float)
Nil
}
......@@ -412,9 +345,8 @@ def suspend(timeout: ToFloat = 0.0) -> Nil {
## # This code will never run because at this point the process has been
## # terminated.
## stdout.print('after')
def terminate -> Nil {
def terminate -> Void {
_INKOC.process_terminate_current
Nil
}
## Registers the given block as this process' panic handler.
......
......@@ -116,6 +116,8 @@ object RunTests impl Command {
## Runs all the tests currently registered, returning once they have been
## completed.
##
## Once this method has been called, no new tests can be registered.
def run(state: RunnerState) {
let mut pending = 0
let start_time = time.monotonic
......@@ -136,13 +138,7 @@ object RunTests impl Command {
.or { index == last_index }
.if_true {
{ pending > 0 }.while_true {
# We may receive messages of other types, such as newly registered
# tests. In practise this shouldn't happen, but technically it's
# possible. As such we use `process.receive_if` here, instead of
# `process.channel`.
let finished_test = process.receive_if do (msg) {
reflection.instance_of?(msg, Test)
} as Test
let finished_test = process.receive as Test
state.formatter.test(finished_test)
......@@ -231,6 +227,9 @@ object Client {
def init(sender: Sender!(Command)) {
## The Sender to use for communicating with the `Runner`.
let @sender = sender
## A boolean indicating if the test suite is running or not.
let mut @running = False
}
## Returns the PID of the `Runner`.
......@@ -240,16 +239,28 @@ object Client {
## Sets a configuration option to the given value.
def configure(option: String, value: Dynamic) {
@running.if_true {
process.panic(
'Configuration settings can not be changed for a running test suite'
)
}
@sender.send(ConfigureRunner.new(option, value))
}
## Adds a new test to the `Runner`.
def add_test(test: Test) {
@running.if_true {
process.panic('New tests can not be added to a running test suite')
}
@sender.send(AddTest.new(test))
}
## Schedules all tests for execution and waits for them to complete.
def run {
@running = True
@sender.send(RunTests.new(process.current))
wait_for_tests
......@@ -260,13 +271,7 @@ object Client {
## If any tests failed to run, this method will terminate the VM with exit
## status code 1.
def wait_for_tests {
# We have no control over what messages may be sent to the current process.
# This means we can't use `process.channel`, and instead have to make sure
# that the message we receive is the message we actually want.
let message = process.receive_if do (message) {
reflection.instance_of?(message, RunnerFinished)
.and { message.pid == @sender.pid as Boolean }
} as RunnerFinished
let message = process.receive as RunnerFinished
message.failures?.if_true {
vm.exit(FAILURE_EXIT_STATUS)
......
......@@ -28,6 +28,7 @@ import test::std::test_mirror
import test::std::test_nil
import test::std::test_object
import test::std::test_os
import test::std::test_process
import test::std::test_range
import test::std::test_string
import test::std::test_string_buffer
......
import std::process::(self, Receiver, Sender)
import std::test
import std::test::assert
import std::time::MonotonicTime
test.group('std::process::Sender.send') do (g) {
g.test('Sending a message to a process') {
let pid = process.spawn {
let parent = process.receive as Integer
process.send(pid: parent, message: parent)
}
Sender.new(pid).send(process.current)
assert.equal(process.receive as Integer, process.current)
}
}
test.group('std::process::Sender.pid') do (g) {
g.test('Obtaining a PID of the receiving end of a Sender') {
let sender = Sender.new(process.current)
assert.equal(sender.pid, process.current)
}
}
test.group('std::process::Receiver.receive') do (g) {
g.test('Receiving a message without a timeout') {
let receiver: Receiver!(Integer) = Receiver.new
process.send(pid: process.current, message: 10)
assert.equal(receiver.receive, 10)
}
g.test('Receiving a message with a timeout') {
let receiver: Receiver!(Integer) = Receiver.new
let message = receiver.receive(0.0001)
assert.equal(message, Nil)
}
}
test.group('std::process.current') do (g) {
g.test('Obtaining the PID of the current process') {
assert.true(process.current >= 0)
}
}
test.group('std::process.send') do (g) {
g.test('Sending a message to a process') {
let message = process.send(pid: process.current, message: 'testing')
let received = process.receive as String
assert.equal(message, 'testing')
assert.equal(received, message)
}
}
test.group('std::process.receive') do (g) {
g.test('Receiving a message without a timeout') {
process.send(pid: process.current, message: 'testing')
let received = process.receive as String
assert.equal(received, 'testing')
}
g.test('Receiving a message with a timeout') {
let received = process.receive(0.001) as ?String
assert.equal(received, Nil)
}
}
test.group('std::process.spawn') do (g) {
g.test('Spawning a process') {
let pid = process.spawn {}
assert.true(pid >= 0)
}
}
test.group('std::process.channel') do (g) {
g.test('Sending and receiving messages using a Sender and Receiver') {
let sender = process.channel!(Integer) lambda (receiver) {
let pid = *receiver.receive
process.send(pid: pid, message: pid)
}
sender.send(process.current)
assert.equal(process.receive as Integer, process.current)
}
}
test.group('std::process.blocking') do (g) {
g.test('Performing a blocking operation') {
assert.equal(process.blocking({ 10 }), 10)
}
}
test.group('std::process.suspend') do (g) {
g.test('Suspending a process') {
assert.equal(process.suspend, Nil)
}
g.test('Suspending a process for a minimum amount of time') {
let start = MonotonicTime.new
let wait = 0.01
process.suspend(wait)
let duration = (MonotonicTime.new - start).to_float
assert.true(duration >= wait)
}
}
test.group('std::process.terminate') do (g) {
g.test('Terminating the current process') {
let pid = process.spawn {
let parent = process.receive as Integer
process.terminate
# This code will never run, unless `process.terminate` somehow doesn't
# terminate the current process.
process.send(pid: parent, message: parent)
}
process.send(pid: pid, message: process.current)
# Only if `process.terminate` _does not_ terminate the process will we
# receive a message.
let message = process.receive(0.01) as ?Integer
assert.equal(message, Nil)
}
}
test.group('std::process.panicking') do (g) {
g.test('Registering a custom panic handler') {
let pid = process.spawn {
let parent = process.receive as Integer
process.panicking do (error) {
process.send(pid: parent, message: error)
}
process.panic('example panic')
}
process.send(pid: pid, message: process.current)
let error = process.receive as String
assert.equal(error, 'example panic')
}
}
test.group('std::process.defer') do (g) {
g.test('Deferring the execution of a Block') {
let mut number = 0
do {
process.defer {
# This will be executed _after_ `number = 1` below.
number = 2
}
number = 1
}.call
assert.equal(number, 2)
}
}
test.group('std::process.pinned') do (g) {
g.test('Pinning a process to an OS thread') {
# There is no reliable way of testing whether we are truly pinned, without
# using some sort of FFI example that uses thread-local storage. Since that
# is far too much to test here, we'll just test that the block returns the
# proper value.
assert.equal(process.pinned({ 10 }), 10)
}
}
test.group('std::process.panic') do (g) {
g.test('Causing a process to panic') {
assert.panic {
process.panic('This is a panic')
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment