Skip to content

Commit

Permalink
Add PonyCheck to standard library (#4034)
Browse files Browse the repository at this point in the history
Closes #4029
  • Loading branch information
SeanTAllen authored Feb 24, 2022
1 parent a75350f commit f9b837e
Show file tree
Hide file tree
Showing 22 changed files with 5,281 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ task:
arm_container:
image: ponylang/ponyc-ci-aarch64-unknown-linux-ubuntu20.04-builder:20211003
cpu: 8
memory: 4
memory: 6

environment:
IMAGE: ponylang/ponyc-ci-aarch64-unknown-linux-ubuntu20.04-builder:20211003
Expand Down
27 changes: 27 additions & 0 deletions .release-notes/add-ponycheck-to-stdlib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Add PonyCheck to the standard library

PonyCheck, a property based testing library, has been added to the standard library. PonyCheck was previously its own project but has been merged into the standard library.

For the most part existing PonyCheck tests will continue to work once you make a few minor changes.

### Remove PonyCheck from your corral.json

As PonyCheck is now part of the standard library, you don't need to use `corral` to fetch it.

### Change the package name

Previously, the PonyCheck package was called `ponycheck`. To conform with standard library naming conventions, the package has been renamed to `pony_check`. So anywhere you previously had:

```pony
use "ponycheck"
```

You'll need to update to:

```pony
use "pony_check"
```

### Update the PonyCheck primitive name

If you were using the `Ponycheck` primitive, you'll need to change it to its new name `PonyCheck`.
1 change: 1 addition & 0 deletions examples/pony_check/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pony_check
56 changes: 56 additions & 0 deletions examples/pony_check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# pony_check

A program showing example tests using the PonyCheck property based testing package.

## How to compile

With a minimal Pony installation, in the same directory as this README file, run `ponyc`. You should see content building the necessary packages, which ends with:

```console
...
Generating
Reachability
Selector painting
Data prototypes
Data types
Function prototypes
Functions
Descriptors
Optimising
Writing ./pony_check.o
Linking ./pony_check
```

## How to Run

Once `pony_check` has been compiled, in the same directory as this README file, run `./pony_check`. You should see a PonyTest runner output showing the tests run and their results; just like you would with PonyTest in general, except the tests in question are PonyCheck tests.

```console
1 test started, 0 complete: list/reverse/one started
2 tests started, 0 complete: list/properties started
3 tests started, 0 complete: list/reverse started
4 tests started, 0 complete: custom_class/map started
5 tests started, 0 complete: custom_class/custom_generator started
6 tests started, 0 complete: async/tcp_sender started
7 tests started, 0 complete: collections/operation_on_random_collection_elements started
8 tests started, 0 complete: custom_class/flat_map started
8 tests started, 1 complete: list/properties complete
8 tests started, 2 complete: custom_class/flat_map complete
8 tests started, 3 complete: custom_class/custom_generator complete
8 tests started, 4 complete: custom_class/map complete
8 tests started, 5 complete: list/reverse/one complete
8 tests started, 6 complete: collections/operation_on_random_collection_elements complete
8 tests started, 7 complete: list/reverse complete
8 tests started, 8 complete: async/tcp_sender complete
---- Passed: list/reverse
---- Passed: list/reverse/one
---- Passed: list/properties
---- Passed: custom_class/flat_map
---- Passed: custom_class/map
---- Passed: custom_class/custom_generator
---- Passed: async/tcp_sender
---- Passed: collections/operation_on_random_collection_elements
----
---- 8 tests ran.
---- Passed: 8
```
134 changes: 134 additions & 0 deletions examples/pony_check/async_tcp_property.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use "itertools"
use "net"
use "pony_check"
use "ponytest"

class _TCPSenderConnectionNotify is TCPConnectionNotify
let _message: String

new create(message: String) =>
_message = message

fun ref connected(conn: TCPConnection ref) =>
conn.write(_message)

fun ref connect_failed(conn: TCPConnection ref) =>
None

fun ref received(
conn: TCPConnection ref,
data: Array[U8] iso,
times: USize)
: Bool
=>
conn.close()
true

class val TCPSender
"""
Class under test.
Simple class that sends a string to a TCP server.
"""

let _auth: AmbientAuth

new val create(auth: AmbientAuth) =>
_auth = auth

fun send(
host: String,
port: String,
message: String): TCPConnection tag^
=>
TCPConnection(
_auth,
recover _TCPSenderConnectionNotify(message) end,
host,
port)


// Test Cruft
class MyTCPConnectionNotify is TCPConnectionNotify
let _ph: PropertyHelper
let _expected: String

new create(ph: PropertyHelper, expected: String) =>
_ph = ph
_expected = expected

fun ref received(
conn: TCPConnection ref,
data: Array[U8] iso,
times: USize)
: Bool
=>
_ph.log("received " + data.size().string() + " bytes", true)
// assert we received the expected string
_ph.assert_eq[USize](data.size(), _expected.size())
for bytes in Iter[U8](_expected.values()).zip[U8]((consume data).values()) do
_ph.assert_eq[U8](bytes._1, bytes._2)
end
// this will signal to the PonyCheck engine that this property is done
// it will nonetheless execute until the end
_ph.complete(true)
conn.close()
true

fun ref connect_failed(conn: TCPConnection ref) =>
_ph.fail("connect failed")
conn.close()

class MyTCPListenNotify is TCPListenNotify

let _sender: TCPSender
let _ph: PropertyHelper
let _expected: String

new create(
sender: TCPSender,
ph: PropertyHelper,
expected: String) =>
_sender = sender
_ph = ph
_expected = expected


fun ref listening(listen: TCPListener ref) =>
let address = listen.local_address()
try
(let host, let port) = address.name(where reversedns = None, servicename = false)?

// now that we know the server's address we can actually send something
_ph.dispose_when_done(
_sender.send(host, port, _expected))
else
_ph.fail("could not determine server host and port")
end

fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ =>
recover iso
MyTCPConnectionNotify(_ph, _expected)
end

fun ref not_listening(listen: TCPListener ref) =>
_ph.fail("not listening")

class _AsyncTCPSenderProperty is Property1[String]
fun name(): String => "async/tcp_sender"

fun params(): PropertyParams =>
PropertyParams(where async' = true, timeout' = 5_000_000_000)

fun gen(): Generator[String] =>
Generators.unicode()

fun ref property(sample: String, ph: PropertyHelper) =>
let sender = TCPSender(ph.env.root)
ph.dispose_when_done(
TCPListener(
ph.env.root,
recover MyTCPListenNotify(sender, ph, "PONYCHECK") end,
"127.0.0.1",
"0"))

91 changes: 91 additions & 0 deletions examples/pony_check/collection_generators.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
This example shows a rather complex scenario.
Here we want to generate both a random size that we use as the length of an array that
we create in our property and a random number of operations on random elements
of the array.
We don't want to use another source of randomness for determining a random element
in our property code, as it is better for reproducability of the property
to make the whole execution only depend on the randomness used when drawing samples
from the Generator.
This way, given a certain seed in the `PropertyParams` we can actually reliably reproduce a failed example.
*/
use "collections"
use "pony_check"

class val _OperationOnCollection[T, R = String] is Stringable
"""Represents a certain operation on an element addressed by idx."""
let idx: USize
let op: {(T): R} val

new val create(idx': USize, op': {(T): R} val) =>
idx = idx'
op = op'

fun string(): String iso^ =>
recover
String.>append("_OperationOnCollection(" + idx.string() + ")")
end

class _OperationOnCollectionProperty is Property1[(USize, Array[_OperationOnCollection[String]])]
fun name(): String => "collections/operation_on_random_collection_elements"

fun gen(): Generator[(USize, Array[_OperationOnCollection[String]])] =>
"""
This generator produces:
* a tuple of the number of elements of a collection to be created in the property code
* an array of a randomly chosen operation on a random element of the collection
Therefore, we first create a generator for the collection size,
then we `flat_map` over this generator, to generate one for an array of operations,
whose index is randomly chosen from a range of `[0, num_elements)`.
The first generated value determines the further values, we want to construct a generator
based on the value of another one. For this kind of construct, we use `Generator.flat_map`.
We then `flat_map` again over the Generator for the element index (that is bound by `num_elements`)
to get a generator for an `_OperationOnCollection[String]` which needs the index as a constructor argument.
The operation generator depends on the value of the randomly chosen element index,
thus we use `Generator.flat_map` again.
"""
Generators.usize(2, 100).flat_map[(USize, Array[_OperationOnCollection[String]])](
{(num_elements: USize) =>
let elements_generator =
Generators.array_of[_OperationOnCollection[String]](
Generators.usize(0, num_elements-1)
.flat_map[_OperationOnCollection[String]]({(element) =>
Generators.one_of[_OperationOnCollection[String]](
[
_OperationOnCollection[String](element, {(s) => recover String.>append(s).>append("foo") end })
_OperationOnCollection[String](element, {(s) => recover String.>append(s).>append("bar") end })
]
)
})
)
Generators.zip2[USize, Array[_OperationOnCollection[String]]](
Generators.unit[USize](num_elements),
elements_generator)
})

fun ref property(sample: (USize, Array[_OperationOnCollection[String]]), h: PropertyHelper) =>
(let len, let ops) = sample

// create and fill the array
let coll = Array[String].create(len)
for i in Range(0, len) do
coll.push(i.string())
end

// execute random operations on random elements of the array
for op in ops.values() do
try
let elem = coll(op.idx)?
let res = op.op(elem)
if not h.assert_true(res.contains("foo") or res.contains("bar")) then return end
else
h.fail("illegal access")
end
end

Loading

0 comments on commit f9b837e

Please sign in to comment.