libparsec
(i.e. code in./src
): crate exposed to the outside world. The JS bindings (i.e.../bindings
) access the content of this crate.libparsec_*
(i.e. code in./crates/
): internal crates,libparsec
itself mostly re-expose them.
Notes:
- Currently the CLI relies on the internal
libparsec_*
crates instead of thelibparsec
. This is for historical reasons and should eventually be fixed.- For simplicity, internal crates don't have their
libparsec_
suffix in the folder hierarchy (e.g.libparsec_types
is located at./crates/types
).
The client is the component that runs a device. It is currently divided into two crates:
client_connection
: Handles connection to the server.client
: Contains everything else: invitation & enrollment, certificate & manifest validation, workspace operations, monitors running background tasks, etc.
Notes:
The
client
crate can be seen as the "main" crate implementing most of the functional logic of the application.
The base types can be divided into:
- What is used to communicate between the client and the server.
We call this the "protocol" and it is defined in
protocol/schema
. - What is used only by the client, but ends up serialized on disk.
We call this simply the "data" and it is defined in
types/schema
. - What is used only internally (i.e. in-memory and never serialized).
The key point here is backward/forward compatibility as serialized data are produced and consumed by different version of Parsec.
protocol
: Implements the protocol, and the API version.types
: Implements the data, and the types that are only for internal use.serialization_format
: Defines the format used for serialization (for both protocol and data).
Notes:
serialization_format
also provides a macro to generate Python bindings for the protocol (used by the server). Note this code is beyond mere mortal comprehension, debugging requires sacrificing your soul to Satan.
Crates with platform in their name have different implementations depending on the platform they are compiled for.
For instance libparsec_platform_mountpoint
uses FUSE on UNIX, WinFSP on Windows,
and is not compatible with web.
The corollary is that crates without platform
in their name are platform-independent,
and hence can be tested on a single platform.
The platform crates:
platform_async
: Asynchronous programming, works differently between web and native (the former has to integrate with the javascript runtime, the latter uses Tokio), so this crate exposes platform-agnostic utilities (e.g. spawn a future, async lock etc.).platform_device_loader
: Loads the device keys file from disk.platform_http_proxy
: Handles HTTP proxy.platform_ipc
: Allows communication between processes to ensure a device is not concurrently used.platform_mountpoint
: FUSE/WinFSP to expose the workspaces as a OS drive.platform_storage
: Database to store on disk manifest/block/certificates etc.
Here, "testing" refers to two use-cases:
- Running tests on a crate (i.e.
cargo test -p my_crate
) - Running the application with test feature (e.g. we want to run the GUI with an dummy organization already configured for quick testing purpose)
testbed
:tests_macros
: Defines the#[parsec_test]
macro that is used to decorate each test function. Among other things, this macro conveniently handles setting up Tokio context (so that our test can be an asynchronous function) and the testbed (configuring a test organization, and starting a testbed server if needed).tests_lite
: Re-expose utilities useful to write tests (e.g.pretty_assertions
,rstest
, theparsec_test
macro etc.). This is a lighter version oftests_fixture
that is needed to teststypes
crate (sincetests_fixture
internally usestypes
crate).tests_fixtures
: This crate re-expose the content oftest_lite
and provides additional helpers such as temporary folder fixture, injection hook, testbed types etc.
Notes:
- From an external point of view, the
tests_fixture
re-expose everything needed. So when writing tests,use libparsec_tests_fixtures::prelude::*
is the single-no-brainer-one-liner™.tests_fixtures
's name is mostly an historical artifact, as it contains far more than only fixtures !
crypto
Implements all the crypto algorithms (signing, encryption, hashing etc.). There are two implementations for those: Rustcrypto (pure Rust) and Sodiumoxide (based on libsodium).zstd
: That's a funny one, you should have a look at its README ;-)
Stick to the following rule:
- All tests are unit tests.
- All test code must be in
tests/unit
(this works by using#[path = "../../tests/unit/my_test.rs"]
to overwrite the test module location). - Try to mimic the first-level hierarchy of
src
in thetests/unit
(e.g.src/foo.rs
->tests/unit/foo.rs
).
In Rust there is concept of unit vs integration testing:
- Unit test are within the tested crate.
- Integration are code outside the tested crate.
"But I don't care about your “crate” concept! Defining a test as unit or integration should depend of what it does, not where it comes from !"
Well tough luck! That's how Rust works, so you'd better play along ¯\(ツ)/¯
More seriously, this behavior comes from the fact the crate is the unit of compilation in Rust. Hence a "unit" test is simply code added to the crate, which allows it to access private components.
In practice this cause the following issues:
- It's complicated to know where to put new tests. For instance two related tests can end up in two separate places because one need access to internal and not the other (with additional fun if you want them to share helpers !).
- An integration test might need to be moved to unit when adding to it an additional check (if the check access internals).
- It's very common to access internals when testing (e.g. define a dump method for test only that returns an internal type).
- Unit tests code is located in the
src
directory and might even be inlined with the code to test. This add a lot of noise when reading the code :(
So to solve this we stick to the "all test code is unit test located in tests/unit
folder".
The drawback to this approach is that it increases compilation time (because the crate is the unit of compilation in Rust, remember? ^^) since 1) all the test code is part of the crate and 2) modifying a test recompile this big crate instead of a very small crate depending on a bigger crate that haven't changed.