Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Introduce crate flow-test #114

Merged
merged 1 commit into from
Feb 12, 2024
Merged

test: Introduce crate flow-test #114

merged 1 commit into from
Feb 12, 2024

Conversation

jakoschiko
Copy link
Collaborator

@jakoschiko jakoschiko commented Jan 28, 2024

First attempt to introduce a test setup that allows to write tests conveniently.

My design constraints:

  • Test should have as few boilerplate as possible
    • We want to write many tests
  • ClientFlow and ServerFlow are tested separately
    • It's possible that both of them could have the same bug that can't be observed if tested simultaneously
    • There should be test setups for both of them
    • Each test setup is mocking the other side with very simple code
  • A test should not spawn any additional threads
    • This makes debugging easier
  • It should be possible to run multiple tests in parallel
    • This is the default for cargo test
    • Ports might be a problem
    • Tokio runtime might be a problem

@coveralls
Copy link
Collaborator

coveralls commented Jan 28, 2024

Pull Request Test Coverage Report for Build 7879436885

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 48.776%

Totals Coverage Status
Change from base Build 7879377050: 0.0%
Covered Lines: 518
Relevant Lines: 1062

💛 - Coveralls

test-setup/Cargo.toml Outdated Show resolved Hide resolved
test-setup/src/client_setup.rs Outdated Show resolved Hide resolved
test-setup/src/client_setup.rs Outdated Show resolved Hide resolved
test-setup/tests/client.rs Outdated Show resolved Hide resolved
@jakoschiko
Copy link
Collaborator Author

The current design looks like:

#[test]
fn simple_example() {
    let mut setup = ClientSetup::new(ClientFlowOptions::default());

    let greeting = setup.server_sends_greeting(b"* OK ...\n");
    setup.client_receives_greeting(greeting);

    let noop = setup.client_sends_command(b"A1 NOOP\n");
    setup.server_receives_command(noop);
}

All actions (send and receive) are sequential and there is no visible async/await in the test. This is very convenient, but it has a caveat: We assume that there is enough TCP buffer between server and client to buffer the message. If we send a very large message, it will fail:

#[test]
fn very_large_greeting() {
    let mut setup = ClientSetup::new(ClientFlowOptions::default());

    let mut gibberish = vec![b'x' as u8; 9999999];
    gibberish.push(b'\n');

    setup.server_sends_gibberish(&gibberish); // <- Timeout because send buffer is full
    setup.client_receives_error(); // <- This line is never executed
}

On my machine it's possible to send messages with up to ~200KB.

What should we do? I think there are 3 options:

  1. Keep the current design because it's sufficient for most tests.
  2. Keep the current design, but buffer all actions under the hood. The test then needs to call a method setup.process() to execute the client and server actions in parallel.
  3. Change the current design drastically. First the test needs to create all relevant Futures. Then the test joins and executes them with our Runtime (which is currently only an implementation detail of ClientSetup). The test would have more control, but would also be more verbose.

What do you think?

@jakoschiko
Copy link
Collaborator Author

jakoschiko commented Feb 8, 2024

I tried out finding a better design and came up with this:

#[test]
fn simple_example() {
    let (rt, mut server, mut client) = TestSetup::default().test_client();

    let greeting = b"* OK ...\r\n";
    rt.run2(server.send(greeting), client.receive_greeting(greeting));

    let noop = b"A1 NOOP\r\n";
    rt.run2(client.send_command(noop), server.receive(noop));

    let status = b"A1 OK ...\r\n";
    rt.run2(server.send(status), client.receive_status(status));
}

I really like the new design!

  • The required line count for a unit test has not changed
  • It works with arbitrarily large messages
  • It has less magic because ServerMock has only two methods now, send and receive

@jakoschiko jakoschiko changed the title test: Introduce crate test-setup test: Introduce crate flow-test Feb 8, 2024
@jakoschiko jakoschiko force-pushed the jakoschiko_test-setup branch from ada2252 to 4e84aca Compare February 9, 2024 12:59
@jakoschiko
Copy link
Collaborator Author

I think the current state is a good proof-of-concept that can be merged and it's ready to be reviewed.

@jakoschiko jakoschiko requested a review from duesee February 9, 2024 13:01
@jakoschiko jakoschiko force-pushed the jakoschiko_test-setup branch 2 times, most recently from 272bc6b to ea3fcd3 Compare February 9, 2024 21:47
@duesee duesee marked this pull request as ready for review February 12, 2024 14:31
Copy link
Owner

@duesee duesee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks simple! I like it, too. I made a few improvements (that you can take, drop, or question) and have a design question:

Design:

  • Using strings (instead of objects) makes the tests easier to read.
    • Do we expect to use way more complicated messages? They can be created from message.dump() so should still be fine.
  • We use a canonical representation for IMAP messages (which technically does not exist.) The types in imap-types are somehow the "canonical" representation, though. So, there is a chance we may get failing test because someone in imap-codec decided to use another "default" representation :-) This is mostly casing but there are a few instances of (item) (with parenthesis) v.s. item (w/o parenthesis). imap-codec (always?) opts for the shortest option. But it could happen that we need to change it due to a broken server or so. Anyway, I generally think it's fine.

@jakoschiko
Copy link
Collaborator Author

Do we expect to use way more complicated messages? They can be created from message.dump() so should still be fine.

Yes, we'll use more complicated messages. Also I want to generate random messages via arbitrary or dicetest. message.dump() is a good solution.

We use a canonical representation for IMAP messages (which technically does not exist.)

The mock is allowed to send non-canonical messages. And that's good because we might want to test that we can parse them (in this case the mock would represent a server/client which is not implemented via imap-codec).

The types in imap-types are somehow the "canonical" representation, though. So, there is a chance we may get failing test because someone in imap-codec decided to use another "default" representation :-)

I think that's okay because it will happen rarely.

@jakoschiko jakoschiko force-pushed the jakoschiko_test-setup branch from 18c6cc2 to 8f0130f Compare February 12, 2024 23:43
@jakoschiko jakoschiko enabled auto-merge (rebase) February 12, 2024 23:43
@jakoschiko jakoschiko merged commit 9f03d4f into main Feb 12, 2024
10 checks passed
@jakoschiko jakoschiko deleted the jakoschiko_test-setup branch February 12, 2024 23:49
@jakoschiko jakoschiko mentioned this pull request Feb 12, 2024
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants