Skip to content

Commit

Permalink
Merge pull request #55 from ferrous-systems/self-check-project
Browse files Browse the repository at this point in the history
Self-check project exercise
  • Loading branch information
jonathanpallant authored Jan 8, 2024
2 parents 6b77ffe + 0a407d3 commit 27e56d3
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 1 deletion.
4 changes: 4 additions & 0 deletions exercise-book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
- [Connected Mailbox](./connected-mailbox.md)
- [Multithreaded mailbox](./multi-threaded-mailbox.md)

# Self-check Project

- [Self-check project](./self-check.md)

# Embedded Rust on the nRF52

- [Preparation](./nrf52-preparation.md)
Expand Down
229 changes: 229 additions & 0 deletions exercise-book/src/self-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<!-- markdownlint-disable MD033 MD014 -->
# Self-Check Project

*This exercise is intended for you to check your Rust knowledge. It is based on our other exercises, so you can follow them one by one instead of attempting to do everything in one go.*

In this exercise we will create a small in-memory message queue that is accessible over a TCP connection and uses a plain text format for its protocol. The protocol has two commands to put a message into the back of the queue and to read a message from the front of the queue. When a user sends a "PUBLISH" we will push the data into a queue, and when the user sends a "RETRIEVE" we will pop some data off the queue (if any is available). The user will connect via TCP to port 7878. Multiple clients can add and remove messages from the queue at the same time.

## After completing this exercise you are able to

- write a Rust binary that uses a Rust library

- combine two Rust packages into a Cargo workspace

- open a TCP port and perform an action when each user connects

- use I/O traits to read/write from a TCP socket

- create a safe protocol parser in Rust manually

- interact with borrowed and owned memory, especially how to take ownership

- handle complex cases using the `match` and `if let` syntax

- handle errors using `Result` and custom error types

- spawn threads

- convert a non-thread-safe type into a thread-safe-type

- lock a Mutex to access the data within

## Tasks

1. Create a Cargo workspace for your project.
2. Create a binary package inside your workspace for your TCP server
3. Implement a simple TCP Server that listens for connections on `127.0.0.1:7878`. For each incoming connection, read all of the input as a string, and send it back to the client.
4. Create a library package inside your workspace for a message protocol parser. Make your TCP server depend on that library using a relative path.
5. Inside your library implement the following function so that it implements the protocol specifications to parse the messages. Use the provided tests to help you with the case handling.

```rust, ignore
pub fn parse(input: &str) -> Result<Command, Error> {
todo!()
}
```
6. Change your TCP Server to *use* your parser crate to parse the input, and provide an appropriate canned response.
7. Set up a `VecDeque` queue and either *push* or *pop* from that queue, depending on the command you have received.
8. Add support for multiple simultaneous client connections using threads. Make sure all clients read and write to the same shared queue.
### Optional Tasks
- Allow each connection to read input line by line as a sequence of commands and execute them in the same order as they come in. This way you should be able to open several connections in terminal and type commands in them one by one.
- Run `cargo fmt` on your codebase.
- Run `cargo clippy` on your codebase.
### Protocol Specification
The protocol has two commands that are sent as messages in the following
form:
- `PUBLISH <payload>\n`
- `RETRIEVE\n`
With the additional properties:
1. The payload cannot contain newlines.
2. A missing newline at the end of the command is an error.
3. Data after the first newline is an error.
4. Empty payloads are allowed. In this case, the command is
`PUBLISH \n`.
Violations against the form of the messages and the properties are
handled with the following error codes:
- `TrailingData` (bytes found after newline)
- `IncompleteMessage` (no newline)
- `EmptyMessage` (empty string instead of a command)
- `UnknownCommand` (string is not empty, but neither `PUBLISH` nor
`RECEIVE`)
- `UnexpectedPayload` (message contains a payload, when it should not)
- `MissingPayload` (message is missing a payload)
### Testing
Below are the tests your protocol parser needs to pass. You can copy them to the bottom of your `lib.rs`.
```rust, ignore
#[cfg(test)]
mod tests {
use super::*;
// Tests placement of \n
#[test]
fn test_missing_nl() {
let line = "RETRIEVE";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::IncompleteMessage);
assert_eq!(result, expected);
}
#[test]
fn test_trailing_data() {
let line = "PUBLISH The message\n is wrong \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::TrailingData);
assert_eq!(result, expected);
}
#[test]
fn test_empty_string() {
let line = "";
let result = parse(line);
let expected = Err(Error::IncompleteMessage);
assert_eq!(result, expected);
}
// Tests for empty messages and unknown commands
#[test]
fn test_only_nl() {
let line = "\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::EmptyMessage);
assert_eq!(result, expected);
}
#[test]
fn test_unknown_command() {
let line = "SERVE \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnknownCommand);
assert_eq!(result, expected);
}
// Tests correct formatting of RETRIEVE command
#[test]
fn test_retrieve_w_whitespace() {
let line = "RETRIEVE \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnexpectedPayload);
assert_eq!(result, expected);
}
#[test]
fn test_retrieve_payload() {
let line = "RETRIEVE this has a payload\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnexpectedPayload);
assert_eq!(result, expected);
}
#[test]
fn test_retrieve() {
let line = "RETRIEVE\n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Retrieve);
assert_eq!(result, expected);
}
// Tests correct formatting of PUBLISH command
#[test]
fn test_publish() {
let line = "PUBLISH TestMessage\n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Publish("TestMessage".into()));
assert_eq!(result, expected);
}
#[test]
fn test_empty_publish() {
let line = "PUBLISH \n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Publish("".into()));
assert_eq!(result, expected);
}
#[test]
fn test_missing_payload() {
let line = "PUBLISH\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::MissingPayload);
assert_eq!(result, expected);
}
}
```

## Help

### Connecting over TCP/IP

#### Using `nc`, `netcat` or `ncat`

The `nc`, `netcat`, or `ncat` tools may be available on your macOS or Linux machine, or on WSL on windows. They all work in a similar fashion.

```console
$ echo "PUBLISH 1234" | nc 127.0.0.1 7878
```

The `echo` command adds a new-line character automatically. Use `echo -n` if you don't want it to add a new-line character.

#### Using our TCP Client

We have written a basic TCP Client which should work on any platform.

```console
$ cd tools/tcp-client
$ cargo run -- "PUBLISH hello"
$ cargo run -- "RETRIEVE"
```

It automatically adds a *newline* character on to the end of every message you send. It is hard-coded to connect to a server at `127.0.0.1:7878`.

### Solution

This exercise is based on three other exercises. Check their solutions below:

- [Simple-DB](../../exercise-solutions/simple-db)
- [Connected Mailbox](../../exercise-solutions/connected-mailbox)
- [Multithreaded mailbox](../../exercise-solutions/multi-threaded-mailbox)
2 changes: 1 addition & 1 deletion exercise-book/src/simple-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ With the additional properties:
Violations against the form of the messages and the properties are
handled with the following error codes:

- `TrailingData` (more than one newline)
- `TrailingData` (bytes found after newline)

- `IncompleteMessage` (no newline)

Expand Down

0 comments on commit 27e56d3

Please sign in to comment.