rename rust/ to taskchampion/

This commit is contained in:
Dustin J. Mitchell 2022-07-10 16:27:52 +00:00 committed by Tomas Babej
parent ccb9a0fdfb
commit 12ecfa2b1e
161 changed files with 15 additions and 15 deletions

View file

@ -0,0 +1,5 @@
[advisories]
ignore = [
"RUSTSEC-2020-0159", # segfault in localtime_r - low risk to TC
"RUSTSEC-2020-0071", # same localtime_r bug as above
]

View file

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

0
taskchampion/.changelogs/.gitignore vendored Normal file
View file

View file

@ -0,0 +1,2 @@
- The SQLite server storage schema has changed incompatibly, in order to add support for snapshots.
As this is not currently ready for production usage, no migration path is provided except deleting the existing database.

View file

@ -0,0 +1,2 @@
- The `avoid_snapshots` configuration value, if set, will cause the replica to
avoid creating snapshots unless required.

View file

@ -0,0 +1 @@
- The encryption format used for synchronization has changed incompatibly

View file

@ -0,0 +1 @@
- The details of how task start/stop is represented have changed. Any existing tasks will all be treated as inactive (stopped).

1
taskchampion/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
**/*.rs.bk

26
taskchampion/CHANGELOG.md Normal file
View file

@ -0,0 +1,26 @@
# Changelog
## [Unreleased]
Note: unreleased change log entries are kept in `.changelogs/` directory in repo root, and can be added with `./script/changelog.py add "Added thing for reason"
## 0.4.1 - 2021-09-24
- Fix for the build process to include the serde feature "derive". 0.4.0 could not be published to crates.io due to this bug.
## 0.4.0 - 2021-09-25
- Breaking: Removed the KV based storage backend in client and server, and replaced with SQLite ([Issue #131](https://github.com/taskchampion/taskchampion/issues/131), [PR #206](https://github.com/taskchampion/taskchampion/pull/206))
## 0.3.0 - 2021-01-11
- Flexible named reports
- Updates to the TaskChampion crate API
- Usability improvements
## 0.2.0 - 2020-11-30
This release is the first "MVP" version of this tool. It can do basic task operations, and supports a synchronization. Major missing features are captured in issues, but briefly:
better command-line API, similar to TaskWarrior
authentication of the replica / server protocol
encryption of replica data before transmission to the server
lots of task features (tags, annotations, dependencies, ..)
lots of CLI features (filtering, modifying, ..)

View file

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at dustin@cs.uchicago.edu. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View file

@ -0,0 +1,65 @@
# Welcome
This application is still in a pre-release state.
That means it's very open to contributions, and we'd love to have your help!
It also means that things are changing quickly, and lots of stuff is planned that's not quite done yet.
If you would like to work on TaskChampion, please contact the developers (via the issue tracker) before spending a lot of time working on a pull request.
Doing so may save you some wasted time and frustration!
A good starting point might be one of the issues tagged with ["good first issue"][first].
[first]: https://github.com/taskchampion/taskchampion/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
# Other Ways To Help
The best way to help this project to grow is to help spread awareness of it.
Tell your friends, post to social media, blog about it -- whatever works best!
Other ideas;
* Improve the documentation where it's unclear or lacking some information
* Build and maintain tools that integrate with TaskChampion
# Development Guide
TaskChampion is a typical Rust application.
To work on TaskChampion, you'll need to [install the latest version of Rust](https://www.rust-lang.org/tools/install).
Once you've done that, run `cargo build` at the top level of this repository to build the binaries.
This will build `task` and `taskchampion-sync-server` executables in the `./target/debug` directory.
You can build optimized versions of these binaries with `cargo build --release`, but the performance difference in the resulting binaries is not noticeable, and the build process will take a long time, so this is not recommended.
## Running Test
It's always a good idea to make sure tests run before you start hacking on a project.
Run `cargo test` from the top-level of this repository to run the tests.
## Read the Source
Aside from that, start reading the docs and the source to learn more!
The book documentation explains lots of the concepts in the design of TaskChampion.
It is linked from the README.
There are three crates in this repository.
You may be able to limit the scope of what you need to understand to just one crate.
* `taskchampion` is the core functionality of the application, implemented as a library
* `taskchampion-cli` implements the command-line interface (in `cli/`)
* `taskchampion-sync-server` implements the synchronization server (in `sync-server/`)
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
## Making a Pull Request
We expect contributors to follow the [GitHub Flow](https://guides.github.com/introduction/flow/).
Aside from that, we have no particular requirements on pull requests.
Make your patch, double-check that it's complete (tests? docs? documentation comments?), and make a new pull request.
Any non-trivial change (particularly those that change the behaviour of the application, or change the API) should be noted in the projects changelog.
In order to manage this, changelog entries are stored as text files in the `.changelog/` directory at the repository root.
To add a new changelog entry, you can simply run `python3 ./script/changelog.py add "Fixed thingo to increase zorbloxification [Issue #2](http://example.com)`
This creates a file named `./changelogs/yyyy-mm-dd-branchname.md` (timestamp, current git branch) which contains a markdown snippet.
If you don't have a Python 3 intepreter installed, you can simply create this file manually. It should contain a list item like `- Fixed thingo [...]`
Periodically (probably just before release), these changelog entries are concatenated combined together and added into the `CHANGELOG.md` file.

21
taskchampion/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Dustin J. Mitchell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
taskchampion/POLICY.md Normal file
View file

@ -0,0 +1,45 @@
# Compatibility & deprecation
Until TaskChampion reaches [v1.0.0](https://github.com/taskchampion/taskchampion/milestone/7), nothing is set in stone. That being said, we aim for the following:
1. Major versions represent significant change and may be incompatible with previous major release.
2. Minor versions are always backwards compatible and might add some new functionality.
3. Patch versions should not introduce any new functionality and do what name implies — fix bugs.
As there are no major releases yet, we do not support any older versions. Users are encouraged to use the latest release.
## ABI policy
1. We target stable `rustc`.
2. TaskChampion will never upgrade any storage to a non-compatible version without explicit user's request.
## API policy
1. Deprecated features return a warning at least 1 minor version prior to being removed.
Example:
> If support of `--bar` is to be dropped in v2.0.0, we shall announce it in v1.9.0 at latest.
2. We aim to issue a notice of newly added functionality when appropriate.
Example:
> "NOTICE: Since v1.1.0 you can use `--foo` in conjunction with `--bar`. Foobar!"
3. TaskChampion always uses UTF-8.
## Command-line interface
Considered to be part of the API policy.
## CLI exit codes
- `0` - No errors, normal exit.
- `1` - Generic error.
- `2` - Never used to avoid conflicts with Bash.
- `3` - Command-line Syntax Error.
# Security
See [SECURITY.md](./SECURITY.md).

52
taskchampion/README.md Normal file
View file

@ -0,0 +1,52 @@
TaskChampion
------------
TaskChampion is an open-source personal task-tracking application.
Use it to keep track of what you need to do, with a quick command-line interface and flexible sorting and filtering.
It is modeled on [TaskWarrior](https://taskwarrior.org), but not a drop-in replacement for that application.
See the [documentation](https://taskchampion.github.io/taskchampion/) for more!
## Status
TaskChampion currently functions as a "testbed" for new functionality that may later be incorporated into TaskWarrior.
It can be developed without the requirements of compatibliity, allowing us to explore and fix edge-cases in things like the replica-synchronization model.
While you are welcome to [help out](https://github.com/taskchampion/taskchampion/blob/main/CONTRIBUTING.md), you should do so with the awareness that your work might never be used.
But, if you just want to get some practice with Rust, we'd be happy to have you.
## Structure
There are five crates here:
* [taskchampion](./taskchampion) - the core of the tool
* [taskchampion-cli](./cli) - the command-line binary
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
* [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C
* [integration-tests](./integration-tests) - integration tests covering _taskchampion-cli_, _taskchampion-sync-server_, and _taskchampion-lib_.
## Code Generation
The _taskchampion_lib_ crate uses a bit of code generation to create the `lib/taskchampion.h` header file.
To regenerate this file, run `cargo xtask codegen`.
## C libraries
NOTE: support for linking against taskchampion is a work in progress.
Contributions and pointers to best practices are appreciated!
The `taskchampion-lib` crate generates libraries suitable for use from C (or any C-compatible language).
The necessary bits are:
* a shared object in `target/$PROFILE/deps` (e.g., `target/debug/deps/libtaskchampion.so`)
* a static library in `target/$PROFILE` (e.g., `target/debug/libtaskchampion.a`)
* a header file, `lib/taskchampion.h`.
Downstream consumers may use either the static or dynamic library, as they prefer.
NOTE: on Windows, the "BCrypt" library must be included when linking to taskchampion.
### As a Rust dependency
If you would prefer to build Taskchampion directly into your project, and have a build system capable of building Rust libraries (such as CMake), the `taskchampion-lib` crate can be referenced as an `rlib` dependency.

17
taskchampion/RELEASING.md Normal file
View file

@ -0,0 +1,17 @@
# Release process
1. Ensure the changelog is updated with everything from the `.changelogs` directory. `python3 ./scripts/changelog.py build` will output a Markdown snippet to include in `CHANGELOG.md` then `rm .changelog/*.txt`
1. Run `git pull upstream main`
1. Run `cargo test`
1. Run `cargo clean && cargo clippy`
1. Run `mdbook test docs`
1. Update `version` in `*/Cargo.toml`. All versions should match.
1. Run `cargo build --release`
1. Commit the changes (Cargo.lock will change too) with comment `vX.Y.Z`.
1. Run `git tag vX.Y.Z`
1. Run `git push upstream`
1. Run `git push --tags upstream`
1. Run `( ./build-docs.sh )`
1. Run `(cd taskchampion; cargo publish)` (note that the other crates do not get published)
1. Navigate to the tag in the GitHub releases UI and create a release with general comments about the changes in the release
1. Upload `./target/release/task` and `./target/release/task-sync-server` to the release

11
taskchampion/SECURITY.md Normal file
View file

@ -0,0 +1,11 @@
# Security
To report a vulnerability, please contact [dustin@cs.uchicago.edu](dustin@cs.uchicago.edu), you may use GPG public-key `D8097934A92E4B4210368102FF8B7AC6154E3226` which is available [here](https://keybase.io/djmitche/pgp_keys.asc?fingerprint=d8097934a92e4b4210368102ff8b7ac6154e3226). Initial response is expected within ~48h.
We kindly ask to follow the responsible disclosure model and refrain from sharing information until:
1. Vulnerabilities are patched in TaskChampion + 60 days to coordinate with distributions.
2. 90 days since the vulnerability is disclosed to us.
We recognise the legitimacy of public interest and accept that security researchers can publish information after 90-days deadline unilaterally.
We will assist with obtaining CVE and acknowledge the vulnerabilites reported.

33
taskchampion/build-docs.sh Executable file
View file

@ -0,0 +1,33 @@
#! /bin/bash
set -x
REMOTE=origin
set -e
if ! [ -f "docs/src/SUMMARY.md" ]; then
echo "Run this from the root of the repo"
exit 1
fi
# create a worktree of this repo, with the `gh-pages` branch checked out
git branch -f gh-pages $REMOTE/gh-pages
if ! [ -d ./docs/tmp ]; then
git worktree add -f docs/tmp gh-pages
fi
# update the wortree
(cd docs/tmp && git pull $REMOTE gh-pages)
# remove all files in the worktree and regenerate the book there
git worktree remove -f docs/tmp
rm -rf docs/tmp/*
mdbook build docs
mkdir docs/tmp
cp -rp docs/book/* docs/tmp
# add everything in the worktree, commit, and push
(cd docs/tmp && git add -A)
(cd docs/tmp && git commit -am "update docs")
(cd docs/tmp && git push $REMOTE gh-pages:gh-pages)

2
taskchampion/docs/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
book
tmp

View file

@ -0,0 +1,3 @@
This is an [mdbook](https://rust-lang.github.io/mdBook/index.html) book.
Minor modifications can be made without installing the mdbook tool, as the content is simple Markdown.
Changes are verified on pull requests.

View file

@ -0,0 +1,2 @@
Copyright (C) Andrew Savchenko - All Rights Reserved
All files within this folder are proprietary and reserved for the use by TaskChampion project.

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -0,0 +1,9 @@
[book]
authors = ["Dustin J. Mitchell"]
language = "en"
multilingual = false
src = "src"
title = "TaskChampion"
[output.html]
default-theme = "ayu"

View file

@ -0,0 +1,15 @@
# Summary
- [Welcome to TaskChampion](./welcome.md)
* [Installation](./installation.md)
* [Running the Sync Server](./running-sync-server.md)
- [Internal Details](./internals.md)
* [Data Model](./data-model.md)
* [Replica Storage](./storage.md)
* [Task Database](./taskdb.md)
* [Tasks](./tasks.md)
* [Synchronization and the Sync Server](./sync.md)
* [Synchronization Model](./sync-model.md)
* [Snapshots](./snapshots.md)
* [Server-Replica Protocol](./sync-protocol.md)
* [Planned Functionality](./plans.md)

View file

@ -0,0 +1,5 @@
# Data Model
A client manages a single offline instance of a single user's task list, called a replica.
This section covers the structure of that data.
Note that this data model is visible only on the client; the server does not have access to client data.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,3 @@
# Installation
As this is currently in development, installation is by cloning the repository and running "cargo build".

View file

@ -0,0 +1,5 @@
# Internal Details
The following sections get into the details of how TaskChampion works.
None of this information is necessary to use TaskChampion, but might be helpful in understanding its behavior.
Developers of TaskChampion and of tools that integrate with TaskChampion should be familiar with this information.

View file

@ -0,0 +1,35 @@
# Planned Functionality
This section is a bit of a to-do list for additional functionality to add to the synchronzation system.
Each feature has some discussion of how it might be implemented.
## Snapshots
As designed, storage required on the server would grow with time, as would the time required for new clients to update to the latest version.
As an optimization, the server also stores "snapshots" containing a full copy of the task database at a given version.
Based on configurable heuristics, it may delete older operations and snapshots, as long as enough data remains for active clients to synchronize and for new clients to initialize.
Since snapshots must be computed by clients, the server may "request" a snapshot when providing the latest version to a client.
This request comes with a number indicating how much it 'wants" the snapshot.
Clients which can easily generate and transmit a snapshot should be generous to the server, while clients with more limited resources can wait until the server's requests are more desperate.
The intent is, where possible, to request snapshots created on well-connected desktop clients over mobile and low-power clients.
## Encryption and Signing
From the server's perspective, all data except for version numbers are opaque binary blobs.
Clients encrypt and sign these blobs using a symmetric key known only to the clients.
This secures the data at-rest on the server.
Note that privacy is not complete, as the server still has some information about users, including source and frequency of synchronization transactions and size of those transactions.
## Backups
In this design, the server is little more than an authenticated storage for encrypted blobs provided by the client.
To allow for failure or data loss on the server, clients are expected to cache these blobs locally for a short time (a week), along with a server-provided HMAC signature.
When data loss is detected -- such as when a client expects the server to have a version N or higher, and the server only has N-1, the client can send those blobs to the server.
The server can validate the HMAC and, if successful, add the blobs to its datastore.
## Expiration
Deleted tasks remain in the task database, and are simply hidden in most views.
All tasks have an expiration time after which they may be flushed, preventing unbounded increase in task database size.
However, purging of a task does not satisfy the necessary OT guarantees, so some further formal design work is required before this is implemented.

View file

@ -0,0 +1,11 @@
# Running the Sync Server
> NOTE: TaskChampion is still in development and not yet feature-complete.
> The server is functional, but lacks any administrative features.
Run `taskchampion-sync-server` to start the sync server.
Use `--port` to specify the port it should listen on, and `--data-dir` to specify the directory which it should store its data.
It only serves HTTP; the expectation is that a frontend proxy will be used for HTTPS support.
The server has optional parameters `--snapshot-days` and `--snapshot-version`, giving the target number of days and versions, respectively, between snapshots of the client state.
The default values for these parameters are generally adequate.

View file

@ -0,0 +1,48 @@
# Snapshots
The basic synchronization model described in the previous page has a few shortcomings:
* servers must store an ever-increasing quantity of versions
* a new replica must download all versions since the beginning in order to derive the current state
Snapshots allow TaskChampion to avoid both of these issues.
A snapshot is a copy of the task database at a specific version.
It is created by a replica, encrypted, and stored on the server.
A new replica can simply download a recent snapshot and apply any additional versions synchronized since that snapshot was made.
Servers can delete and reclaim space used by older versions, as long as newer snapshots are available.
## Snapshot Heuristics
A server implementation must answer a few questions:
* How often should snapshots be made?
* When can versions be deleted?
* When can snapshots be deleted?
A critical invariant is that at least one snapshot must exist for any database that does not have a child of the nil version.
This ensures that a new replica can always derive the latest state.
Aside from that invariant, the server implementation can vary in its answers to these questions, with the following considerations:
Snapshots should be made frequently enough that a new replica can initialize quickly.
Existing replicas will fail to synchronize if they request a child version that has been deleted.
This failure can cause data loss if the replica had local changes.
It's conceivable that replicas may not sync for weeks or months if, for example, they are located on a home computer while the user is on holiday.
## Requesting New Snapshots
The server requests snapshots from replicas, indicating an urgency for the request.
Some replicas, such as those running on PCs or servers, can produce a snapshot even at low urgency.
Other replicas, in more restricted environments such as mobile devices, will only produce a snapshot at high urgency.
This saves resources in these restricted environments.
A snapshot must be made on a replica with no unsynchronized operations.
As such, it only makes sense to request a snapshot in response to a successful AddVersion request.
## Handling Deleted Versions
When a replica requests a child version, the response must distinguish two cases:
1. No such child version exists because the replica is up-to-date.
1. No such child version exists because it has been deleted, and the replica must re-initialize itself.
The details of this logic are covered in the [Server-Replica Protocol](./sync-protocol.md).

View file

@ -0,0 +1,83 @@
# Replica Storage
Each replica has a storage backend.
The interface for this backend is given in `crate::taskstorage::Storage` and `StorageTxn`.
The storage is transaction-protected, with the expectation of a serializable isolation level.
The storage contains the following information:
- `tasks`: a set of tasks, indexed by UUID
- `base_version`: the number of the last version sync'd from the server (a single integer)
- `operations`: all operations performed since base_version
- `working_set`: a mapping from integer -> UUID, used to keep stable small-integer indexes into the tasks for users' convenience. This data is not synchronized with the server and does not affect any consistency guarantees.
## Tasks
The tasks are stored as an un-ordered collection, keyed by task UUID.
Each task in the database has represented by a key-value map.
See [Tasks](./tasks.md) for details on the content of that map.
## Operations
Every change to the task database is captured as an operation.
In other words, operations act as deltas between database states.
Operations are crucial to synchronization of replicas, described in [Synchronization Model](./sync-model.md).
Operations are entirely managed by the replica, and some combinations of operations are described as "invalid" here.
A replica must not create invalid operations, but should be resilient to receiving invalid operations during a synchronization operation.
Each operation has one of the forms
* `Create(uuid)`
* `Delete(uuid, oldTask)`
* `Update(uuid, property, oldValue, newValue, timestamp)`
* `UndoPoint()`
The Create form creates a new task.
It is invalid to create a task that already exists.
Similarly, the Delete form deletes an existing task.
It is invalid to delete a task that does not exist.
The `oldTask` property contains the task data from before it was deleted.
The Update form updates the given property of the given task, where the property and values are strings.
The `oldValue` gives the old value of the property (or None to create a new property), while `newValue` gives the new value (or None to delete a property).
It is invalid to update a task that does not exist.
The timestamp on updates serves as additional metadata and is used to resolve conflicts.
### Application
Each operation can be "applied" to a task database in a natural way:
* Applying `Create` creates a new, empty task in the task database.
* Applying `Delete` deletes a task, including all of its properties, from the task database.
* Applying `Update` modifies the properties of a task.
* Applying `UndoPoint` does nothing.
### Undo
Each operation also contains enough information to reverse its application:
* Undoing `Create` deletes a task.
* Undoing `Delete` creates a task, including all of the properties in `oldTask`.
* Undoing `Update` modifies the properties of a task, reverting to `oldValue`.
* Undoing `UndoPoint` does nothing.
The `UndoPoint` operation serves as a marker of points in the operation sequence to which the user might wish to undo.
For example, creation of a new task with several properities involves several operations, but is a single step from the user's perspective.
An "undo" command reverses operations, removing them from the operations sequence, until it reaches an `UndoPoint` operation.
### Synchronizing Operations
After operations are synchronized to the server, they can no longer be undone.
As such, the [synchronization model](./sync-model.md) uses simpler operations.
Replica operations are converted to sync operations as follows:
* `Create(uuid)` -> `Create(uuid)` (no change)
* `Delete(uuid, oldTask)` -> `Delete(uuid)`
* `Update(uuid, property, oldValue, newValue, timestamp)` -> `Update(uuid, property, newValue, timestamp)`
* `UndoPoint()` -> Ø (dropped from operation sequence)
Once a sequence of operations has been synchronized, there is no need to store those operations on the replica.
The current implementation deletes operations at that time.
An alternative approach is to keep operations for existing tasks, and provide access to those operations as a "history" of modifications to the task.

View file

@ -0,0 +1,138 @@
# Synchronization Model
The [task database](./taskdb.md) also implements synchronization.
Synchronization occurs between disconnected replicas, mediated by a server.
The replicas never communicate directly with one another.
The server does not have access to the task data; it sees only opaque blobs of data with a small amount of metadata.
The synchronization process is a critical part of the task database's functionality, and it cannot function efficiently without occasional synchronization operations
## Operational Transforms
Synchronization is based on [operational transformation](https://en.wikipedia.org/wiki/Operational_transformation).
This section will assume some familiarity with the concept.
## State and Operations
At a given time, the set of tasks in a replica's storage is the essential "state" of that replica.
All modifications to that state occur via operations, as defined in [Replica Storage](./storage.md).
We can draw a network, or graph, with the nodes representing states and the edges representing operations.
For example:
```text
o -- State: {abc-d123: 'get groceries', priority L}
|
| -- Operation: set abc-d123 priority to H
|
o -- State: {abc-d123: 'get groceries', priority H}
```
For those familiar with distributed version control systems, a state is analogous to a revision, while an operation is analogous to a commit.
Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create.
Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on.
The synchronization process uses operational transformation to "linearize" those operations.
This process is analogous (vaguely) to rebasing a sequence of Git commits.
### Sync Operations
The [Replica Storage](./storage.md) model contains additional information in its operations that is not included in operations synchronized to other replicas.
In this document, we will be discussing "sync operations" of the form
* `Create(uuid)`
* `Delete(uuid)`
* `Update(uuid, property, value, timestamp)`
### Versions
Occasionally, database states are given a name (that takes the form of a UUID).
The system as a whole (all replicas) constructs a branch-free sequence of versions and the operations that separate each version from the next.
The version with the nil UUID is implicitly the empty database.
The server stores the operations to change a state from a "parent" version to a "child" version, and provides that information as needed to replicas.
Replicas use this information to update their local task databases, and to generate new versions to send to the server.
Replicas generate a new version to transmit local changes to the server.
The changes are represented as a sequence of operations with the state resulting from the final operation corresponding to the version.
In order to keep the versions in a single sequence, the server will only accept a proposed version from a replica if its parent version matches the latest version on the server.
In the non-conflict case (such as with a single replica), then, a replica's synchronization process involves gathering up the operations it has accumulated since its last synchronization; bundling those operations into a version; and sending that version to the server.
### Replica Invariant
The replica's [storage](./storage.md) contains the current state in `tasks`, the as-yet un-synchronized operations in `operations`, and the last version at which synchronization occurred in `base_version`.
The replica's un-synchronized operations are already reflected in its local `tasks`, so the following invariant holds:
> Applying `operations` to the set of tasks at `base_version` gives a set of tasks identical
> to `tasks`.
### Transformation
When the latest version on the server contains operations that are not present in the replica, then the states have diverged.
For example:
```text
o -- version N
w|\a
o o
x| \b
o o
y| \c
o o -- replica's local state
z|
o -- version N+1
```
(diagram notation: `o` designates a state, lower-case letters designate operations, and versions are presented as if they were numbered sequentially)
In this situation, the replica must "rebase" the local operations onto the latest version from the server and try again.
This process is performed using operational transformation (OT).
The result of this transformation is a sequence of operations based on the latest version, and a sequence of operations the replica can apply to its local task database to reach the same state
Continuing the example above, the resulting operations are shown with `'`:
```text
o -- version N
w|\a
o o
x| \b
o o
y| \c
o o -- replica's intermediate local state
z| |w'
o-N+1 o
a'\ |x'
o o
b'\ |y'
o o
c'\|z'
o -- version N+2
```
The replica applies w' through z' locally, and sends a' through c' to the server as the operations to generate version N+2.
Either path through this graph, a-b-c-w'-x'-y'-z' or a'-b'-c'-w-x-y-z, must generate *precisely* the same final state at version N+2.
Careful selection of the operations and the transformation function ensure this.
See the comments in the source code for the details of how this transformation process is implemented.
## Synchronization Process
To perform a synchronization, the replica first requests the child version of `base_version` from the server (GetChildVersion).
It applies that version to its local `tasks`, rebases its local `operations` as described above, and updates `base_version`.
The replica repeats this process until the server indicates no additional child versions exist.
If there are no un-synchronized local operations, the process is complete.
Otherwise, the replica creates a new version containing its local operations, giving its `base_version` as the parent version, and transmits that to the server (AddVersion).
In most cases, this will succeed, but if another replica has created a new version in the interim, then the new version will conflict with that other replica's new version and the server will respond with the new expected parent version.
In this case, the process repeats.
If the server indicates a conflict twice with the same expected base version, that is an indication that the replica has diverged (something serious has gone wrong).
## Servers
A replica depends on periodic synchronization for performant operation.
Without synchronization, its list of pending operations would grow indefinitely, and tasks could never be expired.
So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically.
TaskChampion provides a `LocalServer` for this purpose.
It implements the `get_child_version` and `add_version` operations as described, storing data on-disk locally, all within the `ta` binary.

View file

@ -0,0 +1,232 @@
# Server-Replica Protocol
The server-replica protocol is defined abstractly in terms of request/response transactions from the replica to the server.
This is made concrete in an HTTP representation.
The protocol builds on the model presented in the previous chapter, and in particular on the synchronization process.
## Clients
From the server's perspective, replicas accessing the same task history are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history.
Each client is identified and authenticated with a "client key", known only to the server and to the replicas replicating the task history.
## Server
For each client, the server is responsible for storing the task history, in the form of a branch-free sequence of versions.
It also stores the latest snapshot, if any exists.
* versions: a set of {versionId: UUID, parentVersionId: UUID, historySegment: bytes}
* latestVersionId: UUID
* snapshotVersionId: UUID
* snapshot: bytes
For each client, it stores a set of versions as well as the latest version ID, defaulting to the nil UUID.
Each version has a version ID, a parent version ID, and a history segment (opaque data containing the operations for that version).
The server should maintain the following invariants for each client:
1. latestVersionId is nil or exists in the set of versions.
2. Given versions v1 and v2 for a client, with v1.versionId != v2.versionId and v1.parentVersionId != nil, v1.parentVersionId != v2.parentVersionId.
In other words, versions do not branch.
3. If snapshotVersionId is nil, then there is a version with parentVersionId == nil.
4. If snapshotVersionId is not nil, then there is a version with parentVersionId = snapshotVersionId.
Note that versions form a linked list beginning with the latestVersionId stored for the client.
This linked list need not continue back to a version with v.parentVersionId = nil.
It may end at any point when v.parentVersionId is not found in the set of Versions.
This observation allows the server to discard older versions.
The third invariant prevents the server from discarding versions if there is no snapshot.
The fourth invariant prevents the server from discarding versions newer than the snapshot.
## Data Formats
### Encryption
The client configuration includes an encryption secret of arbitrary length and a clientId to identify itself.
This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots).
#### Key Derivation
The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations.
The salt is the SHA256 hash of the 16-byte form of the client key.
#### Encryption
The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305.
The client should generate a random nonce, noting that AEAD is _not secure_ if a nonce is used repeatedly for the same key.
AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations.
In this protocol, the AAD is always 17 bytes of the form:
* `app_id` (byte) - always 1
* `version_id` (16 bytes) - 16-byte form of the version ID associated with this data
* for versions (AddVersion, GetChildVersion), the _parent_ version_id
* for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id
The `app_id` field is for future expansion to handle other, non-task data using this protocol.
Including it in the AAD ensures that such data cannot be confused with task data.
Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's `EVP_AEAD_CTX_seal`.
#### Representation
The final byte-stream is comprised of the following structure:
* `version` (byte) - format version (always 1)
* `nonce` (12 bytes) - encryption nonce
* `ciphertext` (remaining bytes) - ciphertext from sealing operation
The `version` field identifies this data format, and future formats will have a value other than 1 in this position.
### Version
The decrypted form of a version is a JSON array containing operations in the order they should be applied.
Each operation has the form `{TYPE: DATA}`, for example:
* `{"Create":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}`
* `{"Delete":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}`
* `{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":"v","timestamp":"2021-10-11T12:47:07.188090948Z"}}`
* `{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":null,"timestamp":"2021-10-11T12:47:07.188090948Z"}}` (to delete a property)
Timestamps are in RFC3339 format with a `Z` suffix.
### Snapshot
The decrypted form of a snapshot is a JSON object mapping task IDs to task properties.
For example (pretty-printed for clarity):
```json
{
"56e0be07-c61f-494c-a54c-bdcfdd52d2a7": {
"description": "a task",
"priority": "H"
},
"4b7ed904-f7b0-4293-8a10-ad452422c7b3": {
"description": "another task"
}
}
```
## Transactions
### AddVersion
The AddVersion transaction requests that the server add a new version to the client's task history.
The request contains the following;
* parent version ID
* history segment
The server determines whether the new version is acceptable, atomically with respect to other requests for the same client.
If it has no versions for the client, it accepts the version.
If it already has one or more versions for the client, then it accepts the version only if the given parent version ID matches its stored latest parent ID.
If the version is accepted, the server generates a new version ID for it.
The version is added to the set of versions for the client, the client's latest version ID is set to the new version ID.
The new version ID is returned in the response to the client.
The response may also include a request for a snapshot, with associated urgency.
If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the latest version ID.
The client may then "rebase" its operations and try again.
Note that if a client receives two conflict responses with the same parent version ID, it is an indication that the client's version history has diverged from that on the server.
### GetChildVersion
The GetChildVersion transaction is a read-only request for a version.
The request consists of a parent version ID.
The server searches its set of versions for a version with the given parent ID.
If found, it returns the version's
* version ID,
* parent version ID (matching that in the request), and
* history segment.
The response is either a version (success, _not-found_, or _gone_, as determined by the first of the following to apply:
* If a version with parentVersionId equal to the requested parentVersionId exists, it is returned.
* If the requested parentVersionId is the nil UUID ..
* ..and snapshotVersionId is nil, the response is _not-found_ (the client has no versions).
* ..and snapshotVersionId is not nil, the response is _gone_ (the first version has been deleted).
* If a version with versionId equal to the requested parentVersionId exists, the response is _not-found_ (the client is up-to-date)
* Otherwise, the response is _gone_ (the requested version has been deleted).
### AddSnapshot
The AddSnapshot transaction requests that the server store a new snapshot, generated by the client.
The request contains the following:
* version ID at which the snapshot was made
* snapshot data (opaque to the server)
The server should validate that the snapshot is for an existing version and is newer than any existing snapshot.
It may also validate that the snapshot is for a "recent" version (e.g., one of the last 5 versions).
If a snapshot already exists for the given version, the server may keep or discard the new snapshot but should return a success indication to the client.
The server response is empty.
### GetSnapshot
The GetSnapshot transaction requests that the server provide the latest snapshot.
The response contains the snapshot version ID and the snapshot data, if those exist.
## HTTP Representation
The transactions above are realized for an HTTP server at `<origin>` using the HTTP requests and responses described here.
The `origin` *should* be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption.
The replica identifies itself to the server using a `clientKey` in the form of a UUID.
This value is passed with every request in the `X-Client-Id` header, in its dashed-hex format.
### AddVersion
The request is a `POST` to `<origin>/v1/client/add-version/<parentVersionId>`.
The request body contains the history segment, optionally encoded using any encoding supported by actix-web.
The content-type must be `application/vnd.taskchampion.history-segment`.
The success response is a 200 OK with an empty body.
The new version ID appears in the `X-Version-Id` header.
If included, a snapshot request appears in the `X-Snapshot-Request` header with value `urgency=low` or `urgency=high`.
On conflict, the response is a 409 CONFLICT with an empty body.
The expected parent version ID appears in the `X-Parent-Version-Id` header.
Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification.
### GetChildVersion
The request is a `GET` to `<origin>/v1/client/get-child-version/<parentVersionId>`.
The response is determined as described above.
The _not-found_ response is 404 NOT FOUND.
The _gone_ response is 410 GONE.
Neither has a response body.
On success, the response is a 200 OK.
The version's history segment is returned in the response body, with content-type `application/vnd.taskchampion.history-segment`.
The version ID appears in the `X-Version-Id` header.
The response body may be encoded, in accordance with any `Accept-Encoding` header in the request.
On failure, a client should treat a 404 NOT FOUND as indicating that it is up-to-date.
Clients should treat a 410 GONE as a synchronization error.
If the client has pending changes to send to the server, based on a now-removed version, then those changes cannot be reconciled and will be lost.
The client should, optionally after consulting the user, download and apply the latest snapshot.
### AddSnapshot
The request is a `POST` to `<origin>/v1/client/add-snapshot/<versionId>`.
The request body contains the snapshot data, optionally encoded using any encoding supported by actix-web.
The content-type must be `application/vnd.taskchampion.snapshot`.
If the version is invalid, as described above, the response should be 400 BAD REQUEST.
The server response should be 200 OK on success.
### GetSnapshot
The request is a `GET` to `<origin>/v1/client/snapshot`.
The response is a 200 OK.
The snapshot is returned in the response body, with content-type `application/vnd.taskchampion.snapshot`.
The version ID appears in the `X-Version-Id` header.
The response body may be encoded, in accordance with any `Accept-Encoding` header in the request.
After downloading and decrypting a snapshot, a client must replace its entire local task database with the content of the snapshot.
Any local operations that had not yet been synchronized must be discarded.
After the snapshot is applied, the client should begin the synchronization process again, starting from the snapshot version.

View file

@ -0,0 +1,7 @@
# Synchronization and the Sync Server
This section covers *synchronization* of *replicas* containing the same set of tasks.
A replica is can perform all operations locally without connecting to a sync server, then share those operations with other replicas when it connects.
Sync is a critical feature of TaskChampion, allowing users to consult and update the same task list on multiple devices, without requiring constant connection.
This is a complex topic, and the section is broken into several chapters, beginning at the lower levels of the implementation and working up.

View file

@ -0,0 +1,32 @@
# Task Database
The task database is a layer of abstraction above the replica storage layer, responsible for maintaining some important invariants.
While the storage is pluggable, there is only one implementation of the task database.
## Reading Data
The task database provides read access to the data in the replica's storage through a variety of methods on the struct.
Each read operation is executed in a transaction, so data may not be consistent between read operations.
In practice, this is not an issue for TaskChampion's purposes.
## Working Set
The task database maintains the working set.
The working set maps small integers to current tasks, for easy reference by command-line users.
This is done in such a way that the task numbers remain stable until the working set is rebuilt, at which point gaps in the numbering, such as for completed tasks, are removed by shifting all higher-numbered tasks downward.
The working set is not replicated, and is not considered a part of any consistency guarantees in the task database.
## Modifying Data
Modifications to the data set are made by applying operations.
Operations are described in [Replica Storage](./storage.md).
Each operation is added to the list of operations in the storage, and simultaneously applied to the tasks in that storage.
Operations are checked for validity as they are applied.
## Deletion and Expiration
Deletion of a task merely changes the task's status to "deleted", leaving it in the Task database.
Actual removal of tasks from the task database takes place as part of _expiration_, triggered by the user as part of a garbage-collection process.
Expiration removes tasks with a `modified` property more than 180 days in the past, by creating a `Delete(uuid)` operation.

View file

@ -0,0 +1,59 @@
# Tasks
Tasks are stored internally as a key/value map with string keys and values.
All fields are optional: the `Create` operation creates an empty task.
Display layers should apply appropriate defaults where necessary.
## Atomicity
The synchronization process does not support read-modify-write operations.
For example, suppose tags are updated by reading a list of tags, adding a tag, and writing the result back.
This would be captured as an `Update` operation containing the amended list of tags.
Suppose two such `Update` operations are made in different replicas and must be reconciled:
* `Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag1", "2020-11-23T14:21:22Z")`
* `Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag2", "2020-11-23T15:08:57Z")`
The result of this reconciliation will be `oldtag,newtag2`, while the user almost certainly intended `oldtag,newtag1,newtag2`.
The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single `Update` operation.
## Validity
_Any_ key/value map is a valid task.
Consumers of task data must make a best effort to interpret any map, even if it contains apparently contradictory information.
For example, a task with status "completed" but no "end" key present should be interpreted as completed at an unknown time.
## Representations
Integers are stored in decimal notation.
Timestamps are stored as UNIX epoch timestamps, in the form of an integer.
## Keys
The following keys, and key formats, are defined:
* `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted
* `description` - the one-line summary of the task
* `modified` - the time of the last modification of this task
* `start` - the most recent time at which this task was started (a task with no `start` key is not active)
* `end` - if present, the time at which this task was completed or deleted (note that this key may not agree with `status`: it may be present for a pending task, or absent for a deleted or completed task)
* `tag_<tag>` - indicates this task has tag `<tag>` (value is an empty string)
* `wait` - indicates the time before which this task should be hidden, as it is not actionable
* `entry` - the time at which the task was created
* `annotation_<timestamp>` - value is an annotation created at the given time
The following are not yet implemented:
* `dep_<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
### UDAs
Any unrecognized keys are treated as "user-defined attributes" (UDAs).
These attributes can be used to store additional data associated with a task.
For example, applications that synchronize tasks with other systems such as calendars or team planning services might store unique identifiers for those systems as UDAs.
The application defining a UDA defines the format of the value.
UDAs _should_ have a namespaced structure of the form `<namespace>.<key>`, where `<namespace>` identifies the application defining the UDA.
For example, a service named "DevSync" synchronizing tasks from GitHub might use UDAs like `devsync.github.issue-id`.
Note that many existing UDAs for Taskwarrior integrations do not follow this pattern; these are referred to as legacy UDAs.

View file

@ -0,0 +1,63 @@
# TaskChampion
TaskChampion is a personal task-tracking tool.
It works from the command line, with simple commands like `ta add "fix the kitchen sink"`.
It can synchronize tasks on multiple devices, and does so in an "offline" mode so you can update your tasks even when you can't reach the server.
If you've heard of [TaskWarrior](https://taskwarrior.org/), this tool is very similar, but with some different design choices and greater reliability.
## Getting Started
> NOTE: TaskChampion is still in development and not yet feature-complete.
> This section is limited to completed functionality.
Once you've [installed TaskChampion](./installation.md), your interface will be via the `ta` command.
Start by adding a task:
```shell
$ ta add learn how to use taskchampion
added task ba57deaf-f97b-4e9c-b9ab-04bc1ecb22b8
```
You can see all of your pending tasks with `ta next`, or just `ta` for short:
```shell
$ ta
Id Description Active Tags
1 learn how to use taskchampion
```
Tell TaskChampion you're working on the task, using the shorthand id:
```shell
$ ta start 1
```
and when you're done with the task, mark it as complete:
```shell
$ ta done 1
```
## Synchronizing
Even if you don't have a server, it's a good idea to sync your task database periodically.
This acts as a backup and also enables some internal house-cleaning.
```shell
$ ta sync
```
Typically sync is run from a crontab, on whatever schedule fits your needs.
To synchronize multiple replicas of your tasks, you will need a sync server and a client key for that server.
Configure these in `~/.config/taskchampion.yml`, for example:
```yaml
server_client_key: "f8d4d09d-f6c7-4dd2-ab50-634ed20a3ff2"
server_origin: "https://taskchampion.example.com"
```
The next run of `ta sync` will upload your task history to that server.
Configuring another device identically and running `ta sync` will download that task history, and continue to stay in sync with subsequent runs of the command.
See [Usage](./using-task-command.md) for more detailed information on using TaskChampion.

View file

@ -0,0 +1,2 @@
test-db
test-sync-server

View file

@ -0,0 +1,25 @@
[package]
name = "integration-tests"
version = "0.4.1"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2018"
publish = false
build = "build.rs"
[dependencies]
taskchampion = { path = "../taskchampion" }
taskchampion-lib = { path = "../lib" }
taskchampion-sync-server = { path = "../sync-server" }
[dev-dependencies]
anyhow = "1.0"
actix-web = "^3.3.2"
actix-rt = "^1.1.1"
tempfile = "3"
pretty_assertions = "1"
log = "^0.4.17"
env_logger = "^0.9.0"
lazy_static = "1"
[build-dependencies]
cc = "1.0.73"

View file

@ -0,0 +1,30 @@
# Integration Tests for Taskchampion
## "Regular" Tests
Some of the tests in `tests/` are just regular integration tests.
Nothing exciting to see.
## Bindings Tests
The bindings tests are a bit more interesting, since they are written in C.
They are composed of a collection of "suites", each in one C file in `integration-tests/src/bindings_tests/`.
Each suite contains a number of tests (using [Unity](http://www.throwtheswitch.org/unity)) and an exported function named after the suite that returns an exit status (1 = failure).
The build script (`integration-tests/build.rs`) builds these files into a library that is linked with the `integration_tests` library crate.
This crate contains a `bindings_tests` module with a pub function for each suite.
Finally, the `integration-tests/tests/bindings.rs` test file calls each of those functions in a separate test case.
### Adding Tests
To add a test, select a suite and add a new test-case function.
Add a `RUN_TEST` invocation for your new function to the `.._tests` function at the bottom.
Keep the `RUN_TEST`s in the same order as the functions they call.
### Adding Suites
To add a suite,
1. Add a new C file in `integration-tests/src/bindings_tests/`, based off of one of the others.
1. Add a the suite name to `suites` in `integration-tests/build.rs`.

View file

@ -0,0 +1,48 @@
use std::env;
use std::fs;
use std::path::Path;
/// Build the Unity-based C test suite in `src/bindings_tests`, linking the result with this
/// package's library crate.
fn build_bindings_tests(suites: &[&'static str]) {
let mut build = cc::Build::new();
build.include("../lib"); // include path for taskchampion.h
build.include("src/bindings_tests/unity");
build.define("UNITY_OUTPUT_CHAR", "test_output");
build.define(
"UNITY_OUTPUT_CHAR_HEADER_DECLARATION",
"test_output(char c)",
);
build.file("src/bindings_tests/unity/unity.c");
let mut files = vec!["src/bindings_tests/test.c".to_string()];
for suite in suites {
files.push(format!("src/bindings_tests/{}.c", suite));
}
for file in files {
build.file(&file);
println!("cargo:rerun-if-changed={}", file);
}
build.compile("bindings-tests");
}
/// Make `bindings_test_suites.rs` listing all of the test suites, for use in building the
/// bindings-test binary.
fn make_suite_file(suites: &[&'static str]) {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("bindings_test_suites.rs");
let mut content = String::new();
for suite in suites {
content.push_str(format!("suite!({}_tests);\n", suite).as_ref());
}
fs::write(&dest_path, content).unwrap();
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let suites = &["uuid", "string", "task", "replica"];
build_bindings_tests(suites);
make_suite_file(suites);
}

View file

@ -0,0 +1,30 @@
use std::fs;
extern "C" {
// set up to send test output to TEST-OUTPUT
fn setup_output();
// close the output file
fn finish_output();
}
// Each suite is represented by a <name>_tests C function in <name>.c.
// All of these C files are built into a library that is linked to the crate -- but not to test
// crates. So, this macro produces a "glue function" that calls the C function, and that can be
// called from test crates.
macro_rules! suite(
{ $s:ident } => {
pub fn $s() -> (i32, String) {
extern "C" {
fn $s() -> i32;
}
unsafe { setup_output() };
let res = unsafe { $s() };
unsafe { finish_output() };
let output = fs::read_to_string("TEST-OUTPUT")
.unwrap_or_else(|e| format!("could not open TEST-OUTPUT: {}", e));
(res, output)
}
};
);
include!(concat!(env!("OUT_DIR"), "/bindings_test_suites.rs"));

View file

@ -0,0 +1,330 @@
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include "taskchampion.h"
#include "unity.h"
// creating an in-memory replica does not crash
static void test_replica_creation(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NOT_NULL(rep);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
// creating an on-disk replica does not crash
static void test_replica_creation_disk(void) {
TCReplica *rep = tc_replica_new_on_disk(tc_string_borrow("test-db"), NULL);
TEST_ASSERT_NOT_NULL(rep);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
// undo on an empty in-memory TCReplica does nothing
static void test_replica_undo_empty(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
int undone;
int rv = tc_replica_undo(rep, &undone);
TEST_ASSERT_EQUAL(TC_RESULT_OK, rv);
TEST_ASSERT_EQUAL(0, undone);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
// adding an undo point succeeds
static void test_replica_add_undo_point(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_add_undo_point(rep, true));
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
// working set operations succeed
static void test_replica_working_set(void) {
TCWorkingSet *ws;
TCTask *task1, *task2, *task3;
TCUuid uuid, uuid1, uuid2, uuid3;
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true));
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
ws = tc_replica_working_set(rep);
TEST_ASSERT_EQUAL(0, tc_working_set_len(ws));
tc_working_set_free(ws);
task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task1"));
TEST_ASSERT_NOT_NULL(task1);
uuid1 = tc_task_get_uuid(task1);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true));
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task2"));
TEST_ASSERT_NOT_NULL(task2);
uuid2 = tc_task_get_uuid(task2);
task3 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task3"));
TEST_ASSERT_NOT_NULL(task3);
uuid3 = tc_task_get_uuid(task3);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false));
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
// finish task2 to leave a "hole"
tc_task_to_mut(task2, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_done(task2));
tc_task_to_immut(task2);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false));
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_task_free(task1);
tc_task_free(task2);
tc_task_free(task3);
// working set should now be
// 0 -> None
// 1 -> uuid1
// 2 -> None
// 3 -> uuid3
ws = tc_replica_working_set(rep);
TEST_ASSERT_EQUAL(2, tc_working_set_len(ws));
TEST_ASSERT_EQUAL(3, tc_working_set_largest_index(ws));
TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 0, &uuid));
TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 1, &uuid));
TEST_ASSERT_EQUAL_MEMORY(uuid1.bytes, uuid.bytes, sizeof(uuid));
TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 2, &uuid));
TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 3, &uuid));
TEST_ASSERT_EQUAL_MEMORY(uuid3.bytes, uuid.bytes, sizeof(uuid));
TEST_ASSERT_EQUAL(1, tc_working_set_by_uuid(ws, uuid1));
TEST_ASSERT_EQUAL(0, tc_working_set_by_uuid(ws, uuid2));
TEST_ASSERT_EQUAL(3, tc_working_set_by_uuid(ws, uuid3));
tc_working_set_free(ws);
TEST_ASSERT_EQUAL(19, tc_replica_num_local_operations(rep));
tc_replica_free(rep);
}
// When tc_replica_undo is passed NULL for undone_out, it still succeeds
static void test_replica_undo_empty_null_undone_out(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
int rv = tc_replica_undo(rep, NULL);
TEST_ASSERT_EQUAL(TC_RESULT_OK, rv);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
// creating a task succeeds and the resulting task looks good
static void test_replica_task_creation(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TCUuid uuid = tc_task_get_uuid(task);
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
TCString desc = tc_task_get_description(task);
TEST_ASSERT_NOT_NULL(desc.ptr);
TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(&desc));
tc_string_free(&desc);
tc_task_free(task);
// get the task again and verify it
task = tc_replica_get_task(rep, uuid);
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes));
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
tc_task_free(task);
tc_replica_free(rep);
}
// When tc_replica_undo is passed NULL for undone_out, it still succeeds
static void test_replica_sync_local(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
mkdir("test-sync-server", 0755); // ignore error, if dir already exists
TCString err;
TCServer *server = tc_server_new_local(tc_string_borrow("test-sync-server"), &err);
TEST_ASSERT_NOT_NULL(server);
TEST_ASSERT_NULL(err.ptr);
int rv = tc_replica_sync(rep, server, false);
TEST_ASSERT_EQUAL(TC_RESULT_OK, rv);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_server_free(server);
tc_replica_free(rep);
// test error handling
server = tc_server_new_local(tc_string_borrow("/no/such/directory"), &err);
TEST_ASSERT_NULL(server);
TEST_ASSERT_NOT_NULL(err.ptr);
tc_string_free(&err);
}
// When tc_replica_undo is passed NULL for undone_out, it still succeeds
static void test_replica_remote_server(void) {
TCString err;
TCServer *server = tc_server_new_remote(
tc_string_borrow("tc.freecinc.com"),
tc_uuid_new_v4(),
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
&err);
TEST_ASSERT_NOT_NULL(server);
TEST_ASSERT_NULL(err.ptr);
// can't actually do anything with this server!
tc_server_free(server);
}
// a replica with tasks in it returns an appropriate list of tasks and list of uuids
static void test_replica_all_tasks(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task1 = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("task1"));
TEST_ASSERT_NOT_NULL(task1);
TCUuid uuid1 = tc_task_get_uuid(task1);
tc_task_free(task1);
TCTask *task2 = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("task2"));
TEST_ASSERT_NOT_NULL(task2);
TCUuid uuid2 = tc_task_get_uuid(task2);
tc_task_free(task2);
{
TCTaskList tasks = tc_replica_all_tasks(rep);
TEST_ASSERT_NOT_NULL(tasks.items);
TEST_ASSERT_EQUAL(2, tasks.len);
bool seen1, seen2 = false;
for (size_t i = 0; i < tasks.len; i++) {
TCTask *task = tasks.items[i];
TCString descr = tc_task_get_description(task);
if (0 == strcmp(tc_string_content(&descr), "task1")) {
seen1 = true;
} else if (0 == strcmp(tc_string_content(&descr), "task2")) {
seen2 = true;
}
tc_string_free(&descr);
}
TEST_ASSERT_TRUE(seen1);
TEST_ASSERT_TRUE(seen2);
tc_task_list_free(&tasks);
TEST_ASSERT_NULL(tasks.items);
}
{
TCUuidList uuids = tc_replica_all_task_uuids(rep);
TEST_ASSERT_NOT_NULL(uuids.items);
TEST_ASSERT_EQUAL(2, uuids.len);
bool seen1, seen2 = false;
for (size_t i = 0; i < uuids.len; i++) {
TCUuid uuid = uuids.items[i];
if (0 == memcmp(&uuid1, &uuid, sizeof(TCUuid))) {
seen1 = true;
} else if (0 == memcmp(&uuid2, &uuid, sizeof(TCUuid))) {
seen2 = true;
}
}
TEST_ASSERT_TRUE(seen1);
TEST_ASSERT_TRUE(seen2);
tc_uuid_list_free(&uuids);
TEST_ASSERT_NULL(uuids.items);
}
tc_replica_free(rep);
}
// importing a task succeeds and the resulting task looks good
static void test_replica_task_import(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCUuid uuid;
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid));
TCTask *task = tc_replica_import_task_with_uuid(rep, uuid);
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes));
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
TCString desc = tc_task_get_description(task);
TEST_ASSERT_NOT_NULL(desc.ptr);
TEST_ASSERT_EQUAL_STRING("", tc_string_content(&desc)); // default value
tc_string_free(&desc);
tc_task_free(task);
// get the task again and verify it
task = tc_replica_get_task(rep, uuid);
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL_MEMORY(uuid.bytes, tc_task_get_uuid(task).bytes, sizeof(uuid.bytes));
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
tc_task_free(task);
tc_replica_free(rep);
}
// importing a task succeeds and the resulting task looks good
static void test_replica_get_task_not_found(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCUuid uuid;
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow("23cb25e0-5d1a-4932-8131-594ac6d3a843"), &uuid));
TCTask *task = tc_replica_get_task(rep, uuid);
TEST_ASSERT_NULL(task);
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
tc_replica_free(rep);
}
int replica_tests(void) {
UNITY_BEGIN();
// each test case above should be named here, in order.
RUN_TEST(test_replica_creation);
RUN_TEST(test_replica_creation_disk);
RUN_TEST(test_replica_undo_empty);
RUN_TEST(test_replica_add_undo_point);
RUN_TEST(test_replica_working_set);
RUN_TEST(test_replica_undo_empty_null_undone_out);
RUN_TEST(test_replica_task_creation);
RUN_TEST(test_replica_sync_local);
RUN_TEST(test_replica_remote_server);
RUN_TEST(test_replica_all_tasks);
RUN_TEST(test_replica_task_import);
RUN_TEST(test_replica_get_task_not_found);
return UNITY_END();
}

View file

@ -0,0 +1,125 @@
#include <stdlib.h>
#include <string.h>
#include "unity.h"
#include "taskchampion.h"
// creating strings does not crash
static void test_string_creation(void) {
TCString s = tc_string_borrow("abcdef");
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// creating cloned strings does not crash
static void test_string_cloning(void) {
char *abcdef = strdup("abcdef");
TCString s = tc_string_clone(abcdef);
TEST_ASSERT_NOT_NULL(s.ptr);
free(abcdef);
TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s));
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// creating cloned strings with invalid utf-8 does not crash
// ..but content is NULL and content_and_len returns the value
static void test_string_cloning_invalid_utf8(void) {
TCString s = tc_string_clone("\xf0\x28\x8c\x28");
TEST_ASSERT_NOT_NULL(s.ptr);
// NOTE: this is not one of the cases where invalid UTF-8 results in NULL,
// but that may change.
size_t len;
const char *buf = tc_string_content_with_len(&s, &len);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_EQUAL(4, len);
TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len);
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// borrowed strings echo back their content
static void test_string_borrowed_strings_echo(void) {
TCString s = tc_string_borrow("abcdef");
TEST_ASSERT_NOT_NULL(s.ptr);
TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s));
size_t len;
const char *buf = tc_string_content_with_len(&s, &len);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_EQUAL(6, len);
TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len);
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// cloned strings echo back their content
static void test_string_cloned_strings_echo(void) {
char *orig = strdup("abcdef");
TCString s = tc_string_clone(orig);
TEST_ASSERT_NOT_NULL(s.ptr);
free(orig);
TEST_ASSERT_EQUAL_STRING("abcdef", tc_string_content(&s));
size_t len;
const char *buf = tc_string_content_with_len(&s, &len);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_EQUAL(6, len);
TEST_ASSERT_EQUAL_MEMORY("abcdef", buf, len);
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// tc_clone_with_len can have NULs, and tc_string_content returns NULL for
// strings containing embedded NULs
static void test_string_content_null_for_embedded_nuls(void) {
TCString s = tc_string_clone_with_len("ab\0de", 5);
TEST_ASSERT_NOT_NULL(s.ptr);
TEST_ASSERT_NULL(tc_string_content(&s));
size_t len;
const char *buf = tc_string_content_with_len(&s, &len);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_EQUAL(5, len);
TEST_ASSERT_EQUAL_MEMORY("ab\0de", buf, len);
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
// tc_string_clone_with_len will accept invalid utf-8, but then tc_string_content
// returns NULL.
static void test_string_clone_with_len_invalid_utf8(void) {
TCString s = tc_string_clone_with_len("\xf0\x28\x8c\x28", 4);
TEST_ASSERT_NOT_NULL(s.ptr);
TEST_ASSERT_NULL(tc_string_content(&s));
size_t len;
const char *buf = tc_string_content_with_len(&s, &len);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_EQUAL(4, len);
TEST_ASSERT_EQUAL_MEMORY("\xf0\x28\x8c\x28", buf, len);
tc_string_free(&s);
TEST_ASSERT_NULL(s.ptr);
}
int string_tests(void) {
UNITY_BEGIN();
// each test case above should be named here, in order.
RUN_TEST(test_string_creation);
RUN_TEST(test_string_cloning);
RUN_TEST(test_string_cloning_invalid_utf8);
RUN_TEST(test_string_borrowed_strings_echo);
RUN_TEST(test_string_cloned_strings_echo);
RUN_TEST(test_string_content_null_for_embedded_nuls);
RUN_TEST(test_string_clone_with_len_invalid_utf8);
return UNITY_END();
}

View file

@ -0,0 +1,668 @@
#include <stdlib.h>
#include <string.h>
#include "unity.h"
#include "taskchampion.h"
// creating a task succeeds and the resulting task looks good
static void test_task_creation(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
TCString desc = tc_task_get_description(task);
TEST_ASSERT_NOT_NULL(desc.ptr);
TEST_ASSERT_EQUAL_STRING("my task", tc_string_content(&desc));
tc_string_free(&desc);
tc_task_free(task);
tc_replica_free(rep);
}
// freeing a mutable task works, marking it immutable
static void test_task_free_mutable_task(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
TCUuid uuid = tc_task_get_uuid(task);
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_status(task, TC_STATUS_DELETED));
TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task));
tc_task_free(task); // implicitly converts to immut
task = tc_replica_get_task(rep, uuid);
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task));
tc_task_free(task);
tc_replica_free(rep);
}
// updating status on a task works
static void test_task_get_set_status(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_status(task, TC_STATUS_DELETED));
TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while mut
tc_task_to_immut(task);
TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task)); // while immut
tc_task_free(task);
tc_replica_free(rep);
}
// updating description on a task works
static void test_task_get_set_description(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TCString desc;
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_description(task, tc_string_borrow("updated")));
desc = tc_task_get_description(task);
TEST_ASSERT_NOT_NULL(desc.ptr);
TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&desc));
tc_string_free(&desc);
tc_task_to_immut(task);
desc = tc_task_get_description(task);
TEST_ASSERT_NOT_NULL(desc.ptr);
TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&desc));
tc_string_free(&desc);
tc_task_free(task);
tc_replica_free(rep);
}
// updating entry on a task works
static void test_task_get_set_entry(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
// creation of a task sets entry to current time
TEST_ASSERT_NOT_EQUAL(0, tc_task_get_entry(task));
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 1643679997));
TEST_ASSERT_EQUAL(1643679997, tc_task_get_entry(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 0));
TEST_ASSERT_EQUAL(0, tc_task_get_entry(task));
tc_task_free(task);
tc_replica_free(rep);
}
// updating wait on a task works
static void test_task_get_set_wait_and_is_waiting(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
// wait is not set on creation
TEST_ASSERT_EQUAL(0, tc_task_get_wait(task));
TEST_ASSERT_FALSE(tc_task_is_waiting(task));
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085
TEST_ASSERT_EQUAL(3643679997, tc_task_get_wait(task));
TEST_ASSERT_TRUE(tc_task_is_waiting(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 643679997)); // THE PAST!
TEST_ASSERT_EQUAL(643679997, tc_task_get_wait(task));
TEST_ASSERT_FALSE(tc_task_is_waiting(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 0));
TEST_ASSERT_EQUAL(0, tc_task_get_wait(task));
TEST_ASSERT_FALSE(tc_task_is_waiting(task));
tc_task_free(task);
tc_replica_free(rep);
}
// updating modified on a task works
static void test_task_get_set_modified(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
// creation of a task sets modified to current time
TEST_ASSERT_NOT_EQUAL(0, tc_task_get_modified(task));
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_modified(task, 1643679997));
TEST_ASSERT_EQUAL(1643679997, tc_task_get_modified(task));
// zero is not allowed
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_set_modified(task, 0));
tc_task_free(task);
tc_replica_free(rep);
}
// starting and stopping a task works, as seen by tc_task_is_active
static void test_task_start_stop_is_active(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TEST_ASSERT_FALSE(tc_task_is_active(task));
tc_task_to_mut(task, rep);
TEST_ASSERT_FALSE(tc_task_is_active(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_start(task));
TEST_ASSERT_TRUE(tc_task_is_active(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_stop(task));
TEST_ASSERT_FALSE(tc_task_is_active(task));
tc_task_free(task);
tc_replica_free(rep);
}
// tc_task_done and delete work and set the status
static void test_task_done_and_delete(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_STATUS_PENDING, tc_task_get_status(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_done(task));
TEST_ASSERT_EQUAL(TC_STATUS_COMPLETED, tc_task_get_status(task));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_delete(task));
TEST_ASSERT_EQUAL(TC_STATUS_DELETED, tc_task_get_status(task));
tc_task_free(task);
tc_replica_free(rep);
}
// adding and removing tags to a task works, and invalid tags are rejected
static void test_task_add_remove_has_tag(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
tc_task_to_mut(task, rep);
TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next")));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next")));
TEST_ASSERT_NULL(tc_task_error(task).ptr);
TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next")));
// invalid - synthetic tag
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("PENDING")));
TCString err = tc_task_error(task);
TEST_ASSERT_NOT_NULL(err.ptr);
tc_string_free(&err);
// invald - not a valid tag string
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("my tag")));
err = tc_task_error(task);
TEST_ASSERT_NOT_NULL(err.ptr);
tc_string_free(&err);
// invald - not utf-8
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_task_add_tag(task, tc_string_borrow("\xf0\x28\x8c\x28")));
err = tc_task_error(task);
TEST_ASSERT_NOT_NULL(err.ptr);
tc_string_free(&err);
TEST_ASSERT_TRUE(tc_task_has_tag(task, tc_string_borrow("next")));
// remove the tag
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_remove_tag(task, tc_string_borrow("next")));
TEST_ASSERT_NULL(tc_task_error(task).ptr);
TEST_ASSERT_FALSE(tc_task_has_tag(task, tc_string_borrow("next")));
tc_task_free(task);
tc_replica_free(rep);
}
// get_tags returns the list of tags
static void test_task_get_tags(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next")));
TCStringList tags = tc_task_get_tags(task);
int found_pending = false, found_next = false;
for (size_t i = 0; i < tags.len; i++) {
if (strcmp("PENDING", tc_string_content(&tags.items[i])) == 0) {
found_pending = true;
}
if (strcmp("next", tc_string_content(&tags.items[i])) == 0) {
found_next = true;
}
}
TEST_ASSERT_TRUE(found_pending);
TEST_ASSERT_TRUE(found_next);
tc_string_list_free(&tags);
TEST_ASSERT_NULL(tags.items);
tc_task_free(task);
tc_replica_free(rep);
}
// annotation manipulation (add, remove, list, free)
static void test_task_annotations(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
TCAnnotationList anns = tc_task_get_annotations(task);
TEST_ASSERT_EQUAL(0, anns.len);
TEST_ASSERT_NOT_NULL(anns.items);
tc_annotation_list_free(&anns);
tc_task_to_mut(task, rep);
TCAnnotation ann;
ann.entry = 1644623411;
ann.description = tc_string_borrow("ann1");
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann));
TEST_ASSERT_NULL(ann.description.ptr);
ann.entry = 1644623422;
ann.description = tc_string_borrow("ann2");
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann));
TEST_ASSERT_NULL(ann.description.ptr);
anns = tc_task_get_annotations(task);
int found1 = false, found2 = false;
for (size_t i = 0; i < anns.len; i++) {
if (0 == strcmp("ann1", tc_string_content(&anns.items[i].description))) {
TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623411);
found1 = true;
}
if (0 == strcmp("ann2", tc_string_content(&anns.items[i].description))) {
TEST_ASSERT_EQUAL(anns.items[i].entry, 1644623422);
found2 = true;
}
}
TEST_ASSERT_TRUE(found1);
TEST_ASSERT_TRUE(found2);
tc_annotation_list_free(&anns);
TEST_ASSERT_NULL(anns.items);
tc_task_free(task);
tc_replica_free(rep);
}
// UDA manipulation (add, remove, list, free)
static void test_task_udas(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(
rep,
TC_STATUS_PENDING,
tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
tc_task_to_mut(task, rep);
TCString value;
TCUdaList udas;
TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")).ptr);
TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr);
udas = tc_task_get_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(0, udas.len);
tc_uda_list_free(&udas);
udas = tc_task_get_legacy_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(0, udas.len);
tc_uda_list_free(&udas);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_set_uda(task,
tc_string_borrow("ns"),
tc_string_borrow("u1"),
tc_string_borrow("vvv")));
value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"));
TEST_ASSERT_NOT_NULL(value.ptr);
TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&value));
tc_string_free(&value);
TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr);
udas = tc_task_get_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(1, udas.len);
TEST_ASSERT_EQUAL_STRING("ns", tc_string_content(&udas.items[0].ns));
TEST_ASSERT_EQUAL_STRING("u1", tc_string_content(&udas.items[0].key));
TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&udas.items[0].value));
tc_uda_list_free(&udas);
udas = tc_task_get_legacy_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(1, udas.len);
TEST_ASSERT_NULL(udas.items[0].ns.ptr);
TEST_ASSERT_EQUAL_STRING("ns.u1", tc_string_content(&udas.items[0].key));
TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&udas.items[0].value));
tc_uda_list_free(&udas);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_set_legacy_uda(task,
tc_string_borrow("leg1"),
tc_string_borrow("legv")));
value = tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1"));
TEST_ASSERT_NOT_NULL(value.ptr);
TEST_ASSERT_EQUAL_STRING("vvv", tc_string_content(&value));
tc_string_free(&value);
value = tc_task_get_legacy_uda(task, tc_string_borrow("leg1"));
TEST_ASSERT_NOT_NULL(value.ptr);
TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&value));
tc_string_free(&value);
udas = tc_task_get_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(2, udas.len);
tc_uda_list_free(&udas);
udas = tc_task_get_legacy_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(2, udas.len);
tc_uda_list_free(&udas);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_uda(task,
tc_string_borrow("ns"),
tc_string_borrow("u1")));
TEST_ASSERT_NULL(tc_task_get_uda(task, tc_string_borrow("ns"), tc_string_borrow("u1")).ptr);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_uda(task,
tc_string_borrow("ns"),
tc_string_borrow("u1")));
udas = tc_task_get_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(1, udas.len);
TEST_ASSERT_EQUAL_STRING("", tc_string_content(&udas.items[0].ns));
TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(&udas.items[0].key));
TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&udas.items[0].value));
tc_uda_list_free(&udas);
udas = tc_task_get_legacy_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(1, udas.len);
TEST_ASSERT_NULL(udas.items[0].ns.ptr);
TEST_ASSERT_EQUAL_STRING("leg1", tc_string_content(&udas.items[0].key));
TEST_ASSERT_EQUAL_STRING("legv", tc_string_content(&udas.items[0].value));
tc_uda_list_free(&udas);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_legacy_uda(task,
tc_string_borrow("leg1")));
TEST_ASSERT_NULL(tc_task_get_legacy_uda(task, tc_string_borrow("leg1")).ptr);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_legacy_uda(task,
tc_string_borrow("leg1")));
udas = tc_task_get_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(0, udas.len);
tc_uda_list_free(&udas);
udas = tc_task_get_legacy_udas(task);
TEST_ASSERT_NOT_NULL(udas.items);
TEST_ASSERT_EQUAL(0, udas.len);
tc_uda_list_free(&udas);
tc_task_free(task);
tc_replica_free(rep);
}
// dependency manipulation
static void test_task_dependencies(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task 1"));
TEST_ASSERT_NOT_NULL(task1);
TCTask *task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task 2"));
TEST_ASSERT_NOT_NULL(task2);
TCUuidList deps;
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(0, deps.len);
tc_uuid_list_free(&deps);
tc_task_to_mut(task1, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_add_dependency(task1, tc_task_get_uuid(task2)));
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(1, deps.len);
TEST_ASSERT_EQUAL_MEMORY(tc_task_get_uuid(task2).bytes, deps.items[0].bytes, 16);
tc_uuid_list_free(&deps);
TEST_ASSERT_EQUAL(TC_RESULT_OK,
tc_task_remove_dependency(task1, tc_task_get_uuid(task2)));
deps = tc_task_get_dependencies(task1);
TEST_ASSERT_EQUAL(0, deps.len);
tc_uuid_list_free(&deps);
tc_task_free(task1);
tc_task_free(task2);
tc_replica_free(rep);
}
static void tckvlist_assert_key(TCKVList *list, char *key, char *value) {
TEST_ASSERT_NOT_NULL(list);
for (size_t i = 0; i < list->len; i++) {
if (0 == strcmp(tc_string_content(&list->items[i].key), key)) {
TEST_ASSERT_EQUAL_STRING(value, tc_string_content(&list->items[i].value));
return;
}
}
TEST_FAIL_MESSAGE("key not found");
}
// get_tags returns the list of tags
static void test_task_taskmap(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("my task"));
TEST_ASSERT_NOT_NULL(task);
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_tag(task, tc_string_borrow("next")));
TCAnnotation ann;
ann.entry = 1644623411;
ann.description = tc_string_borrow("ann1");
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_add_annotation(task, &ann));
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085
TCKVList taskmap = tc_task_get_taskmap(task);
tckvlist_assert_key(&taskmap, "annotation_1644623411", "ann1");
tckvlist_assert_key(&taskmap, "tag_next", "");
tckvlist_assert_key(&taskmap, "status", "pending");
tckvlist_assert_key(&taskmap, "description", "my task");
tc_kv_list_free(&taskmap);
tc_task_free(task);
tc_replica_free(rep);
}
// taking from a task list behaves correctly
static void test_task_list_take(void) {
TCReplica *rep = tc_replica_new_in_memory();
TEST_ASSERT_NULL(tc_replica_error(rep).ptr);
TCTask *task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("t"));
TEST_ASSERT_NOT_NULL(task1);
TCTask *task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("t"));
TEST_ASSERT_NOT_NULL(task2);
tc_task_free(task2);
TCString desc;
TCTaskList tasks = tc_replica_all_tasks(rep);
TEST_ASSERT_NOT_NULL(tasks.items);
TEST_ASSERT_EQUAL(2, tasks.len);
task1 = tc_task_list_take(&tasks, 5); // out of bounds
TEST_ASSERT_NULL(task1);
task1 = tc_task_list_take(&tasks, 0);
TEST_ASSERT_NOT_NULL(task1);
desc = tc_task_get_description(task1);
TEST_ASSERT_EQUAL_STRING("t", tc_string_content(&desc));
tc_string_free(&desc);
task2 = tc_task_list_take(&tasks, 1);
TEST_ASSERT_NOT_NULL(task2);
desc = tc_task_get_description(task2);
TEST_ASSERT_EQUAL_STRING("t", tc_string_content(&desc));
tc_string_free(&desc);
tc_task_free(task1);
tc_task_free(task2);
task1 = tc_task_list_take(&tasks, 0); // already taken
TEST_ASSERT_NULL(task1);
task1 = tc_task_list_take(&tasks, 5); // out of bounds
TEST_ASSERT_NULL(task1);
tc_task_list_free(&tasks);
TEST_ASSERT_NULL(tasks.items);
tc_replica_free(rep);
}
int task_tests(void) {
UNITY_BEGIN();
// each test case above should be named here, in order.
RUN_TEST(test_task_creation);
RUN_TEST(test_task_free_mutable_task);
RUN_TEST(test_task_get_set_status);
RUN_TEST(test_task_get_set_description);
RUN_TEST(test_task_get_set_entry);
RUN_TEST(test_task_get_set_modified);
RUN_TEST(test_task_get_set_wait_and_is_waiting);
RUN_TEST(test_task_start_stop_is_active);
RUN_TEST(test_task_done_and_delete);
RUN_TEST(test_task_add_remove_has_tag);
RUN_TEST(test_task_get_tags);
RUN_TEST(test_task_annotations);
RUN_TEST(test_task_udas);
RUN_TEST(test_task_dependencies);
RUN_TEST(test_task_taskmap);
RUN_TEST(test_task_list_take);
return UNITY_END();
}

View file

@ -0,0 +1,30 @@
#include <stdio.h>
#include "unity.h"
// these functions are shared between all test "suites"
// and cannot be customized per-suite.
void setUp(void) { }
void tearDown(void) { }
static FILE *output = NULL;
// Set up for test_output, writing output to "TEST-OUTPUT" in the
// current directory. The Rust test harness reads this file to get
// the output and display it only on failure. This is called by
// the Rust test harness
void setup_output(void) {
output = fopen("TEST-OUTPUT", "w");
}
// Close the output file. Called by the Rust test harness.
void finish_output(void) {
fclose(output);
output = NULL;
}
// this replaces UNITY_OUTPUT_CHAR, and writes output to
// TEST-OUTPUT in the current directory; the Rust test harness
// will read this data if the test fails.
void test_output(char c) {
fputc(c, output);
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) <year> 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,3 @@
# Unity
This directory contains the src from https://github.com/ThrowTheSwitch/Unity, revision 8ba01386008196a92ef4fdbdb0b00f2434c79563.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,661 @@
/* ==========================================
Unity Project - A Test Framework for C
Copyright (c) 2007-21 Mike Karlesky, Mark VanderVoord, Greg Williams
[Released under MIT License. Please refer to license.txt for details]
========================================== */
#ifndef UNITY_FRAMEWORK_H
#define UNITY_FRAMEWORK_H
#define UNITY
#define UNITY_VERSION_MAJOR 2
#define UNITY_VERSION_MINOR 5
#define UNITY_VERSION_BUILD 4
#define UNITY_VERSION ((UNITY_VERSION_MAJOR << 16) | (UNITY_VERSION_MINOR << 8) | UNITY_VERSION_BUILD)
#ifdef __cplusplus
extern "C"
{
#endif
#include "unity_internals.h"
/*-------------------------------------------------------
* Test Setup / Teardown
*-------------------------------------------------------*/
/* These functions are intended to be called before and after each test.
* If using unity directly, these will need to be provided for each test
* executable built. If you are using the test runner generator and/or
* Ceedling, these are optional. */
void setUp(void);
void tearDown(void);
/* These functions are intended to be called at the beginning and end of an
* entire test suite. suiteTearDown() is passed the number of tests that
* failed, and its return value becomes the exit code of main(). If using
* Unity directly, you're in charge of calling these if they are desired.
* If using Ceedling or the test runner generator, these will be called
* automatically if they exist. */
void suiteSetUp(void);
int suiteTearDown(int num_failures);
/*-------------------------------------------------------
* Test Reset and Verify
*-------------------------------------------------------*/
/* These functions are intended to be called before during tests in order
* to support complex test loops, etc. Both are NOT built into Unity. Instead
* the test runner generator will create them. resetTest will run teardown and
* setup again, verifying any end-of-test needs between. verifyTest will only
* run the verification. */
void resetTest(void);
void verifyTest(void);
/*-------------------------------------------------------
* Configuration Options
*-------------------------------------------------------
* All options described below should be passed as a compiler flag to all files using Unity. If you must add #defines, place them BEFORE the #include above.
* Integers/longs/pointers
* - Unity attempts to automatically discover your integer sizes
* - define UNITY_EXCLUDE_STDINT_H to stop attempting to look in <stdint.h>
* - define UNITY_EXCLUDE_LIMITS_H to stop attempting to look in <limits.h>
* - If you cannot use the automatic methods above, you can force Unity by using these options:
* - define UNITY_SUPPORT_64
* - set UNITY_INT_WIDTH
* - set UNITY_LONG_WIDTH
* - set UNITY_POINTER_WIDTH
* Floats
* - define UNITY_EXCLUDE_FLOAT to disallow floating point comparisons
* - define UNITY_FLOAT_PRECISION to specify the precision to use when doing TEST_ASSERT_EQUAL_FLOAT
* - define UNITY_FLOAT_TYPE to specify doubles instead of single precision floats
* - define UNITY_INCLUDE_DOUBLE to allow double floating point comparisons
* - define UNITY_EXCLUDE_DOUBLE to disallow double floating point comparisons (default)
* - define UNITY_DOUBLE_PRECISION to specify the precision to use when doing TEST_ASSERT_EQUAL_DOUBLE
* - define UNITY_DOUBLE_TYPE to specify something other than double
* - define UNITY_EXCLUDE_FLOAT_PRINT to trim binary size, won't print floating point values in errors
* Output
* - by default, Unity prints to standard out with putchar. define UNITY_OUTPUT_CHAR(a) with a different function if desired
* - define UNITY_DIFFERENTIATE_FINAL_FAIL to print FAILED (vs. FAIL) at test end summary - for automated search for failure
* Optimization
* - by default, line numbers are stored in unsigned shorts. Define UNITY_LINE_TYPE with a different type if your files are huge
* - by default, test and failure counters are unsigned shorts. Define UNITY_COUNTER_TYPE with a different type if you want to save space or have more than 65535 Tests.
* Test Cases
* - define UNITY_SUPPORT_TEST_CASES to include the TEST_CASE macro, though really it's mostly about the runner generator script
* Parameterized Tests
* - you'll want to create a define of TEST_CASE(...) which basically evaluates to nothing
* Tests with Arguments
* - you'll want to define UNITY_USE_COMMAND_LINE_ARGS if you have the test runner passing arguments to Unity
*-------------------------------------------------------
* Basic Fail and Ignore
*-------------------------------------------------------*/
#define TEST_FAIL_MESSAGE(message) UNITY_TEST_FAIL(__LINE__, (message))
#define TEST_FAIL() UNITY_TEST_FAIL(__LINE__, NULL)
#define TEST_IGNORE_MESSAGE(message) UNITY_TEST_IGNORE(__LINE__, (message))
#define TEST_IGNORE() UNITY_TEST_IGNORE(__LINE__, NULL)
#define TEST_MESSAGE(message) UnityMessage((message), __LINE__)
#define TEST_ONLY()
#ifdef UNITY_INCLUDE_PRINT_FORMATTED
#define TEST_PRINTF(message, ...) UnityPrintF(__LINE__, (message), __VA_ARGS__)
#endif
/* It is not necessary for you to call PASS. A PASS condition is assumed if nothing fails.
* This method allows you to abort a test immediately with a PASS state, ignoring the remainder of the test. */
#define TEST_PASS() TEST_ABORT()
#define TEST_PASS_MESSAGE(message) do { UnityMessage((message), __LINE__); TEST_ABORT(); } while (0)
/* This macro does nothing, but it is useful for build tools (like Ceedling) to make use of this to figure out
* which files should be linked to in order to perform a test. Use it like TEST_FILE("sandwiches.c") */
#define TEST_FILE(a)
/*-------------------------------------------------------
* Test Asserts (simple)
*-------------------------------------------------------*/
/* Boolean */
#define TEST_ASSERT(condition) UNITY_TEST_ASSERT( (condition), __LINE__, " Expression Evaluated To FALSE")
#define TEST_ASSERT_TRUE(condition) UNITY_TEST_ASSERT( (condition), __LINE__, " Expected TRUE Was FALSE")
#define TEST_ASSERT_UNLESS(condition) UNITY_TEST_ASSERT( !(condition), __LINE__, " Expression Evaluated To TRUE")
#define TEST_ASSERT_FALSE(condition) UNITY_TEST_ASSERT( !(condition), __LINE__, " Expected FALSE Was TRUE")
#define TEST_ASSERT_NULL(pointer) UNITY_TEST_ASSERT_NULL( (pointer), __LINE__, " Expected NULL")
#define TEST_ASSERT_NOT_NULL(pointer) UNITY_TEST_ASSERT_NOT_NULL((pointer), __LINE__, " Expected Non-NULL")
#define TEST_ASSERT_EMPTY(pointer) UNITY_TEST_ASSERT_EMPTY( (pointer), __LINE__, " Expected Empty")
#define TEST_ASSERT_NOT_EMPTY(pointer) UNITY_TEST_ASSERT_NOT_EMPTY((pointer), __LINE__, " Expected Non-Empty")
/* Integers (of all sizes) */
#define TEST_ASSERT_EQUAL_INT(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT8(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT8((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT16(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT16((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT32(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT32((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT64(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT64((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT8(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT8( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT16(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT16( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT32(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT32( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT64(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT64( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_size_t(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX8(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX8( (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX16(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX16((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX32(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX64(expected, actual) UNITY_TEST_ASSERT_EQUAL_HEX64((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_CHAR(expected, actual) UNITY_TEST_ASSERT_EQUAL_CHAR((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_BITS(mask, expected, actual) UNITY_TEST_ASSERT_BITS((mask), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_BITS_HIGH(mask, actual) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT)(-1), (actual), __LINE__, NULL)
#define TEST_ASSERT_BITS_LOW(mask, actual) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT)(0), (actual), __LINE__, NULL)
#define TEST_ASSERT_BIT_HIGH(bit, actual) UNITY_TEST_ASSERT_BITS(((UNITY_UINT)1 << (bit)), (UNITY_UINT)(-1), (actual), __LINE__, NULL)
#define TEST_ASSERT_BIT_LOW(bit, actual) UNITY_TEST_ASSERT_BITS(((UNITY_UINT)1 << (bit)), (UNITY_UINT)(0), (actual), __LINE__, NULL)
/* Integer Not Equal To (of all sizes) */
#define TEST_ASSERT_NOT_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_INT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_HEX64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_NOT_EQUAL_CHAR((threshold), (actual), __LINE__, NULL)
/* Integer Greater Than/ Less Than (of all sizes) */
#define TEST_ASSERT_GREATER_THAN(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_INT(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_INT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_INT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_INT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_INT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_INT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_UINT(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_UINT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_UINT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_UINT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_UINT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_size_t(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_HEX8(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_HEX16(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_HEX32(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_HEX64(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_HEX64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_THAN_CHAR(threshold, actual) UNITY_TEST_ASSERT_GREATER_THAN_CHAR((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_INT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_INT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_INT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_INT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_INT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_INT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_UINT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_UINT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_UINT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_UINT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_UINT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_size_t(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_HEX8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_HEX16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_HEX32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_HEX64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_HEX64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_THAN_CHAR(threshold, actual) UNITY_TEST_ASSERT_SMALLER_THAN_CHAR((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_GREATER_OR_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_INT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_INT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_INT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_INT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_INT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_UINT(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_UINT8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_UINT16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_UINT32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_UINT64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_size_t(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_HEX8(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_HEX16(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_HEX32(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_HEX64(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, NULL)
#define TEST_ASSERT_LESS_OR_EQUAL_CHAR(threshold, actual) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, NULL)
/* Integer Ranges (of all sizes) */
#define TEST_ASSERT_INT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_INT8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT8_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_INT16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT16_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_INT32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT32_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_INT64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_INT64_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_UINT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_UINT8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT8_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_UINT16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT16_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_UINT32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT32_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_UINT64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT64_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_size_t_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_HEX_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_HEX8_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX8_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_HEX16_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX16_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_HEX32_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_HEX64_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_HEX64_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_CHAR_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_CHAR_WITHIN((delta), (expected), (actual), __LINE__, NULL)
/* Integer Array Ranges (of all sizes) */
#define TEST_ASSERT_INT_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_INT8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_INT16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_INT32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_INT64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_UINT_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_UINT8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_UINT16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_UINT32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_UINT64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_size_t_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_HEX_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_HEX8_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_HEX16_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_HEX32_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_HEX64_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
#define TEST_ASSERT_CHAR_ARRAY_WITHIN(delta, expected, actual, num_elements) UNITY_TEST_ASSERT_CHAR_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, NULL)
/* Structs and Strings */
#define TEST_ASSERT_EQUAL_PTR(expected, actual) UNITY_TEST_ASSERT_EQUAL_PTR((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_STRING(expected, actual) UNITY_TEST_ASSERT_EQUAL_STRING((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_STRING_LEN(expected, actual, len) UNITY_TEST_ASSERT_EQUAL_STRING_LEN((expected), (actual), (len), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_MEMORY(expected, actual, len) UNITY_TEST_ASSERT_EQUAL_MEMORY((expected), (actual), (len), __LINE__, NULL)
/* Arrays */
#define TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_INT64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_size_t_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX8_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX16_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX16_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX32_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_HEX64_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_PTR_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_PTR_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_STRING_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_STRING_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_MEMORY_ARRAY(expected, actual, len, num_elements) UNITY_TEST_ASSERT_EQUAL_MEMORY_ARRAY((expected), (actual), (len), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_CHAR_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_CHAR_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
/* Arrays Compared To Single Value */
#define TEST_ASSERT_EACH_EQUAL_INT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_INT8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT8((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_INT16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT16((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_INT32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT32((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_INT64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_INT64((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_UINT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_UINT8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT8((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_UINT16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT16((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_UINT32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT32((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_UINT64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT64((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_size_t(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_HEX(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_HEX8(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX8((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_HEX16(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX16((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_HEX32(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_HEX64(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_HEX64((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_PTR(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_PTR((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_STRING(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_STRING((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_MEMORY(expected, actual, len, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_MEMORY((expected), (actual), (len), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_CHAR(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_CHAR((expected), (actual), (num_elements), __LINE__, NULL)
/* Floating Point (If Enabled) */
#define TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_FLOAT_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_FLOAT(expected, actual) UNITY_TEST_ASSERT_EQUAL_FLOAT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_FLOAT_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_FLOAT(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NEG_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NAN(actual) UNITY_TEST_ASSERT_FLOAT_IS_NAN((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_DETERMINATE(actual) UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NOT_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NOT_NEG_INF(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NOT_NAN(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN((actual), __LINE__, NULL)
#define TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE(actual) UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE((actual), __LINE__, NULL)
/* Double (If Enabled) */
#define TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual) UNITY_TEST_ASSERT_DOUBLE_WITHIN((delta), (expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_DOUBLE(expected, actual) UNITY_TEST_ASSERT_EQUAL_DOUBLE((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_DOUBLE_ARRAY(expected, actual, num_elements) UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_EACH_EQUAL_DOUBLE(expected, actual, num_elements) UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE((expected), (actual), (num_elements), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NEG_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NAN(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NAN((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_DETERMINATE(actual) UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NOT_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NOT_NAN(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN((actual), __LINE__, NULL)
#define TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE(actual) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE((actual), __LINE__, NULL)
/* Shorthand */
#ifdef UNITY_SHORTHAND_AS_OLD
#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, " Expected Not-Equal")
#endif
#ifdef UNITY_SHORTHAND_AS_INT
#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
#ifdef UNITY_SHORTHAND_AS_MEM
#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT_EQUAL_MEMORY((&expected), (&actual), sizeof(expected), __LINE__, NULL)
#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
#ifdef UNITY_SHORTHAND_AS_RAW
#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) == (actual)), __LINE__, " Expected Equal")
#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, " Expected Not-Equal")
#endif
#ifdef UNITY_SHORTHAND_AS_NONE
#define TEST_ASSERT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#define TEST_ASSERT_NOT_EQUAL(expected, actual) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
/*-------------------------------------------------------
* Test Asserts (with additional messages)
*-------------------------------------------------------*/
/* Boolean */
#define TEST_ASSERT_MESSAGE(condition, message) UNITY_TEST_ASSERT( (condition), __LINE__, (message))
#define TEST_ASSERT_TRUE_MESSAGE(condition, message) UNITY_TEST_ASSERT( (condition), __LINE__, (message))
#define TEST_ASSERT_UNLESS_MESSAGE(condition, message) UNITY_TEST_ASSERT( !(condition), __LINE__, (message))
#define TEST_ASSERT_FALSE_MESSAGE(condition, message) UNITY_TEST_ASSERT( !(condition), __LINE__, (message))
#define TEST_ASSERT_NULL_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NULL( (pointer), __LINE__, (message))
#define TEST_ASSERT_NOT_NULL_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NOT_NULL((pointer), __LINE__, (message))
#define TEST_ASSERT_EMPTY_MESSAGE(pointer, message) UNITY_TEST_ASSERT_EMPTY( (pointer), __LINE__, (message))
#define TEST_ASSERT_NOT_EMPTY_MESSAGE(pointer, message) UNITY_TEST_ASSERT_NOT_EMPTY((pointer), __LINE__, (message))
/* Integers (of all sizes) */
#define TEST_ASSERT_EQUAL_INT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT8((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT16((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT32((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT64((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT8( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT16( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT32( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT64( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_size_t_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_UINT( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX8_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX8( (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX16_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX16((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX32_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX32((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX64_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_HEX64((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_BITS_MESSAGE(mask, expected, actual, message) UNITY_TEST_ASSERT_BITS((mask), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_BITS_HIGH_MESSAGE(mask, actual, message) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT32)(-1), (actual), __LINE__, (message))
#define TEST_ASSERT_BITS_LOW_MESSAGE(mask, actual, message) UNITY_TEST_ASSERT_BITS((mask), (UNITY_UINT32)(0), (actual), __LINE__, (message))
#define TEST_ASSERT_BIT_HIGH_MESSAGE(bit, actual, message) UNITY_TEST_ASSERT_BITS(((UNITY_UINT32)1 << (bit)), (UNITY_UINT32)(-1), (actual), __LINE__, (message))
#define TEST_ASSERT_BIT_LOW_MESSAGE(bit, actual, message) UNITY_TEST_ASSERT_BITS(((UNITY_UINT32)1 << (bit)), (UNITY_UINT32)(0), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_CHAR_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_CHAR((expected), (actual), __LINE__, (message))
/* Integer Not Equal To (of all sizes) */
#define TEST_ASSERT_NOT_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_INT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_HEX64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_NOT_EQUAL_CHAR((threshold), (actual), __LINE__, (message))
/* Integer Greater Than/ Less Than (of all sizes) */
#define TEST_ASSERT_GREATER_THAN_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_INT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_HEX64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_THAN_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_THAN_CHAR((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_INT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_HEX64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_THAN_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_THAN_CHAR((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_INT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_GREATER_OR_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_GREATER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_INT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_INT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_INT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_INT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_INT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_UINT_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_UINT8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_UINT16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_UINT32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_UINT64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_size_t_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_UINT((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_HEX8_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX8((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_HEX16_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX16((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_HEX32_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX32((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_HEX64_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_HEX64((threshold), (actual), __LINE__, (message))
#define TEST_ASSERT_LESS_OR_EQUAL_CHAR_MESSAGE(threshold, actual, message) UNITY_TEST_ASSERT_SMALLER_OR_EQUAL_CHAR((threshold), (actual), __LINE__, (message))
/* Integer Ranges (of all sizes) */
#define TEST_ASSERT_INT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_INT8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT8_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_INT16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT16_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_INT32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT32_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_INT64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_INT64_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_UINT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_UINT8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT8_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_UINT16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT16_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_UINT32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT32_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_UINT64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT64_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_size_t_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_UINT_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_HEX_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_HEX8_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX8_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_HEX16_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX16_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_HEX32_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX32_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_HEX64_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_HEX64_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_CHAR_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_CHAR_WITHIN((delta), (expected), (actual), __LINE__, (message))
/* Integer Array Ranges (of all sizes) */
#define TEST_ASSERT_INT_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_INT8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_INT16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_INT32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_INT64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_INT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_UINT_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_UINT8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_UINT16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_UINT32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_UINT64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_size_t_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_UINT_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_HEX_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_HEX8_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX8_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_HEX16_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX16_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_HEX32_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX32_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_HEX64_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_HEX64_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
#define TEST_ASSERT_CHAR_ARRAY_WITHIN_MESSAGE(delta, expected, actual, num_elements, message) UNITY_TEST_ASSERT_CHAR_ARRAY_WITHIN((delta), (expected), (actual), num_elements, __LINE__, (message))
/* Structs and Strings */
#define TEST_ASSERT_EQUAL_PTR_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_PTR((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_STRING_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_STRING((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(expected, actual, len, message) UNITY_TEST_ASSERT_EQUAL_STRING_LEN((expected), (actual), (len), __LINE__, (message))
#define TEST_ASSERT_EQUAL_MEMORY_MESSAGE(expected, actual, len, message) UNITY_TEST_ASSERT_EQUAL_MEMORY((expected), (actual), (len), __LINE__, (message))
/* Arrays */
#define TEST_ASSERT_EQUAL_INT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT8_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT16_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT32_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_INT64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_INT64_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT8_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT16_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT32_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_UINT64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT64_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_size_t_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_UINT_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX8_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX8_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX16_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX16_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX32_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX32_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_HEX64_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_HEX64_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_PTR_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_PTR_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_STRING_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_STRING_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_MEMORY_ARRAY_MESSAGE(expected, actual, len, num_elements, message) UNITY_TEST_ASSERT_EQUAL_MEMORY_ARRAY((expected), (actual), (len), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EQUAL_CHAR_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_CHAR_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
/* Arrays Compared To Single Value*/
#define TEST_ASSERT_EACH_EQUAL_INT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_INT8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT8((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_INT16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT16((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_INT32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT32((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_INT64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_INT64((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_UINT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_UINT8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT8((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_UINT16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT16((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_UINT32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT32((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_UINT64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT64((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_size_t_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_UINT((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_HEX_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_HEX8_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX8((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_HEX16_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX16((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_HEX32_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX32((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_HEX64_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_HEX64((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_PTR_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_PTR((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_STRING_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_STRING((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_MEMORY_MESSAGE(expected, actual, len, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_MEMORY((expected), (actual), (len), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_CHAR_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_CHAR((expected), (actual), (num_elements), __LINE__, (message))
/* Floating Point (If Enabled) */
#define TEST_ASSERT_FLOAT_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_FLOAT_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_FLOAT_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_FLOAT((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_FLOAT_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_FLOAT_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_FLOAT_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_FLOAT((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_INF((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NEG_INF((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NAN((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_DETERMINATE((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NOT_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_INF((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NOT_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NEG_INF((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NOT_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_NAN((actual), __LINE__, (message))
#define TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE((actual), __LINE__, (message))
/* Double (If Enabled) */
#define TEST_ASSERT_DOUBLE_WITHIN_MESSAGE(delta, expected, actual, message) UNITY_TEST_ASSERT_DOUBLE_WITHIN((delta), (expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_DOUBLE_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_DOUBLE((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_EQUAL_DOUBLE_ARRAY_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EQUAL_DOUBLE_ARRAY((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_EACH_EQUAL_DOUBLE_MESSAGE(expected, actual, num_elements, message) UNITY_TEST_ASSERT_EACH_EQUAL_DOUBLE((expected), (actual), (num_elements), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_INF((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NEG_INF((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NAN((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_DETERMINATE((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NOT_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_INF((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NOT_NAN_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_NAN((actual), __LINE__, (message))
#define TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE_MESSAGE(actual, message) UNITY_TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE((actual), __LINE__, (message))
/* Shorthand */
#ifdef UNITY_SHORTHAND_AS_OLD
#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, (message))
#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, (message))
#endif
#ifdef UNITY_SHORTHAND_AS_INT
#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, message)
#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
#ifdef UNITY_SHORTHAND_AS_MEM
#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT_EQUAL_MEMORY((&expected), (&actual), sizeof(expected), __LINE__, message)
#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
#ifdef UNITY_SHORTHAND_AS_RAW
#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) == (actual)), __LINE__, message)
#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_ASSERT(((expected) != (actual)), __LINE__, message)
#endif
#ifdef UNITY_SHORTHAND_AS_NONE
#define TEST_ASSERT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#define TEST_ASSERT_NOT_EQUAL_MESSAGE(expected, actual, message) UNITY_TEST_FAIL(__LINE__, UnityStrErrShorthand)
#endif
/* end of UNITY_FRAMEWORK_H */
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,72 @@
#include <string.h>
#include "unity.h"
#include "taskchampion.h"
// creating UUIDs does not crash
static void test_uuid_creation(void) {
tc_uuid_new_v4();
tc_uuid_nil();
}
// converting UUIDs to a buf works
static void test_uuid_to_buf(void) {
TEST_ASSERT_EQUAL(TC_UUID_STRING_BYTES, 36);
TCUuid u2 = tc_uuid_nil();
char u2str[TC_UUID_STRING_BYTES];
tc_uuid_to_buf(u2, u2str);
TEST_ASSERT_EQUAL_MEMORY("00000000-0000-0000-0000-000000000000", u2str, TC_UUID_STRING_BYTES);
}
// converting UUIDs to a buf works
static void test_uuid_to_str(void) {
TCUuid u = tc_uuid_nil();
TCString s = tc_uuid_to_str(u);
TEST_ASSERT_EQUAL_STRING(
"00000000-0000-0000-0000-000000000000",
tc_string_content(&s));
tc_string_free(&s);
}
// converting valid UUIDs from string works
static void test_uuid_valid_from_str(void) {
TCUuid u;
char *ustr = "23cb25e0-5d1a-4932-8131-594ac6d3a843";
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_uuid_from_str(tc_string_borrow(ustr), &u));
TEST_ASSERT_EQUAL(0x23, u.bytes[0]);
TEST_ASSERT_EQUAL(0x43, u.bytes[15]);
}
// converting invalid UUIDs from string fails as expected
static void test_uuid_invalid_string_fails(void) {
TCUuid u;
char *ustr = "not-a-valid-uuid";
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_borrow(ustr), &u));
}
// converting invalid UTF-8 UUIDs from string fails as expected
static void test_uuid_bad_utf8(void) {
TCUuid u;
char *ustr = "\xf0\x28\x8c\xbc";
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_borrow(ustr), &u));
}
// converting a string with embedded NUL fails as expected
static void test_uuid_embedded_nul(void) {
TCUuid u;
TEST_ASSERT_EQUAL(TC_RESULT_ERROR, tc_uuid_from_str(tc_string_clone_with_len("ab\0de", 5), &u));
}
int uuid_tests(void) {
UNITY_BEGIN();
// each test case above should be named here, in order.
RUN_TEST(test_uuid_creation);
RUN_TEST(test_uuid_valid_from_str);
RUN_TEST(test_uuid_to_buf);
RUN_TEST(test_uuid_to_str);
RUN_TEST(test_uuid_invalid_string_fails);
RUN_TEST(test_uuid_bad_utf8);
RUN_TEST(test_uuid_embedded_nul);
return UNITY_END();
}

View file

@ -0,0 +1,2 @@
pub mod bindings_tests;
pub use taskchampion_lib::*;

View file

@ -0,0 +1,31 @@
use lazy_static::lazy_static;
use std::sync::Mutex;
use tempfile::TempDir;
lazy_static! {
// the C library running the tests is not reentrant, so we use a mutex to ensure that only one
// test runs at a time.
static ref MUTEX: Mutex<()> = Mutex::new(());
}
macro_rules! suite(
{ $s:ident } => {
#[test]
fn $s() {
let tmp_dir = TempDir::new().expect("TempDir failed");
let (res, output) = {
let _guard = MUTEX.lock().unwrap();
// run the tests in the temp dir (NOTE: this must be inside
// the mutex guard!)
std::env::set_current_dir(tmp_dir.as_ref()).unwrap();
integration_tests::bindings_tests::$s()
};
println!("{}", output);
if res != 0 {
assert!(false, "test failed");
}
}
};
);
include!(concat!(env!("OUT_DIR"), "/bindings_test_suites.rs"));

View file

@ -0,0 +1,90 @@
use actix_web::{App, HttpServer};
use pretty_assertions::assert_eq;
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
#[actix_rt::test]
async fn cross_sync() -> anyhow::Result<()> {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
let httpserver =
HttpServer::new(move || App::new().configure(|sc| server.config(sc))).bind("0.0.0.0:0")?;
// bind was to :0, so the kernel will have selected an unused port
let port = httpserver.addrs()[0].port();
httpserver.run();
// set up two replicas, and demonstrate replication between them
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
let client_key = Uuid::new_v4();
let encryption_secret = b"abc123".to_vec();
let make_server = || {
ServerConfig::Remote {
origin: format!("http://127.0.0.1:{}", port),
client_key,
encryption_secret: encryption_secret.clone(),
}
.into_server()
};
let mut serv1 = make_server()?;
let mut serv2 = make_server()?;
// add some tasks on rep1
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
// modify t1
let mut t1 = t1.into_mut(&mut rep1);
t1.start()?;
let t1 = t1.into_immut();
rep1.sync(&mut serv1, false)?;
rep2.sync(&mut serv2, false)?;
// those tasks should exist on rep2 now
let t12 = rep2
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep2");
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
assert_eq!(t12.get_description(), "test 1");
assert_eq!(t12.is_active(), true);
assert_eq!(t22.get_description(), "test 2");
assert_eq!(t22.is_active(), false);
// make non-conflicting changes on the two replicas
let mut t2 = t2.into_mut(&mut rep1);
t2.set_status(Status::Completed)?;
let t2 = t2.into_immut();
let mut t12 = t12.into_mut(&mut rep2);
t12.set_status(Status::Completed)?;
// sync those changes back and forth
rep1.sync(&mut serv1, false)?; // rep1 -> server
rep2.sync(&mut serv2, false)?; // server -> rep2, rep2 -> server
rep1.sync(&mut serv1, false)?; // server -> rep1
let t1 = rep1
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep1");
assert_eq!(t1.get_status(), Status::Completed);
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
assert_eq!(t22.get_status(), Status::Completed);
// note that we just drop the server here..
Ok(())
}

View file

@ -0,0 +1,94 @@
use actix_web::{App, HttpServer};
use pretty_assertions::assert_eq;
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
use taskchampion_sync_server::{
storage::InMemoryStorage, Server, ServerConfig as SyncServerConfig,
};
const NUM_VERSIONS: u32 = 50;
#[actix_rt::test]
async fn sync_with_snapshots() -> anyhow::Result<()> {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
let sync_server_config = SyncServerConfig {
snapshot_days: 100,
snapshot_versions: 3,
};
let server = Server::new(sync_server_config, Box::new(InMemoryStorage::new()));
let httpserver =
HttpServer::new(move || App::new().configure(|sc| server.config(sc))).bind("0.0.0.0:0")?;
// bind was to :0, so the kernel will have selected an unused port
let port = httpserver.addrs()[0].port();
httpserver.run();
let client_key = Uuid::new_v4();
let encryption_secret = b"abc123".to_vec();
let make_server = || {
ServerConfig::Remote {
origin: format!("http://127.0.0.1:{}", port),
client_key,
encryption_secret: encryption_secret.clone(),
}
.into_server()
};
// first we set up a single replica and sync it a lot of times, to establish a sync history.
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut serv1 = make_server()?;
let mut t1 = rep1.new_task(Status::Pending, "test 1".into())?;
log::info!("Applying modifications on replica 1");
for i in 0..=NUM_VERSIONS {
let mut t1m = t1.into_mut(&mut rep1);
t1m.start()?;
t1m.stop()?;
t1m.set_description(format!("revision {}", i))?;
t1 = t1m.into_immut();
rep1.sync(&mut serv1, false)?;
}
// now set up a second replica and sync it; it should catch up on that history, using a
// snapshot. Note that we can't verify that it used a snapshot, because the server currently
// keeps all versions (so rep2 could sync from the beginning of the version history). You can
// manually verify that it is applying a snapshot by adding `assert!(false)` below and skimming
// the logs.
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut serv2 = make_server()?;
log::info!("Syncing replica 2");
rep2.sync(&mut serv2, false)?;
// those tasks should exist on rep2 now
let t12 = rep2
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep2");
assert_eq!(t12.get_description(), format!("revision {}", NUM_VERSIONS));
assert_eq!(t12.is_active(), false);
// sync that back to replica 1
t12.into_mut(&mut rep2)
.set_description("sync-back".to_owned())?;
rep2.sync(&mut serv2, false)?;
rep1.sync(&mut serv1, false)?;
let t11 = rep1
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep1");
assert_eq!(t11.get_description(), "sync-back");
// uncomment this to force a failure and see the logs
// assert!(false);
// note that we just drop the server here..
Ok(())
}

View file

@ -0,0 +1,72 @@
use taskchampion::chrono::{TimeZone, Utc};
use taskchampion::{Replica, ServerConfig, Status, StorageConfig};
use tempfile::TempDir;
#[test]
fn update_and_delete_sync_delete_first() -> anyhow::Result<()> {
update_and_delete_sync(true)
}
#[test]
fn update_and_delete_sync_update_first() -> anyhow::Result<()> {
update_and_delete_sync(false)
}
/// Test what happens when an update is sync'd into a repo after a task is deleted.
/// If delete_first, then the deletion is sync'd to the server first; otherwise
/// the update is sync'd first. Either way, the task is gone.
fn update_and_delete_sync(delete_first: bool) -> anyhow::Result<()> {
// set up two replicas, and demonstrate replication between them
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
let tmp_dir = TempDir::new().expect("TempDir failed");
let mut server = ServerConfig::Local {
server_dir: tmp_dir.path().to_path_buf(),
}
.into_server()?;
// add a task on rep1, and sync it to rep2
let t = rep1.new_task(Status::Pending, "test task".into())?;
let u = t.get_uuid();
rep1.sync(&mut server, false)?;
rep2.sync(&mut server, false)?;
// mark the task as deleted, long in the past, on rep2
{
let mut t = rep2.get_task(u)?.unwrap().into_mut(&mut rep2);
t.delete()?;
t.set_modified(Utc.ymd(1980, 1, 1).and_hms(0, 0, 0))?;
}
// sync it back to rep1
rep2.sync(&mut server, false)?;
rep1.sync(&mut server, false)?;
// expire the task on rep1 and check that it is gone locally
rep1.expire_tasks()?;
assert!(rep1.get_task(u)?.is_none());
// modify the task on rep2
{
let mut t = rep2.get_task(u)?.unwrap().into_mut(&mut rep2);
t.set_description("modified".to_string())?;
}
// sync back and forth
if delete_first {
rep1.sync(&mut server, false)?;
}
rep2.sync(&mut server, false)?;
rep1.sync(&mut server, false)?;
if !delete_first {
rep2.sync(&mut server, false)?;
}
// check that the task is gone on both replicas
assert!(rep1.get_task(u)?.is_none());
assert!(rep2.get_task(u)?.is_none());
Ok(())
}

View file

@ -0,0 +1,12 @@
[package]
name = "taskchampion-lib"
version = "0.1.0"
edition = "2018"
[dependencies]
libc = "0.2.126"
taskchampion = { path = "../taskchampion" }
anyhow = "1.0"
[dev-dependencies]
pretty_assertions = "1"

View file

@ -0,0 +1,2 @@
taskchampion.h: cbindgen.toml ../target/debug/libtaskchampion.so
cbindgen --config cbindgen.toml --crate taskchampion-lib --output $@

View file

@ -0,0 +1,76 @@
/**
* TaskChampion
*
* This file defines the C interface to libtaskchampion. This is a thin
* wrapper around the Rust `taskchampion` crate. Refer to the documentation
* for that crate at https://docs.rs/taskchampion/latest/taskchampion/ for API
* details. The comments in this file focus mostly on the low-level details of
* passing values to and from TaskChampion.
*
* # Overview
*
* This library defines two major types used to interact with the API, that map directly
* to Rust types.
*
* * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html
* * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html
* * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html
* * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html
*
* It also defines a few utility types:
*
* * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings.
* * TCList - a list of objects represented as a C array
* * see below for the remainder
*
* # Safety
*
* Each type contains specific instructions to ensure memory safety.
* The general rules are as follows.
*
* No types in this library are threadsafe. All values should be used in only
* one thread for their entire lifetime. It is safe to use unrelated values in
* different threads (for example, different threads may use different
* TCReplica values concurrently).
*
* ## Pass by Pointer
*
* Several types such as TCReplica and TCString are "opaque" types and always
* handled as pointers in C. The bytes these pointers address are private to
* the Rust implemetation and must not be accessed from C.
*
* Pass-by-pointer values have exactly one owner, and that owner is responsible
* for freeing the value (using a `tc__free` function), or transferring
* ownership elsewhere. Except where documented otherwise, when a value is
* passed to C, ownership passes to C as well. When a value is passed to Rust,
* ownership stays with the C code. The exception is TCString, ownership of
* which passes to Rust when it is used as a function argument.
*
* The limited circumstances where one value must not outlive another, due to
* pointer references between them, are documented below.
*
* ## Pass by Value
*
* Types such as TCUuid and TCList are passed by value, and contain fields
* that are accessible from C. C code is free to access the content of these
* types in a _read_only_ fashion.
*
* Pass-by-value values that contain pointers also have exactly one owner,
* responsible for freeing the value or transferring ownership. The tc__free
* functions for these types will replace the pointers with NULL to guard
* against use-after-free errors. The interior pointers in such values should
* never be freed directly (for example, `tc_string_free(tcuda.value)` is an
* error).
*
* TCUuid is a special case, because it does not contain pointers. It can be
* freely copied and need not be freed.
*
* ## Lists
*
* Lists are a special kind of pass-by-value type. Each contains `len` and
* `items`, where `items` is an array of length `len`. Lists, and the values
* in the `items` array, must be treated as read-only. On return from an API
* function, a list's ownership is with the C caller, which must eventually
* free the list. List data must be freed with the `tc__list_free` function.
* It is an error to free any value in the `items` array of a list.
*/

View file

@ -0,0 +1,152 @@
use crate::traits::*;
use crate::types::*;
use taskchampion::chrono::prelude::*;
/// TCAnnotation contains the details of an annotation.
///
/// # Safety
///
/// An annotation must be initialized from a tc_.. function, and later freed
/// with `tc_annotation_free` or `tc_annotation_list_free`.
///
/// Any function taking a `*TCAnnotation` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function;
/// - the memory referenced by the pointer must never be modified by C code; and
/// - ownership transfers to the called function, and the value must not be used
/// after the call returns. In fact, the value will be zeroed out to ensure this.
///
/// TCAnnotations are not threadsafe.
#[repr(C)]
pub struct TCAnnotation {
/// Time the annotation was made. Must be nonzero.
pub entry: libc::time_t,
/// Content of the annotation. Must not be NULL.
pub description: TCString,
}
impl PassByValue for TCAnnotation {
// NOTE: we cannot use `RustType = Annotation` here because conversion of the
// Rust to a String can fail.
type RustType = (DateTime<Utc>, RustString<'static>);
unsafe fn from_ctype(mut self) -> Self::RustType {
// SAFETY:
// - any time_t value is valid
// - time_t is copy, so ownership is not important
let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap();
// SAFETY:
// - self.description is valid (came from return_val in as_ctype)
// - self is owned, so we can take ownership of this TCString
let description =
unsafe { TCString::take_val_from_arg(&mut self.description, TCString::default()) };
(entry, description)
}
fn as_ctype((entry, description): Self::RustType) -> Self {
TCAnnotation {
entry: libc::time_t::as_ctype(Some(entry)),
// SAFETY:
// - ownership of the TCString tied to ownership of Self
description: unsafe { TCString::return_val(description) },
}
}
}
impl Default for TCAnnotation {
fn default() -> Self {
TCAnnotation {
entry: 0 as libc::time_t,
description: TCString::default(),
}
}
}
/// TCAnnotationList represents a list of annotations.
///
/// The content of this struct must be treated as read-only.
#[repr(C)]
pub struct TCAnnotationList {
/// number of annotations in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// array of annotations. these remain owned by the TCAnnotationList instance and will be freed by
/// tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList.
items: *mut TCAnnotation,
}
impl CList for TCAnnotationList {
type Element = TCAnnotation;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCAnnotationList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used
/// after this call.
#[no_mangle]
pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) {
debug_assert!(!tcann.is_null());
// SAFETY:
// - tcann is not NULL
// - *tcann is a valid TCAnnotation (caller promised to treat it as read-only)
let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) };
drop(annotation);
}
/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList.
#[no_mangle]
pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) {
// SAFETY:
// - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcanns) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
assert!(!tcanns.items.is_null());
assert_eq!(tcanns.len, 0);
assert_eq!(tcanns._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_annotation_list_free(&mut tcanns) };
assert!(tcanns.items.is_null());
assert_eq!(tcanns.len, 0);
assert_eq!(tcanns._capacity, 0);
}
}

View file

@ -0,0 +1,34 @@
//! Trait implementations for a few atomic types
use crate::traits::*;
use taskchampion::chrono::prelude::*;
impl PassByValue for usize {
type RustType = usize;
unsafe fn from_ctype(self) -> usize {
self
}
fn as_ctype(arg: usize) -> usize {
arg
}
}
/// Convert an Option<DateTime<Utc>> to a libc::time_t, or zero if not set.
impl PassByValue for libc::time_t {
type RustType = Option<DateTime<Utc>>;
unsafe fn from_ctype(self) -> Option<DateTime<Utc>> {
if self == 0 {
None
} else {
Some(Utc.timestamp(self as i64, 0))
}
}
fn as_ctype(arg: Option<DateTime<Utc>>) -> libc::time_t {
arg.map(|ts| ts.timestamp() as libc::time_t)
.unwrap_or(0 as libc::time_t)
}
}

115
taskchampion/lib/src/kv.rs Normal file
View file

@ -0,0 +1,115 @@
use crate::traits::*;
use crate::types::*;
/// TCKV contains a key/value pair that is part of a task.
///
/// Neither key nor value are ever NULL. They remain owned by the TCKV and
/// will be freed when it is freed with tc_kv_list_free.
#[repr(C)]
pub struct TCKV {
pub key: TCString,
pub value: TCString,
}
impl PassByValue for TCKV {
type RustType = (RustString<'static>, RustString<'static>);
unsafe fn from_ctype(self) -> Self::RustType {
// SAFETY:
// - self.key is not NULL (field docstring)
// - self.key came from return_ptr in as_ctype
// - self is owned, so we can take ownership of this TCString
let key = unsafe { TCString::val_from_arg(self.key) };
// SAFETY: (same)
let value = unsafe { TCString::val_from_arg(self.value) };
(key, value)
}
fn as_ctype((key, value): Self::RustType) -> Self {
TCKV {
// SAFETY:
// - ownership of the TCString tied to ownership of Self
key: unsafe { TCString::return_val(key) },
// SAFETY:
// - ownership of the TCString tied to ownership of Self
value: unsafe { TCString::return_val(value) },
}
}
}
/// TCKVList represents a list of key/value pairs.
///
/// The content of this struct must be treated as read-only.
#[repr(C)]
pub struct TCKVList {
/// number of key/value pairs in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// array of TCKV's. these remain owned by the TCKVList instance and will be freed by
/// tc_kv_list_free. This pointer is never NULL for a valid TCKVList.
items: *mut TCKV,
}
impl CList for TCKVList {
type Element = TCKV;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCKVList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList.
#[no_mangle]
pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) {
// SAFETY:
// - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tckvs) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tckvs = unsafe { TCKVList::return_val(Vec::new()) };
assert!(!tckvs.items.is_null());
assert_eq!(tckvs.len, 0);
assert_eq!(tckvs._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tckvs = unsafe { TCKVList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_kv_list_free(&mut tckvs) };
assert!(tckvs.items.is_null());
assert_eq!(tckvs.len, 0);
assert_eq!(tckvs._capacity, 0);
}
}

View file

@ -0,0 +1,55 @@
#![warn(unsafe_op_in_unsafe_fn)]
#![allow(unused_unsafe)]
// Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020
// #![warn(clippy::undocumented_unsafe_blocks)]
// docstrings for extern "C" functions are reflected into C, and do not benefit
// from safety docs.
#![allow(clippy::missing_safety_doc)]
// deny some things that are typically warnings
#![deny(clippy::derivable_impls)]
#![deny(clippy::wrong_self_convention)]
#![deny(clippy::extra_unused_lifetimes)]
#![deny(clippy::unnecessary_to_owned)]
mod traits;
mod util;
pub mod annotation;
pub use annotation::*;
pub mod atomic;
pub use atomic::*;
pub mod kv;
pub use kv::*;
pub mod replica;
pub use replica::*;
pub mod result;
pub use result::*;
pub mod server;
pub use server::*;
pub mod status;
pub use status::*;
pub mod string;
pub use string::*;
pub mod task;
pub use task::*;
pub mod uda;
pub use uda::*;
pub mod uuid;
pub use uuid::*;
pub mod workingset;
pub use workingset::*;
pub(crate) mod types {
pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList};
pub(crate) use crate::kv::{TCKVList, TCKV};
pub(crate) use crate::replica::TCReplica;
pub(crate) use crate::result::TCResult;
pub(crate) use crate::server::TCServer;
pub(crate) use crate::status::TCStatus;
pub(crate) use crate::string::{RustString, TCString, TCStringList};
pub(crate) use crate::task::{TCTask, TCTaskList};
pub(crate) use crate::uda::{TCUda, TCUdaList, Uda};
pub(crate) use crate::uuid::{TCUuid, TCUuidList};
pub(crate) use crate::workingset::TCWorkingSet;
}

View file

@ -0,0 +1,443 @@
use crate::traits::*;
use crate::types::*;
use crate::util::err_to_ruststring;
use std::ptr::NonNull;
use taskchampion::{Replica, StorageConfig};
/// A replica represents an instance of a user's task data, providing an easy interface
/// for querying and modifying that data.
///
/// # Error Handling
///
/// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then
/// `tc_replica_error` will return the error message.
///
/// # Safety
///
/// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and
/// must later be freed to avoid a memory leak.
///
/// Any function taking a `*TCReplica` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function;
/// - the memory referenced by the pointer must never be modified by C code; and
/// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller.
///
/// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again.
///
/// TCReplicas are not threadsafe.
pub struct TCReplica {
/// The wrapped Replica
inner: Replica,
/// If true, this replica has an outstanding &mut (for a TaskMut)
mut_borrowed: bool,
/// The error from the most recent operation, if any
error: Option<RustString<'static>>,
}
impl PassByPointer for TCReplica {}
impl TCReplica {
/// Mutably borrow the inner Replica
pub(crate) fn borrow_mut(&mut self) -> &mut Replica {
if self.mut_borrowed {
panic!("replica is already borrowed");
}
self.mut_borrowed = true;
&mut self.inner
}
/// Release the borrow made by [`borrow_mut`]
pub(crate) fn release_borrow(&mut self) {
if !self.mut_borrowed {
panic!("replica is not borrowed");
}
self.mut_borrowed = false;
}
}
impl From<Replica> for TCReplica {
fn from(rep: Replica) -> TCReplica {
TCReplica {
inner: rep,
mut_borrowed: false,
error: None,
}
}
}
/// Utility function to allow using `?` notation to return an error value. This makes
/// a mutable borrow, because most Replica methods require a `&mut`.
fn wrap<T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T
where
F: FnOnce(&mut Replica) -> anyhow::Result<T>,
{
debug_assert!(!rep.is_null());
// SAFETY:
// - rep is not NULL (promised by caller)
// - *rep is a valid TCReplica (promised by caller)
// - rep is valid for the duration of this function
// - rep is not modified by anything else (not threadsafe)
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
if rep.mut_borrowed {
panic!("replica is borrowed and cannot be used");
}
rep.error = None;
match f(&mut rep.inner) {
Ok(v) => v,
Err(e) => {
rep.error = Some(err_to_ruststring(e));
err_value
}
}
}
/// Utility function to allow using `?` notation to return an error value in the constructor.
fn wrap_constructor<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe { *error_out = TCString::default() };
}
match f() {
Ok(v) => v,
Err(e) => {
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe {
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
}
}
err_value
}
}
}
/// Create a new TCReplica with an in-memory database. The contents of the database will be
/// lost when it is freed with tc_replica_free.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica {
let storage = StorageConfig::InMemory
.into_storage()
.expect("in-memory always succeeds");
// SAFETY:
// - caller promises to free this value
unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }
}
/// Create a new TCReplica with an on-disk database having the given filename. On error, a string
/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller
/// must free this string.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_on_disk(
path: TCString,
error_out: *mut TCString,
) -> *mut TCReplica {
wrap_constructor(
|| {
// SAFETY:
// - path is valid (promised by caller)
// - caller will not use path after this call (convention)
let mut path = unsafe { TCString::val_from_arg(path) };
let storage = StorageConfig::OnDisk {
taskdb_dir: path.to_path_buf_mut()?,
}
.into_storage()?;
// SAFETY:
// - caller promises to free this value
Ok(unsafe { TCReplica::from(Replica::new(storage)).return_ptr() })
},
error_out,
std::ptr::null_mut(),
)
}
/// Get a list of all tasks in the replica.
///
/// Returns a TCTaskList with a NULL items field on error.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList {
wrap(
rep,
|rep| {
// note that the Replica API returns a hashmap here, but we discard
// the keys and return a simple list. The task UUIDs are available
// from task.get_uuid(), so information is not lost.
let tasks: Vec<_> = rep
.all_tasks()?
.drain()
.map(|(_uuid, t)| {
Some(
NonNull::new(
// SAFETY:
// - caller promises to free this value (via freeing the list)
unsafe { TCTask::from(t).return_ptr() },
)
.expect("TCTask::return_ptr returned NULL"),
)
})
.collect();
// SAFETY:
// - value is not allocated and need not be freed
Ok(unsafe { TCTaskList::return_val(tasks) })
},
TCTaskList::null_value(),
)
}
/// Get a list of all uuids for tasks in the replica.
///
/// Returns a TCUuidList with a NULL items field on error.
///
/// The caller must free the UUID list with `tc_uuid_list_free`.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList {
wrap(
rep,
|rep| {
let uuids: Vec<_> = rep
.all_task_uuids()?
.drain(..)
// SAFETY:
// - value is not allocated and need not be freed
.map(|uuid| unsafe { TCUuid::return_val(uuid) })
.collect();
// SAFETY:
// - value will be freed (promised by caller)
Ok(unsafe { TCUuidList::return_val(uuids) })
},
TCUuidList::null_value(),
)
}
/// Get the current working set for this replica. The resulting value must be freed
/// with tc_working_set_free.
///
/// Returns NULL on error.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet {
wrap(
rep,
|rep| {
let ws = rep.working_set()?;
// SAFETY:
// - caller promises to free this value
Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) })
},
std::ptr::null_mut(),
)
}
/// Get an existing task by its UUID.
///
/// Returns NULL when the task does not exist, and on error. Consult tc_replica_error
/// to distinguish the two conditions.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask {
wrap(
rep,
|rep| {
// SAFETY:
// - tcuuid is a valid TCUuid (all bytes are valid)
// - tcuuid is Copy so ownership doesn't matter
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
if let Some(task) = rep.get_task(uuid)? {
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
} else {
Ok(std::ptr::null_mut())
}
},
std::ptr::null_mut(),
)
}
/// Create a new task. The task must not already exist.
///
/// Returns the task, or NULL on error.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_task(
rep: *mut TCReplica,
status: TCStatus,
description: TCString,
) -> *mut TCTask {
// SAFETY:
// - description is valid (promised by caller)
// - caller will not use description after this call (convention)
let mut description = unsafe { TCString::val_from_arg(description) };
wrap(
rep,
|rep| {
let task = rep.new_task(status.into(), description.as_str()?.to_string())?;
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
},
std::ptr::null_mut(),
)
}
/// Create a new task. The task must not already exist.
///
/// Returns the task, or NULL on error.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_import_task_with_uuid(
rep: *mut TCReplica,
tcuuid: TCUuid,
) -> *mut TCTask {
wrap(
rep,
|rep| {
// SAFETY:
// - tcuuid is a valid TCUuid (all bytes are valid)
// - tcuuid is Copy so ownership doesn't matter
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
let task = rep.import_task_with_uuid(uuid)?;
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
},
std::ptr::null_mut(),
)
}
/// Synchronize this replica with a server.
///
/// The `server` argument remains owned by the caller, and must be freed explicitly.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_sync(
rep: *mut TCReplica,
server: *mut TCServer,
avoid_snapshots: bool,
) -> TCResult {
wrap(
rep,
|rep| {
debug_assert!(!server.is_null());
// SAFETY:
// - server is not NULL
// - *server is a valid TCServer (promised by caller)
// - server is valid for the lifetime of tc_replica_sync (not threadsafe)
// - server will not be accessed simultaneously (not threadsafe)
let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) };
rep.sync(server.as_mut(), avoid_snapshots)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Undo local operations until the most recent UndoPoint.
///
/// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if
/// there are no operations that can be done.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_undo(rep: *mut TCReplica, undone_out: *mut i32) -> TCResult {
wrap(
rep,
|rep| {
let undone = if rep.undo()? { 1 } else { 0 };
if !undone_out.is_null() {
// SAFETY:
// - undone_out is not NULL (just checked)
// - undone_out is properly aligned (implicitly promised by caller)
unsafe { *undone_out = undone };
}
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Get the number of local, un-synchronized operations, or -1 on error
#[no_mangle]
pub unsafe extern "C" fn tc_replica_num_local_operations(rep: *mut TCReplica) -> i64 {
wrap(
rep,
|rep| {
let count = rep.num_local_operations()? as i64;
Ok(count)
},
-1,
)
}
/// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically
/// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already
/// been created by this Replica, and may be useful when a Replica instance is held for a long time
/// and used to apply more than one user-visible change.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_add_undo_point(rep: *mut TCReplica, force: bool) -> TCResult {
wrap(
rep,
|rep| {
rep.add_undo_point(force)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber`
/// is true, then existing tasks may be moved to new working-set indices; in any case, on
/// completion all pending tasks are in the working set and all non- pending tasks are not.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_rebuild_working_set(
rep: *mut TCReplica,
renumber: bool,
) -> TCResult {
wrap(
rep,
|rep| {
rep.rebuild_working_set(renumber)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent
/// calls to this function will return NULL. The rep pointer must not be NULL. The caller must
/// free the returned string.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> TCString {
// SAFETY:
// - rep is not NULL (promised by caller)
// - *rep is a valid TCReplica (promised by caller)
// - rep is valid for the duration of this function
// - rep is not modified by anything else (not threadsafe)
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
if let Some(rstring) = rep.error.take() {
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(rstring) }
} else {
TCString::default()
}
}
/// Free a replica. The replica may not be used after this function returns and must not be freed
/// more than once.
#[no_mangle]
pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) {
// SAFETY:
// - replica is not NULL (promised by caller)
// - replica is valid (promised by caller)
// - caller will not use description after this call (promised by caller)
let replica = unsafe { TCReplica::take_from_ptr_arg(rep) };
if replica.mut_borrowed {
panic!("replica is borrowed and cannot be freed");
}
drop(replica);
}

View file

@ -0,0 +1,9 @@
/// A result from a TC operation. Typically if this value is TC_RESULT_ERROR,
/// the associated object's `tc_.._error` method will return an error message.
/// cbindgen:prefix-with-name
/// cbindgen:rename-all=ScreamingSnakeCase
#[repr(i32)]
pub enum TCResult {
Error = -1,
Ok = 0,
}

View file

@ -0,0 +1,143 @@
use crate::traits::*;
use crate::types::*;
use crate::util::err_to_ruststring;
use taskchampion::{Server, ServerConfig};
/// TCServer represents an interface to a sync server. Aside from new and free, a server
/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`.
///
/// ## Safety
///
/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously.
pub struct TCServer(Box<dyn Server>);
impl PassByPointer for TCServer {}
impl From<Box<dyn Server>> for TCServer {
fn from(server: Box<dyn Server>) -> TCServer {
TCServer(server)
}
}
impl AsMut<Box<dyn Server>> for TCServer {
fn as_mut(&mut self) -> &mut Box<dyn Server> {
&mut self.0
}
}
/// Utility function to allow using `?` notation to return an error value.
fn wrap<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe { *error_out = TCString::default() };
}
match f() {
Ok(v) => v,
Err(e) => {
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe {
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
}
}
err_value
}
}
}
/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the
/// description of the arguments.
///
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
/// returned. The caller must free this string.
///
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
#[no_mangle]
pub unsafe extern "C" fn tc_server_new_local(
server_dir: TCString,
error_out: *mut TCString,
) -> *mut TCServer {
wrap(
|| {
// SAFETY:
// - server_dir is valid (promised by caller)
// - caller will not use server_dir after this call (convention)
let mut server_dir = unsafe { TCString::val_from_arg(server_dir) };
let server_config = ServerConfig::Local {
server_dir: server_dir.to_path_buf_mut()?,
};
let server = server_config.into_server()?;
// SAFETY: caller promises to free this server.
Ok(unsafe { TCServer::return_ptr(server.into()) })
},
error_out,
std::ptr::null_mut(),
)
}
/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the
/// description of the arguments.
///
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
/// returned. The caller must free this string.
///
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
#[no_mangle]
pub unsafe extern "C" fn tc_server_new_remote(
origin: TCString,
client_key: TCUuid,
encryption_secret: TCString,
error_out: *mut TCString,
) -> *mut TCServer {
wrap(
|| {
// SAFETY:
// - origin is valid (promised by caller)
// - origin ownership is transferred to this function
let origin = unsafe { TCString::val_from_arg(origin) }.into_string()?;
// SAFETY:
// - client_key is a valid Uuid (any 8-byte sequence counts)
let client_key = unsafe { TCUuid::val_from_arg(client_key) };
// SAFETY:
// - encryption_secret is valid (promised by caller)
// - encryption_secret ownership is transferred to this function
let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) }
.as_bytes()
.to_vec();
let server_config = ServerConfig::Remote {
origin,
client_key,
encryption_secret,
};
let server = server_config.into_server()?;
// SAFETY: caller promises to free this server.
Ok(unsafe { TCServer::return_ptr(server.into()) })
},
error_out,
std::ptr::null_mut(),
)
}
/// Free a server. The server may not be used after this function returns and must not be freed
/// more than once.
#[no_mangle]
pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) {
debug_assert!(!server.is_null());
// SAFETY:
// - server is not NULL
// - server came from tc_server_new_.., which used return_ptr
// - server will not be used after (promised by caller)
let server = unsafe { TCServer::take_from_ptr_arg(server) };
drop(server);
}

View file

@ -0,0 +1,36 @@
pub use taskchampion::Status;
/// The status of a task, as defined by the task data model.
/// cbindgen:prefix-with-name
/// cbindgen:rename-all=ScreamingSnakeCase
#[repr(C)]
pub enum TCStatus {
Pending,
Completed,
Deleted,
/// Unknown signifies a status in the task DB that was not
/// recognized.
Unknown,
}
impl From<TCStatus> for Status {
fn from(status: TCStatus) -> Status {
match status {
TCStatus::Pending => Status::Pending,
TCStatus::Completed => Status::Completed,
TCStatus::Deleted => Status::Deleted,
TCStatus::Unknown => Status::Unknown("unknown".to_string()),
}
}
}
impl From<Status> for TCStatus {
fn from(status: Status) -> TCStatus {
match status {
Status::Pending => TCStatus::Pending,
Status::Completed => TCStatus::Completed,
Status::Deleted => TCStatus::Deleted,
Status::Unknown(_) => TCStatus::Unknown,
}
}
}

View file

@ -0,0 +1,712 @@
use crate::traits::*;
use crate::util::{string_into_raw_parts, vec_into_raw_parts};
use std::ffi::{CStr, CString, OsString};
use std::os::raw::c_char;
use std::path::PathBuf;
/// TCString supports passing strings into and out of the TaskChampion API.
///
/// # Rust Strings and C Strings
///
/// A Rust string can contain embedded NUL characters, while C considers such a character to mark
/// the end of a string. Strings containing embedded NULs cannot be represented as a "C string"
/// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In
/// general, these two functions should be used for handling arbitrary data, while more convenient
/// forms may be used where embedded NUL characters are impossible, such as in static strings.
///
/// # UTF-8
///
/// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given
/// a `*TCString` containing invalid UTF-8.
///
/// # Safety
///
/// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All
/// other fields in a TCString are private and must not be used from C. They exist in the struct
/// to ensure proper allocation and alignment.
///
/// When a `TCString` appears as a return value or output argument, ownership is passed to the
/// caller. The caller must pass that ownership back to another function or free the string.
///
/// Any function taking a `TCString` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function; and
/// - the memory referenced by the pointer must never be modified by C code.
///
/// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is
/// given as a function argument, and the caller must not use or free TCStrings after passing them
/// to such API functions.
///
/// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail
/// for such a value.
///
/// TCString is not threadsafe.
/// cbindgen:field-names=[ptr, _u1, _u2, _u3]
#[repr(C)]
pub struct TCString {
// defined based on the type
ptr: *mut libc::c_void,
len: usize,
cap: usize,
// type of TCString this represents
ty: u8,
}
// TODO: figure out how to ignore this but still use it in TCString
/// A discriminator for TCString
#[repr(u8)]
enum TCStringType {
/// Null. Nothing is contained in this string.
///
/// * `ptr` is NULL.
/// * `len` and `cap` are zero.
Null = 0,
/// A CString.
///
/// * `ptr` is the result of CString::into_raw, containing a terminating NUL. It may not be
/// valid UTF-8.
/// * `len` and `cap` are zero.
CString,
/// A CStr, referencing memory borrowed from C
///
/// * `ptr` points to the string, containing a terminating NUL. It may not be valid UTF-8.
/// * `len` and `cap` are zero.
CStr,
/// A String.
///
/// * `ptr`, `len`, and `cap` are as would be returned from String::into_raw_parts.
String,
/// A byte sequence.
///
/// * `ptr`, `len`, and `cap` are as would be returned from Vec::into_raw_parts.
Bytes,
}
impl Default for TCString {
fn default() -> Self {
TCString {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
ty: TCStringType::Null as u8,
}
}
}
impl TCString {
pub(crate) fn is_null(&self) -> bool {
self.ptr.is_null()
}
}
#[derive(PartialEq, Debug)]
pub enum RustString<'a> {
Null,
CString(CString),
CStr(&'a CStr),
String(String),
Bytes(Vec<u8>),
}
impl<'a> Default for RustString<'a> {
fn default() -> Self {
RustString::Null
}
}
impl PassByValue for TCString {
type RustType = RustString<'static>;
unsafe fn from_ctype(self) -> Self::RustType {
match self.ty {
ty if ty == TCStringType::CString as u8 => {
// SAFETY:
// - ptr was derived from CString::into_raw
// - data was not modified since that time (caller promises)
RustString::CString(unsafe { CString::from_raw(self.ptr as *mut c_char) })
}
ty if ty == TCStringType::CStr as u8 => {
// SAFETY:
// - ptr was created by CStr::as_ptr
// - data was not modified since that time (caller promises)
RustString::CStr(unsafe { CStr::from_ptr(self.ptr as *mut c_char) })
}
ty if ty == TCStringType::String as u8 => {
// SAFETY:
// - ptr was created by string_into_raw_parts
// - data was not modified since that time (caller promises)
RustString::String(unsafe {
String::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
})
}
ty if ty == TCStringType::Bytes as u8 => {
// SAFETY:
// - ptr was created by vec_into_raw_parts
// - data was not modified since that time (caller promises)
RustString::Bytes(unsafe {
Vec::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
})
}
_ => RustString::Null,
}
}
fn as_ctype(arg: Self::RustType) -> Self {
match arg {
RustString::Null => Self {
ty: TCStringType::Null as u8,
..Default::default()
},
RustString::CString(cstring) => Self {
ty: TCStringType::CString as u8,
ptr: cstring.into_raw() as *mut libc::c_void,
..Default::default()
},
RustString::CStr(cstr) => Self {
ty: TCStringType::CStr as u8,
ptr: cstr.as_ptr() as *mut libc::c_void,
..Default::default()
},
RustString::String(string) => {
let (ptr, len, cap) = string_into_raw_parts(string);
Self {
ty: TCStringType::String as u8,
ptr: ptr as *mut libc::c_void,
len,
cap,
}
}
RustString::Bytes(bytes) => {
let (ptr, len, cap) = vec_into_raw_parts(bytes);
Self {
ty: TCStringType::Bytes as u8,
ptr: ptr as *mut libc::c_void,
len,
cap,
}
}
}
}
}
impl<'a> RustString<'a> {
/// Get a regular Rust &str for this value.
pub(crate) fn as_str(&mut self) -> Result<&str, std::str::Utf8Error> {
match self {
RustString::CString(cstring) => cstring.as_c_str().to_str(),
RustString::CStr(cstr) => cstr.to_str(),
RustString::String(ref string) => Ok(string.as_ref()),
RustString::Bytes(_) => {
self.bytes_to_string()?;
self.as_str() // now the String variant, so won't recurse
}
RustString::Null => unreachable!(),
}
}
/// Consume this RustString and return an equivalent String, or an error if not
/// valid UTF-8. In the error condition, the original data is lost.
pub(crate) fn into_string(mut self) -> Result<String, std::str::Utf8Error> {
match self {
RustString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()),
RustString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()),
RustString::String(string) => Ok(string),
RustString::Bytes(_) => {
self.bytes_to_string()?;
self.into_string() // now the String variant, so won't recurse
}
RustString::Null => unreachable!(),
}
}
pub(crate) fn as_bytes(&self) -> &[u8] {
match self {
RustString::CString(cstring) => cstring.as_bytes(),
RustString::CStr(cstr) => cstr.to_bytes(),
RustString::String(string) => string.as_bytes(),
RustString::Bytes(bytes) => bytes.as_ref(),
RustString::Null => unreachable!(),
}
}
/// Convert the RustString, in place, from the Bytes to String variant. On successful return,
/// the RustString has variant RustString::String.
fn bytes_to_string(&mut self) -> Result<(), std::str::Utf8Error> {
let mut owned = RustString::Null;
// temporarily swap a Null value into self; we'll swap that back
// shortly.
std::mem::swap(self, &mut owned);
match owned {
RustString::Bytes(bytes) => match String::from_utf8(bytes) {
Ok(string) => {
*self = RustString::String(string);
Ok(())
}
Err(e) => {
let (e, bytes) = (e.utf8_error(), e.into_bytes());
// put self back as we found it
*self = RustString::Bytes(bytes);
Err(e)
}
},
_ => {
// not bytes, so just swap back
std::mem::swap(self, &mut owned);
Ok(())
}
}
}
/// Convert the RustString, in place, into one of the C variants. If this is not
/// possible, such as if the string contains an embedded NUL, then the string
/// remains unchanged.
fn string_to_cstring(&mut self) {
let mut owned = RustString::Null;
// temporarily swap a Null value into self; we'll swap that back shortly
std::mem::swap(self, &mut owned);
match owned {
RustString::String(string) => {
match CString::new(string) {
Ok(cstring) => {
*self = RustString::CString(cstring);
}
Err(nul_err) => {
// recover the underlying String from the NulError and restore
// the RustString
let original_bytes = nul_err.into_vec();
// SAFETY: original_bytes came from a String moments ago, so still valid utf8
let string = unsafe { String::from_utf8_unchecked(original_bytes) };
*self = RustString::String(string);
}
}
}
_ => {
// not a CString, so just swap back
std::mem::swap(self, &mut owned);
}
}
}
pub(crate) fn to_path_buf_mut(&mut self) -> Result<PathBuf, std::str::Utf8Error> {
#[cfg(unix)]
let path: OsString = {
// on UNIX, we can use the bytes directly, without requiring that they
// be valid UTF-8.
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
OsStr::from_bytes(self.as_bytes()).to_os_string()
};
#[cfg(windows)]
let path: OsString = {
// on Windows, we assume the filename is valid Unicode, so it can be
// represented as UTF-8.
OsString::from(self.as_str()?.to_string())
};
Ok(path.into())
}
}
impl<'a> From<String> for RustString<'a> {
fn from(string: String) -> RustString<'a> {
RustString::String(string)
}
}
impl<'a> From<&str> for RustString<'static> {
fn from(string: &str) -> RustString<'static> {
RustString::String(string.to_string())
}
}
/// Utility function to borrow a TCString from a pointer arg, modify it,
/// and restore it.
///
/// This implements a kind of "interior mutability", relying on the
/// single-threaded use of all TC* types.
///
/// # SAFETY
///
/// - tcstring must not be NULL
/// - *tcstring must be a valid TCString
/// - *tcstring must not be accessed by anything else, despite the *const
unsafe fn wrap<T, F>(tcstring: *const TCString, f: F) -> T
where
F: FnOnce(&mut RustString) -> T,
{
debug_assert!(!tcstring.is_null());
// SAFETY:
// - we have exclusive to *tcstring (promised by caller)
let tcstring = tcstring as *mut TCString;
// SAFETY:
// - tcstring is not NULL
// - *tcstring is a valid string (promised by caller)
let mut rstring = unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) };
let rv = f(&mut rstring);
// update the caller's TCString with the updated RustString
// SAFETY:
// - tcstring is not NULL (we just took from it)
// - tcstring points to valid memory (we just took from it)
unsafe { TCString::val_to_arg_out(rstring, tcstring) };
rv
}
/// TCStringList represents a list of strings.
///
/// The content of this struct must be treated as read-only.
#[repr(C)]
pub struct TCStringList {
/// number of strings in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// TCStringList representing each string. these remain owned by the TCStringList instance and will
/// be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the
/// *TCStringList at indexes 0..len-1 are not NULL.
items: *mut TCString,
}
impl CList for TCStringList {
type Element = TCString;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCStringList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Create a new TCString referencing the given C string. The C string must remain valid and
/// unchanged until after the TCString is freed. It's typically easiest to ensure this by using a
/// static string.
///
/// NOTE: this function does _not_ take responsibility for freeing the given C string. The
/// given string can be freed once the TCString referencing it has been freed.
///
/// For example:
///
/// ```text
/// char *url = get_item_url(..); // dynamically allocate C string
/// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed
/// free(url); // string is no longer referenced and can be freed
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> TCString {
debug_assert!(!cstr.is_null());
// SAFETY:
// - cstr is not NULL (promised by caller, verified by assertion)
// - cstr's lifetime exceeds that of the TCString (promised by caller)
// - cstr contains a valid NUL terminator (promised by caller)
// - cstr's content will not change before it is destroyed (promised by caller)
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::CStr(cstr)) }
}
/// Create a new TCString by cloning the content of the given C string. The resulting TCString
/// is independent of the given string, which can be freed or overwritten immediately.
#[no_mangle]
pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> TCString {
debug_assert!(!cstr.is_null());
// SAFETY:
// - cstr is not NULL (promised by caller, verified by assertion)
// - cstr's lifetime exceeds that of this function (by C convention)
// - cstr contains a valid NUL terminator (promised by caller)
// - cstr's content will not change before it is destroyed (by C convention)
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
let cstring: CString = cstr.into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::CString(cstring)) }
}
/// Create a new TCString containing the given string with the given length. This allows creation
/// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting
/// TCString is independent of the passed buffer, which may be reused or freed immediately.
///
/// The length should _not_ include any trailing NUL.
///
/// The given length must be less than half the maximum value of usize.
#[no_mangle]
pub unsafe extern "C" fn tc_string_clone_with_len(
buf: *const libc::c_char,
len: usize,
) -> TCString {
debug_assert!(!buf.is_null());
debug_assert!(len < isize::MAX as usize);
// SAFETY:
// - buf is valid for len bytes (by C convention)
// - (no alignment requirements for a byte slice)
// - content of buf will not be mutated during the lifetime of this slice (lifetime
// does not outlive this function call)
// - the length of the buffer is less than isize::MAX (promised by caller)
let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) };
// allocate and copy into Rust-controlled memory
let vec = slice.to_vec();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::Bytes(vec)) }
}
/// Get the content of the string as a regular C string. The given string must be valid. The
/// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The
/// returned C string is valid until the TCString is freed or passed to another TC API function.
///
/// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is
/// valid and NUL-free.
///
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
/// terminator. The pointer must not be NULL.
///
/// This function does _not_ take ownership of the TCString.
#[no_mangle]
pub unsafe extern "C" fn tc_string_content(tcstring: *const TCString) -> *const libc::c_char {
// SAFETY;
// - tcstring is not NULL (promised by caller)
// - *tcstring is valid (promised by caller)
// - *tcstring is not accessed concurrently (single-threaded)
unsafe {
wrap(tcstring, |rstring| {
// try to eliminate the Bytes variant. If this fails, we'll return NULL
// below, so the error is ignorable.
let _ = rstring.bytes_to_string();
// and eliminate the String variant
rstring.string_to_cstring();
match &rstring {
RustString::CString(cstring) => cstring.as_ptr(),
RustString::String(_) => std::ptr::null(), // string_to_cstring failed
RustString::CStr(cstr) => cstr.as_ptr(),
RustString::Bytes(_) => std::ptr::null(), // already returned above
RustString::Null => unreachable!(),
}
})
}
}
/// Get the content of the string as a pointer and length. The given string must not be NULL.
/// This function can return any string, even one including NUL bytes or invalid UTF-8. The
/// returned buffer is valid until the TCString is freed or passed to another TaskChampio
/// function.
///
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
/// terminator. The pointer must not be NULL.
///
/// This function does _not_ take ownership of the TCString.
#[no_mangle]
pub unsafe extern "C" fn tc_string_content_with_len(
tcstring: *const TCString,
len_out: *mut usize,
) -> *const libc::c_char {
// SAFETY;
// - tcstring is not NULL (promised by caller)
// - *tcstring is valid (promised by caller)
// - *tcstring is not accessed concurrently (single-threaded)
unsafe {
wrap(tcstring, |rstring| {
let bytes = rstring.as_bytes();
// SAFETY:
// - len_out is not NULL (promised by caller)
// - len_out points to valid memory (promised by caller)
// - len_out is properly aligned (C convention)
usize::val_to_arg_out(bytes.len(), len_out);
bytes.as_ptr() as *const libc::c_char
})
}
}
/// Free a TCString. The given string must not be NULL. The string must not be used
/// after this function returns, and must not be freed more than once.
#[no_mangle]
pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) {
// SAFETY:
// - tcstring is not NULL (promised by caller)
// - caller is exclusive owner of tcstring (promised by caller)
drop(unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) });
}
/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList.
#[no_mangle]
pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) {
// SAFETY:
// - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcstrings) };
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn empty_list_has_non_null_pointer() {
let tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
assert!(!tcstrings.items.is_null());
assert_eq!(tcstrings.len, 0);
assert_eq!(tcstrings._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_string_list_free(&mut tcstrings) };
assert!(tcstrings.items.is_null());
assert_eq!(tcstrings.len, 0);
assert_eq!(tcstrings._capacity, 0);
}
const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28";
fn make_cstring() -> RustString<'static> {
RustString::CString(CString::new("a string").unwrap())
}
fn make_cstr() -> RustString<'static> {
let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap();
RustString::CStr(&cstr)
}
fn make_string() -> RustString<'static> {
RustString::String("a string".into())
}
fn make_string_with_nul() -> RustString<'static> {
RustString::String("a \0 nul!".into())
}
fn make_invalid_bytes() -> RustString<'static> {
RustString::Bytes(INVALID_UTF8.to_vec())
}
fn make_bytes() -> RustString<'static> {
RustString::Bytes(b"bytes".to_vec())
}
#[test]
fn cstring_as_str() {
assert_eq!(make_cstring().as_str().unwrap(), "a string");
}
#[test]
fn cstr_as_str() {
assert_eq!(make_cstr().as_str().unwrap(), "a string");
}
#[test]
fn string_as_str() {
assert_eq!(make_string().as_str().unwrap(), "a string");
}
#[test]
fn string_with_nul_as_str() {
assert_eq!(make_string_with_nul().as_str().unwrap(), "a \0 nul!");
}
#[test]
fn invalid_bytes_as_str() {
let as_str_err = make_invalid_bytes().as_str().unwrap_err();
assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid
}
#[test]
fn valid_bytes_as_str() {
assert_eq!(make_bytes().as_str().unwrap(), "bytes");
}
#[test]
fn cstring_as_bytes() {
assert_eq!(make_cstring().as_bytes(), b"a string");
}
#[test]
fn cstr_as_bytes() {
assert_eq!(make_cstr().as_bytes(), b"a string");
}
#[test]
fn string_as_bytes() {
assert_eq!(make_string().as_bytes(), b"a string");
}
#[test]
fn string_with_nul_as_bytes() {
assert_eq!(make_string_with_nul().as_bytes(), b"a \0 nul!");
}
#[test]
fn invalid_bytes_as_bytes() {
assert_eq!(make_invalid_bytes().as_bytes(), INVALID_UTF8);
}
#[test]
fn cstring_string_to_cstring() {
let mut tcstring = make_cstring();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstring()); // unchanged
}
#[test]
fn cstr_string_to_cstring() {
let mut tcstring = make_cstr();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstr()); // unchanged
}
#[test]
fn string_string_to_cstring() {
let mut tcstring = make_string();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstring()); // converted to CString, same content
}
#[test]
fn string_with_nul_string_to_cstring() {
let mut tcstring = make_string_with_nul();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_string_with_nul()); // unchanged
}
#[test]
fn bytes_string_to_cstring() {
let mut tcstring = make_bytes();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_bytes()); // unchanged
}
}

View file

@ -0,0 +1,934 @@
use crate::traits::*;
use crate::types::*;
use crate::util::err_to_ruststring;
use std::convert::TryFrom;
use std::ops::Deref;
use std::ptr::NonNull;
use std::str::FromStr;
use taskchampion::chrono::{TimeZone, Utc};
use taskchampion::{Annotation, Tag, Task, TaskMut, Uuid};
/// A task, as publicly exposed by this library.
///
/// A task begins in "immutable" mode. It must be converted to "mutable" mode
/// to make any changes, and doing so requires exclusive access to the replica
/// until the task is freed or converted back to immutable mode.
///
/// An immutable task carries no reference to the replica that created it, and can be used until it
/// is freed or converted to a TaskMut. A mutable task carries a reference to the replica and
/// must be freed or made immutable before the replica is freed.
///
/// All `tc_task_..` functions taking a task as an argument require that it not be NULL.
///
/// When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then
/// `tc_task_error` will return the error message.
///
/// # Safety
///
/// A task is an owned object, and must be freed with tc_task_free (or, if part of a list,
/// with tc_task_list_free).
///
/// Any function taking a `*TCTask` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function;
/// - the memory referenced by the pointer must never be modified by C code; and
/// - except for `tc_{task,task_list}_free`, ownership of a `*TCTask` remains with the caller.
///
/// Once passed to tc_task_free, a `*TCTask` becomes invalid and must not be used again.
///
/// TCTasks are not threadsafe.
pub struct TCTask {
/// The wrapped Task or TaskMut
inner: Inner,
/// The error from the most recent operation, if any
error: Option<RustString<'static>>,
}
enum Inner {
/// A regular, immutable task
Immutable(Task),
/// A mutable task, together with the replica to which it holds an exclusive
/// reference.
Mutable(TaskMut<'static>, *mut TCReplica),
/// A transitional state for a TCTask as it goes from mutable to immutable and back. A task
/// can only be in this state outside of [`to_mut`] and [`to_immut`] if a panic occurs during
/// one of those methods.
Invalid,
}
impl PassByPointer for TCTask {}
impl TCTask {
/// Make an immutable TCTask into a mutable TCTask. Does nothing if the task
/// is already mutable.
///
/// # Safety
///
/// The tcreplica pointer must not be NULL, and the replica it points to must not
/// be freed before TCTask.to_immut completes.
unsafe fn to_mut(&mut self, tcreplica: *mut TCReplica) {
self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) {
Inner::Immutable(task) => {
// SAFETY:
// - tcreplica is not null (promised by caller)
// - tcreplica outlives the pointer in this variant (promised by caller)
let tcreplica_ref: &mut TCReplica =
unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) };
let rep_ref = tcreplica_ref.borrow_mut();
Inner::Mutable(task.into_mut(rep_ref), tcreplica)
}
Inner::Mutable(task, tcreplica) => Inner::Mutable(task, tcreplica),
Inner::Invalid => unreachable!(),
}
}
/// Make an mutable TCTask into a immutable TCTask. Does nothing if the task
/// is already immutable.
#[allow(clippy::wrong_self_convention)] // to_immut_mut is not better!
fn to_immut(&mut self) {
self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) {
Inner::Immutable(task) => Inner::Immutable(task),
Inner::Mutable(task, tcreplica) => {
// SAFETY:
// - tcreplica is not null (promised by caller of to_mut, which created this
// variant)
// - tcreplica is still alive (promised by caller of to_mut)
let tcreplica_ref: &mut TCReplica =
unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) };
tcreplica_ref.release_borrow();
Inner::Immutable(task.into_immut())
}
Inner::Invalid => unreachable!(),
}
}
}
impl From<Task> for TCTask {
fn from(task: Task) -> TCTask {
TCTask {
inner: Inner::Immutable(task),
error: None,
}
}
}
/// Utility function to get a shared reference to the underlying Task. All Task getters
/// are error-free, so this does not handle errors.
fn wrap<T, F>(task: *mut TCTask, f: F) -> T
where
F: FnOnce(&Task) -> T,
{
// SAFETY:
// - task is not null (promised by caller)
// - task outlives this function (promised by caller)
let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) };
let task: &Task = match &tctask.inner {
Inner::Immutable(t) => t,
Inner::Mutable(t, _) => t.deref(),
Inner::Invalid => unreachable!(),
};
tctask.error = None;
f(task)
}
/// Utility function to get a mutable reference to the underlying Task. The
/// TCTask must be mutable. The inner function may use `?` syntax to return an
/// error, which will be represented with the `err_value` returned to C.
fn wrap_mut<T, F>(task: *mut TCTask, f: F, err_value: T) -> T
where
F: FnOnce(&mut TaskMut) -> anyhow::Result<T>,
{
// SAFETY:
// - task is not null (promised by caller)
// - task outlives this function (promised by caller)
let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) };
let task: &mut TaskMut = match tctask.inner {
Inner::Immutable(_) => panic!("Task is immutable"),
Inner::Mutable(ref mut t, _) => t,
Inner::Invalid => unreachable!(),
};
tctask.error = None;
match f(task) {
Ok(rv) => rv,
Err(e) => {
tctask.error = Some(err_to_ruststring(e));
err_value
}
}
}
impl TryFrom<RustString<'static>> for Tag {
type Error = anyhow::Error;
fn try_from(mut rstring: RustString) -> Result<Tag, anyhow::Error> {
let tagstr = rstring.as_str()?;
Tag::from_str(tagstr)
}
}
/// TCTaskList represents a list of tasks.
///
/// The content of this struct must be treated as read-only: no fields or anything they reference
/// should be modified directly by C code.
///
/// When an item is taken from this list, its pointer in `items` is set to NULL.
#[repr(C)]
pub struct TCTaskList {
/// number of tasks in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// array of pointers representing each task. these remain owned by the TCTaskList instance and
/// will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList,
/// and the *TCTaskList at indexes 0..len-1 are not NULL.
items: *mut Option<NonNull<TCTask>>,
}
impl CList for TCTaskList {
type Element = Option<NonNull<TCTask>>;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCTaskList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Convert an immutable task into a mutable task.
///
/// The task must not be NULL. It is modified in-place, and becomes mutable.
///
/// The replica must not be NULL. After this function returns, the replica _cannot be used at all_
/// until this task is made immutable again. This implies that it is not allowed for more than one
/// task associated with a replica to be mutable at any time.
///
/// Typical mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`:
///
/// ```c
/// tc_task_to_mut(task, rep);
/// success = tc_task_done(task);
/// tc_task_to_immut(task, rep);
/// if (!success) { ... }
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_task_to_mut(task: *mut TCTask, tcreplica: *mut TCReplica) {
// SAFETY:
// - task is not null (promised by caller)
// - task outlives 'a (promised by caller)
let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) };
// SAFETY:
// - tcreplica is not NULL (promised by caller)
// - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller,
// who cannot call tc_replica_free during this time)
unsafe { tctask.to_mut(tcreplica) };
}
/// Convert a mutable task into an immutable task.
///
/// The task must not be NULL. It is modified in-place, and becomes immutable.
///
/// The replica passed to `tc_task_to_mut` may be used freely after this call.
#[no_mangle]
pub unsafe extern "C" fn tc_task_to_immut(task: *mut TCTask) {
// SAFETY:
// - task is not null (promised by caller)
// - task outlives 'a (promised by caller)
let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) };
tctask.to_immut();
}
/// Get a task's UUID.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid {
wrap(task, |task| {
// SAFETY:
// - value is not allocated and need not be freed
unsafe { TCUuid::return_val(task.get_uuid()) }
})
}
/// Get a task's status.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_status(task: *mut TCTask) -> TCStatus {
wrap(task, |task| task.get_status().into())
}
/// Get the underlying key/value pairs for this task. The returned TCKVList is
/// a "snapshot" of the task and will not be updated if the task is subsequently
/// modified. It is the caller's responsibility to free the TCKVList.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList {
wrap(task, |task| {
let vec: Vec<TCKV> = task
.get_taskmap()
.iter()
.map(|(k, v)| {
let key = RustString::from(k.as_ref());
let value = RustString::from(v.as_ref());
TCKV::as_ctype((key, value))
})
.collect();
// SAFETY:
// - caller will free this list
unsafe { TCKVList::return_val(vec) }
})
}
/// Get a task's description, or NULL if the task cannot be represented as a C string (e.g., if it
/// contains embedded NUL characters).
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> TCString {
wrap(task, |task| {
let descr = task.get_description();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(descr.into()) }
})
}
/// Get the entry timestamp for a task (when it was created), or 0 if not set.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_entry(task: *mut TCTask) -> libc::time_t {
wrap(task, |task| libc::time_t::as_ctype(task.get_entry()))
}
/// Get the wait timestamp for a task, or 0 if not set.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_wait(task: *mut TCTask) -> libc::time_t {
wrap(task, |task| libc::time_t::as_ctype(task.get_wait()))
}
/// Get the modified timestamp for a task, or 0 if not set.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_modified(task: *mut TCTask) -> libc::time_t {
wrap(task, |task| libc::time_t::as_ctype(task.get_modified()))
}
/// Check if a task is waiting.
#[no_mangle]
pub unsafe extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool {
wrap(task, |task| task.is_waiting())
}
/// Check if a task is active (started and not stopped).
#[no_mangle]
pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool {
wrap(task, |task| task.is_active())
}
/// Check if a task has the given tag. If the tag is invalid, this function will return false, as
/// that (invalid) tag is not present. No error will be reported via `tc_task_error`.
#[no_mangle]
pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: TCString) -> bool {
// SAFETY:
// - tag is valid (promised by caller)
// - caller will not use tag after this call (convention)
let tcstring = unsafe { TCString::val_from_arg(tag) };
wrap(task, |task| {
if let Ok(tag) = Tag::try_from(tcstring) {
task.has_tag(&tag)
} else {
false
}
})
}
/// Get the tags for the task.
///
/// The caller must free the returned TCStringList instance. The TCStringList instance does not
/// reference the task and the two may be freed in any order.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_tags(task: *mut TCTask) -> TCStringList {
wrap(task, |task| {
let vec: Vec<TCString> = task
.get_tags()
.map(|t| {
// SAFETY:
// - this TCString will be freed via tc_string_list_free.
unsafe { TCString::return_val(t.as_ref().into()) }
})
.collect();
// SAFETY:
// - caller will free the list
unsafe { TCStringList::return_val(vec) }
})
}
/// Get the annotations for the task.
///
/// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not
/// reference the task and the two may be freed in any order.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_annotations(task: *mut TCTask) -> TCAnnotationList {
wrap(task, |task| {
let vec: Vec<TCAnnotation> = task
.get_annotations()
.map(|a| {
let description = RustString::from(a.description);
TCAnnotation::as_ctype((a.entry, description))
})
.collect();
// SAFETY:
// - caller will free the list
unsafe { TCAnnotationList::return_val(vec) }
})
}
/// Get the named UDA from the task.
///
/// Returns a TCString with NULL ptr field if the UDA does not exist.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_uda(
task: *mut TCTask,
ns: TCString,
key: TCString,
) -> TCString {
wrap(task, |task| {
// SAFETY:
// - ns is valid (promised by caller)
// - caller will not use ns after this call (convention)
if let Ok(ns) = unsafe { TCString::val_from_arg(ns) }.as_str() {
// SAFETY: same
if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() {
if let Some(value) = task.get_uda(ns, key) {
// SAFETY:
// - caller will free this string (caller promises)
return unsafe { TCString::return_val(value.into()) };
}
}
}
TCString::default()
})
}
/// Get the named legacy UDA from the task.
///
/// Returns NULL if the UDA does not exist.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_legacy_uda(task: *mut TCTask, key: TCString) -> TCString {
wrap(task, |task| {
// SAFETY:
// - key is valid (promised by caller)
// - caller will not use key after this call (convention)
if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() {
if let Some(value) = task.get_legacy_uda(key) {
// SAFETY:
// - caller will free this string (caller promises)
return unsafe { TCString::return_val(value.into()) };
}
}
TCString::default()
})
}
/// Get all UDAs for this task.
///
/// Legacy UDAs are represented with an empty string in the ns field.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUdaList {
wrap(task, |task| {
let vec: Vec<TCUda> = task
.get_udas()
.map(|((ns, key), value)| {
// SAFETY:
// - will be freed by tc_uda_list_free
unsafe {
TCUda::return_val(Uda {
ns: Some(ns.into()),
key: key.into(),
value: value.into(),
})
}
})
.collect();
// SAFETY:
// - caller will free this list
unsafe { TCUdaList::return_val(vec) }
})
}
/// Get all UDAs for this task.
///
/// All TCUdas in this list have a NULL ns field. The entire UDA key is
/// included in the key field. The caller must free the returned list.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUdaList {
wrap(task, |task| {
let vec: Vec<TCUda> = task
.get_legacy_udas()
.map(|(key, value)| {
// SAFETY:
// - will be freed by tc_uda_list_free
unsafe {
TCUda::return_val(Uda {
ns: None,
key: key.into(),
value: value.into(),
})
}
})
.collect();
// SAFETY:
// - caller will free this list
unsafe { TCUdaList::return_val(vec) }
})
}
/// Set a mutable task's status.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_status(task: *mut TCTask, status: TCStatus) -> TCResult {
wrap_mut(
task,
|task| {
task.set_status(status.into())?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a mutable task's description.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_description(
task: *mut TCTask,
description: TCString,
) -> TCResult {
// SAFETY:
// - description is valid (promised by caller)
// - caller will not use description after this call (convention)
let mut description = unsafe { TCString::val_from_arg(description) };
wrap_mut(
task,
|task| {
task.set_description(description.as_str()?.to_string())?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a mutable task's entry (creation time). Pass entry=0 to unset
/// the entry field.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> TCResult {
wrap_mut(
task,
|task| {
// SAFETY: any time_t value is a valid timestamp
task.set_entry(unsafe { entry.from_ctype() })?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCResult {
wrap_mut(
task,
|task| {
// SAFETY: any time_t value is a valid timestamp
task.set_wait(unsafe { wait.from_ctype() })?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a mutable task's modified timestamp. The value cannot be zero.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_modified(
task: *mut TCTask,
modified: libc::time_t,
) -> TCResult {
wrap_mut(
task,
|task| {
task.set_modified(
// SAFETY: any time_t value is a valid timestamp
unsafe { modified.from_ctype() }
.ok_or_else(|| anyhow::anyhow!("modified cannot be zero"))?,
)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Start a task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult {
wrap_mut(
task,
|task| {
task.start()?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Stop a task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult {
wrap_mut(
task,
|task| {
task.stop()?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Mark a task as done.
#[no_mangle]
pub unsafe extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult {
wrap_mut(
task,
|task| {
task.done()?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Mark a task as deleted.
#[no_mangle]
pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult {
wrap_mut(
task,
|task| {
task.delete()?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Add a tag to a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: TCString) -> TCResult {
// SAFETY:
// - tag is valid (promised by caller)
// - caller will not use tag after this call (convention)
let tcstring = unsafe { TCString::val_from_arg(tag) };
wrap_mut(
task,
|task| {
let tag = Tag::try_from(tcstring)?;
task.add_tag(&tag)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove a tag from a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: TCString) -> TCResult {
// SAFETY:
// - tag is valid (promised by caller)
// - caller will not use tag after this call (convention)
let tcstring = unsafe { TCString::val_from_arg(tag) };
wrap_mut(
task,
|task| {
let tag = Tag::try_from(tcstring)?;
task.remove_tag(&tag)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Add an annotation to a mutable task. This call takes ownership of the
/// passed annotation, which must not be used after the call returns.
#[no_mangle]
pub unsafe extern "C" fn tc_task_add_annotation(
task: *mut TCTask,
annotation: *mut TCAnnotation,
) -> TCResult {
// SAFETY:
// - annotation is not NULL (promised by caller)
// - annotation is return from a tc_string_.. so is valid
// - caller will not use annotation after this call
let (entry, description) =
unsafe { TCAnnotation::take_val_from_arg(annotation, TCAnnotation::default()) };
wrap_mut(
task,
|task| {
let description = description.into_string()?;
task.add_annotation(Annotation { entry, description })?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove an annotation from a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64) -> TCResult {
wrap_mut(
task,
|task| {
task.remove_annotation(Utc.timestamp(entry, 0))?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a UDA on a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_uda(
task: *mut TCTask,
ns: TCString,
key: TCString,
value: TCString,
) -> TCResult {
// safety:
// - ns is valid (promised by caller)
// - caller will not use ns after this call (convention)
let mut ns = unsafe { TCString::val_from_arg(ns) };
// SAFETY: same
let mut key = unsafe { TCString::val_from_arg(key) };
// SAFETY: same
let mut value = unsafe { TCString::val_from_arg(value) };
wrap_mut(
task,
|task| {
task.set_uda(ns.as_str()?, key.as_str()?, value.as_str()?.to_string())?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove a UDA fraom a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_uda(
task: *mut TCTask,
ns: TCString,
key: TCString,
) -> TCResult {
// safety:
// - ns is valid (promised by caller)
// - caller will not use ns after this call (convention)
let mut ns = unsafe { TCString::val_from_arg(ns) };
// SAFETY: same
let mut key = unsafe { TCString::val_from_arg(key) };
wrap_mut(
task,
|task| {
task.remove_uda(ns.as_str()?, key.as_str()?)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Set a legacy UDA on a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_set_legacy_uda(
task: *mut TCTask,
key: TCString,
value: TCString,
) -> TCResult {
// safety:
// - key is valid (promised by caller)
// - caller will not use key after this call (convention)
let mut key = unsafe { TCString::val_from_arg(key) };
// SAFETY: same
let mut value = unsafe { TCString::val_from_arg(value) };
wrap_mut(
task,
|task| {
task.set_legacy_uda(key.as_str()?.to_string(), value.as_str()?.to_string())?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove a UDA fraom a mutable task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_legacy_uda(task: *mut TCTask, key: TCString) -> TCResult {
// safety:
// - key is valid (promised by caller)
// - caller will not use key after this call (convention)
let mut key = unsafe { TCString::val_from_arg(key) };
wrap_mut(
task,
|task| {
task.remove_legacy_uda(key.as_str()?.to_string())?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Get all dependencies for a task.
#[no_mangle]
pub unsafe extern "C" fn tc_task_get_dependencies(task: *mut TCTask) -> TCUuidList {
wrap(task, |task| {
let vec: Vec<TCUuid> = task
.get_dependencies()
.map(|u| {
// SAFETY:
// - value is not allocated
unsafe { TCUuid::return_val(u) }
})
.collect();
// SAFETY:
// - caller will free this list
unsafe { TCUuidList::return_val(vec) }
})
}
/// Add a dependency.
#[no_mangle]
pub unsafe extern "C" fn tc_task_add_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) };
wrap_mut(
task,
|task| {
task.add_dependency(dep)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Remove a dependency.
#[no_mangle]
pub unsafe extern "C" fn tc_task_remove_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) };
wrap_mut(
task,
|task| {
task.remove_dependency(dep)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
/// Get the latest error for a task, or a string NULL ptr field if the last operation succeeded.
/// Subsequent calls to this function will return NULL. The task pointer must not be NULL. The
/// caller must free the returned string.
#[no_mangle]
pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> TCString {
// SAFETY:
// - task is not null (promised by caller)
// - task outlives 'a (promised by caller)
let task: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) };
if let Some(rstring) = task.error.take() {
// SAFETY:
// - caller promises to free this value
unsafe { TCString::return_val(rstring) }
} else {
TCString::default()
}
}
/// Free a task. The given task must not be NULL. The task must not be used after this function
/// returns, and must not be freed more than once.
///
/// If the task is currently mutable, it will first be made immutable.
#[no_mangle]
pub unsafe extern "C" fn tc_task_free(task: *mut TCTask) {
// SAFETY:
// - task is not NULL (promised by caller)
// - caller will not use the TCTask after this (promised by caller)
let mut tctask = unsafe { TCTask::take_from_ptr_arg(task) };
// convert to immut if it was mutable
tctask.to_immut();
drop(tctask);
}
/// Take an item from a TCTaskList. After this call, the indexed item is no longer associated
/// with the list and becomes the caller's responsibility, just as if it had been returned from
/// `tc_replica_get_task`.
///
/// The corresponding element in the `items` array will be set to NULL. If that field is already
/// NULL (that is, if the item has already been taken), this function will return NULL. If the
/// index is out of bounds, this function will also return NULL.
///
/// The passed TCTaskList remains owned by the caller.
#[no_mangle]
pub unsafe extern "C" fn tc_task_list_take(tasks: *mut TCTaskList, index: usize) -> *mut TCTask {
// SAFETY:
// - tasks is not NULL and points to a valid TCTaskList (caller is not allowed to
// modify the list directly, and tc_task_list_take leaves the list valid)
let p = unsafe { take_optional_pointer_list_item(tasks, index) };
if let Some(p) = p {
p.as_ptr()
} else {
std::ptr::null_mut()
}
}
/// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList.
#[no_mangle]
pub unsafe extern "C" fn tc_task_list_free(tasks: *mut TCTaskList) {
// SAFETY:
// - tasks is not NULL and points to a valid TCTaskList (caller is not allowed to
// modify the list directly, and tc_task_list_take leaves the list valid)
// - caller promises not to use the value after return
unsafe { drop_optional_pointer_list(tasks) };
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tasks = unsafe { TCTaskList::return_val(Vec::new()) };
assert!(!tasks.items.is_null());
assert_eq!(tasks.len, 0);
assert_eq!(tasks._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tasks = unsafe { TCTaskList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_task_list_free(&mut tasks) };
assert!(tasks.items.is_null());
assert_eq!(tasks.len, 0);
assert_eq!(tasks._capacity, 0);
}
}

View file

@ -0,0 +1,353 @@
use crate::util::vec_into_raw_parts;
use std::ptr::NonNull;
/// Support for values passed to Rust by value. These are represented as full structs in C. Such
/// values are implicitly copyable, via C's struct assignment.
///
/// The Rust and C types may differ, with from_ctype and as_ctype converting between them.
/// Implement this trait for the C type.
///
/// The RustType must be droppable (not containing raw pointers).
pub(crate) trait PassByValue: Sized {
type RustType;
/// Convert a C value to a Rust value.
///
/// # Safety
///
/// `self` must be a valid CType.
#[allow(clippy::wrong_self_convention)]
unsafe fn from_ctype(self) -> Self::RustType;
/// Convert a Rust value to a C value.
fn as_ctype(arg: Self::RustType) -> Self;
/// Take a value from C as an argument.
///
/// # Safety
///
/// - `self` must be a valid instance of the C type. This is typically ensured either by
/// requiring that C code not modify it, or by defining the valid values in C comments.
unsafe fn val_from_arg(arg: Self) -> Self::RustType {
// SAFETY:
// - arg is a valid CType (promised by caller)
unsafe { arg.from_ctype() }
}
/// Take a value from C as a pointer argument, replacing it with the given value. This is used
/// to invalidate the C value as an additional assurance against subsequent use of the value.
///
/// # Safety
///
/// - arg must not be NULL
/// - *arg must be a valid, properly aligned instance of the C type
unsafe fn take_val_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType {
// SAFETY:
// - arg is valid (promised by caller)
// - replacement is valid and aligned (guaranteed by Rust)
unsafe { std::ptr::swap(arg, &mut replacement) };
// SAFETY:
// - replacement (formerly *arg) is a valid CType (promised by caller)
unsafe { PassByValue::val_from_arg(replacement) }
}
/// Return a value to C
///
/// # Safety
///
/// - if the value is allocated, the caller must ensure that the value is eventually freed
unsafe fn return_val(arg: Self::RustType) -> Self {
Self::as_ctype(arg)
}
/// Return a value to C, via an "output parameter"
///
/// # Safety
///
/// - `arg_out` must not be NULL and must be properly aligned and pointing to valid memory
/// of the size of CType.
unsafe fn val_to_arg_out(val: Self::RustType, arg_out: *mut Self) {
debug_assert!(!arg_out.is_null());
// SAFETY:
// - arg_out is not NULL (promised by caller, asserted)
// - arg_out is properly aligned and points to valid memory (promised by caller)
unsafe { *arg_out = Self::as_ctype(val) };
}
}
/// Support for values passed to Rust by pointer. These are represented as opaque structs in C,
/// and always handled as pointers.
pub(crate) trait PassByPointer: Sized {
/// Take a value from C as an argument.
///
/// # Safety
///
/// - arg must not be NULL
/// - arg must be a value returned from Box::into_raw (via return_ptr or ptr_to_arg_out)
/// - arg becomes invalid and must not be used after this call
unsafe fn take_from_ptr_arg(arg: *mut Self) -> Self {
debug_assert!(!arg.is_null());
// SAFETY: see docstring
unsafe { *(Box::from_raw(arg)) }
}
/// Borrow a value from C as an argument.
///
/// # Safety
///
/// - arg must not be NULL
/// - *arg must be a valid instance of Self
/// - arg must be valid for the lifetime assigned by the caller
/// - arg must not be modified by anything else during that lifetime
unsafe fn from_ptr_arg_ref<'a>(arg: *const Self) -> &'a Self {
debug_assert!(!arg.is_null());
// SAFETY: see docstring
unsafe { &*arg }
}
/// Mutably borrow a value from C as an argument.
///
/// # Safety
///
/// - arg must not be NULL
/// - *arg must be a valid instance of Self
/// - arg must be valid for the lifetime assigned by the caller
/// - arg must not be accessed by anything else during that lifetime
unsafe fn from_ptr_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self {
debug_assert!(!arg.is_null());
// SAFETY: see docstring
unsafe { &mut *arg }
}
/// Return a value to C, transferring ownership
///
/// # Safety
///
/// - the caller must ensure that the value is eventually freed
unsafe fn return_ptr(self) -> *mut Self {
Box::into_raw(Box::new(self))
}
/// Return a value to C, transferring ownership, via an "output parameter".
///
/// # Safety
///
/// - the caller must ensure that the value is eventually freed
/// - arg_out must not be NULL
/// - arg_out must point to valid, properly aligned memory for a pointer value
unsafe fn ptr_to_arg_out(self, arg_out: *mut *mut Self) {
debug_assert!(!arg_out.is_null());
// SAFETY: see docstring
unsafe { *arg_out = self.return_ptr() };
}
}
/// Support for C lists of objects referenced by value.
///
/// The underlying C type should have three fields, containing items, length, and capacity. The
/// required trait functions just fetch and set these fields.
///
/// The PassByValue trait will be implemented automatically, converting between the C type and
/// `Vec<Element>`.
///
/// The element type can be PassByValue or PassByPointer. If the latter, it should use either
/// `NonNull<T>` or `Option<NonNull<T>>` to represent the element. The latter is an "optional
/// pointer list", where elements can be omitted.
///
/// For most cases, it is only necessary to implement `tc_.._free` that calls one of the
/// drop_..._list functions.
///
/// # Safety
///
/// The C type must be documented as read-only. None of the fields may be modified, nor anything
/// accessible via the `items` array. The exception is modification via "taking" elements.
///
/// This class guarantees that the items pointer is non-NULL for any valid list (even when len=0).
pub(crate) trait CList: Sized {
type Element;
/// Create a new CList from the given items, len, and capacity.
///
/// # Safety
///
/// The arguments must either:
/// - be NULL, 0, and 0, respectively; or
/// - be valid for Vec::from_raw_parts
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self;
/// Return a mutable slice representing the elements in this list.
fn slice(&mut self) -> &mut [Self::Element];
/// Get the items, len, and capacity (in that order) for this instance. These must be
/// precisely the same values passed tearlier to `from_raw_parts`.
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize);
/// Generate a NULL value. By default this is a NULL items pointer with zero length and
/// capacity.
fn null_value() -> Self {
// SAFETY:
// - satisfies the first case in from_raw_parts' safety documentation
unsafe { Self::from_raw_parts(std::ptr::null_mut(), 0, 0) }
}
}
/// Given a CList containing pass-by-value values, drop all of the values and
/// the list.
///
/// This is a convenience function for `tc_.._list_free` functions.
///
/// # Safety
///
/// - List must be non-NULL and point to a valid CL instance
/// - The caller must not use the value array points to after this function, as
/// it has been freed. It will be replaced with the null value.
pub(crate) unsafe fn drop_value_list<CL, T>(list: *mut CL)
where
CL: CList<Element = T>,
T: PassByValue,
{
debug_assert!(!list.is_null());
// SAFETY:
// - *list is a valid CL (promised by caller)
let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) };
// first, drop each of the elements in turn
for e in vec.drain(..) {
// SAFETY:
// - e is a valid Element (promised by caller)
// - e is owned
drop(unsafe { PassByValue::val_from_arg(e) });
}
// then drop the vector
drop(vec);
}
/// Given a CList containing NonNull pointers, drop all of the pointed-to values and the list.
///
/// This is a convenience function for `tc_.._list_free` functions.
///
/// # Safety
///
/// - List must be non-NULL and point to a valid CL instance
/// - The caller must not use the value array points to after this function, as
/// it has been freed. It will be replaced with the null value.
#[allow(dead_code)] // this was useful once, and might be again?
pub(crate) unsafe fn drop_pointer_list<CL, T>(list: *mut CL)
where
CL: CList<Element = NonNull<T>>,
T: PassByPointer,
{
debug_assert!(!list.is_null());
// SAFETY:
// - *list is a valid CL (promised by caller)
let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) };
// first, drop each of the elements in turn
for e in vec.drain(..) {
// SAFETY:
// - e is a valid Element (promised by caller)
// - e is owned
drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) });
}
// then drop the vector
drop(vec);
}
/// Given a CList containing optional pointers, drop all of the non-null pointed-to values and the
/// list.
///
/// This is a convenience function for `tc_.._list_free` functions, for lists from which items
/// can be taken.
///
/// # Safety
///
/// - List must be non-NULL and point to a valid CL instance
/// - The caller must not use the value array points to after this function, as
/// it has been freed. It will be replaced with the null value.
pub(crate) unsafe fn drop_optional_pointer_list<CL, T>(list: *mut CL)
where
CL: CList<Element = Option<NonNull<T>>>,
T: PassByPointer,
{
debug_assert!(!list.is_null());
// SAFETY:
// - *list is a valid CL (promised by caller)
let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) };
// first, drop each of the elements in turn
for e in vec.drain(..) {
if let Some(e) = e {
// SAFETY:
// - e is a valid Element (promised by caller)
// - e is owned
drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) });
}
}
// then drop the vector
drop(vec);
}
/// Take a value from an optional pointer list, returning the value and replacing its array
/// element with NULL.
///
/// This is a convenience function for `tc_.._list_take` functions, for lists from which items
/// can be taken.
///
/// The returned value will be None if the element has already been taken, or if the index is
/// out of bounds.
///
/// # Safety
///
/// - List must be non-NULL and point to a valid CL instance
pub(crate) unsafe fn take_optional_pointer_list_item<CL, T>(
list: *mut CL,
index: usize,
) -> Option<NonNull<T>>
where
CL: CList<Element = Option<NonNull<T>>>,
T: PassByPointer,
{
debug_assert!(!list.is_null());
// SAFETy:
// - list is properly aligned, dereferencable, and points to an initialized CL, since it is valid
// - the lifetime of the resulting reference is limited to this function, during which time
// nothing else refers to this memory.
let slice = unsafe { list.as_mut() }.unwrap().slice();
if let Some(elt_ref) = slice.get_mut(index) {
let mut rv = None;
if let Some(elt) = elt_ref.as_mut() {
rv = Some(*elt);
*elt_ref = None; // clear out the array element
}
rv
} else {
None // index out of bounds
}
}
impl<A> PassByValue for A
where
A: CList,
{
type RustType = Vec<A::Element>;
unsafe fn from_ctype(self) -> Self::RustType {
let (items, len, cap) = self.into_raw_parts();
debug_assert!(!items.is_null());
// SAFETY:
// - CList::from_raw_parts requires that items, len, and cap be valid for
// Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller)
// - CList::into_raw_parts returns precisely the values passed to from_raw_parts.
// - those parts are passed to Vec::from_raw_parts here.
unsafe { Vec::from_raw_parts(items as *mut _, len, cap) }
}
fn as_ctype(arg: Self::RustType) -> Self {
let (items, len, cap) = vec_into_raw_parts(arg);
// SAFETY:
// - satisfies the second case in from_raw_parts' safety documentation
unsafe { Self::from_raw_parts(items, len, cap) }
}
}

148
taskchampion/lib/src/uda.rs Normal file
View file

@ -0,0 +1,148 @@
use crate::traits::*;
use crate::types::*;
/// TCUda contains the details of a UDA.
#[repr(C)]
#[derive(Default)]
pub struct TCUda {
/// Namespace of the UDA. For legacy UDAs, this may have a NULL ptr field.
pub ns: TCString,
/// UDA key. Must not be NULL.
pub key: TCString,
/// Content of the UDA. Must not be NULL.
pub value: TCString,
}
pub(crate) struct Uda {
pub ns: Option<RustString<'static>>,
pub key: RustString<'static>,
pub value: RustString<'static>,
}
impl PassByValue for TCUda {
type RustType = Uda;
unsafe fn from_ctype(self) -> Self::RustType {
Uda {
ns: if self.ns.is_null() {
None
} else {
// SAFETY:
// - self is owned, so we can take ownership of this TCString
// - self.ns is a valid, non-null TCString (NULL just checked)
Some(unsafe { TCString::val_from_arg(self.ns) })
},
// SAFETY:
// - self is owned, so we can take ownership of this TCString
// - self.key is a valid, non-null TCString (see type docstring)
key: unsafe { TCString::val_from_arg(self.key) },
// SAFETY:
// - self is owned, so we can take ownership of this TCString
// - self.value is a valid, non-null TCString (see type docstring)
value: unsafe { TCString::val_from_arg(self.value) },
}
}
fn as_ctype(uda: Uda) -> Self {
TCUda {
// SAFETY: caller assumes ownership of this value
ns: if let Some(ns) = uda.ns {
unsafe { TCString::return_val(ns) }
} else {
TCString::default()
},
// SAFETY: caller assumes ownership of this value
key: unsafe { TCString::return_val(uda.key) },
// SAFETY: caller assumes ownership of this value
value: unsafe { TCString::return_val(uda.value) },
}
}
}
/// TCUdaList represents a list of UDAs.
///
/// The content of this struct must be treated as read-only.
#[repr(C)]
pub struct TCUdaList {
/// number of UDAs in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// array of UDAs. These remain owned by the TCUdaList instance and will be freed by
/// tc_uda_list_free. This pointer is never NULL for a valid TCUdaList.
items: *mut TCUda,
}
impl CList for TCUdaList {
type Element = TCUda;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCUdaList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Free a TCUda instance. The instance, and the TCStrings it contains, must not be used
/// after this call.
#[no_mangle]
pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUda) {
debug_assert!(!tcuda.is_null());
// SAFETY:
// - *tcuda is a valid TCUda (caller promises to treat it as read-only)
let uda = unsafe { TCUda::take_val_from_arg(tcuda, TCUda::default()) };
drop(uda);
}
/// Free a TCUdaList instance. The instance, and all TCUdas it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUdaList.
#[no_mangle]
pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUdaList) {
// SAFETY:
// - tcudas is not NULL and points to a valid TCUdaList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcudas) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tcudas = unsafe { TCUdaList::return_val(Vec::new()) };
assert!(!tcudas.items.is_null());
assert_eq!(tcudas.len, 0);
assert_eq!(tcudas._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcudas = unsafe { TCUdaList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_uda_list_free(&mut tcudas) };
assert!(tcudas.items.is_null());
assert_eq!(tcudas.len, 0);
assert_eq!(tcudas._capacity, 0);
}
}

View file

@ -0,0 +1,23 @@
use crate::string::RustString;
pub(crate) fn err_to_ruststring(e: impl std::string::ToString) -> RustString<'static> {
RustString::from(e.to_string())
}
/// An implementation of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap.
pub(crate) fn vec_into_raw_parts<T>(vec: Vec<T>) -> (*mut T, usize, usize) {
// emulate Vec::into_raw_parts():
// - disable dropping the Vec with ManuallyDrop
// - extract ptr, len, and capacity using those methods
let mut vec = std::mem::ManuallyDrop::new(vec);
(vec.as_mut_ptr(), vec.len(), vec.capacity())
}
/// An implementation of String::into_raw_parts, which is still unstable. Returns ptr, len, cap.
pub(crate) fn string_into_raw_parts(string: String) -> (*mut u8, usize, usize) {
// emulate String::into_raw_parts():
// - disable dropping the String with ManuallyDrop
// - extract ptr, len, and capacity using those methods
let mut string = std::mem::ManuallyDrop::new(string);
(string.as_mut_ptr(), string.len(), string.capacity())
}

View file

@ -0,0 +1,176 @@
use crate::traits::*;
use crate::types::*;
use libc;
use taskchampion::Uuid;
// NOTE: this must be a simple constant so that cbindgen can evaluate it
/// Length, in bytes, of the string representation of a UUID (without NUL terminator)
pub const TC_UUID_STRING_BYTES: usize = 36;
/// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed.
/// Uuids are typically treated as opaque, but the bytes are available in big-endian format.
///
/// cbindgen:field-names=[bytes]
#[repr(C)]
pub struct TCUuid([u8; 16]);
impl PassByValue for TCUuid {
type RustType = Uuid;
unsafe fn from_ctype(self) -> Self::RustType {
// SAFETY:
// - any 16-byte value is a valid Uuid
Uuid::from_bytes(self.0)
}
fn as_ctype(arg: Uuid) -> Self {
TCUuid(*arg.as_bytes())
}
}
/// Create a new, randomly-generated UUID.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_new_v4() -> TCUuid {
// SAFETY:
// - value is not allocated
unsafe { TCUuid::return_val(Uuid::new_v4()) }
}
/// Create a new UUID with the nil value.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_nil() -> TCUuid {
// SAFETY:
// - value is not allocated
unsafe { TCUuid::return_val(Uuid::nil()) }
}
/// TCUuidList represents a list of uuids.
///
/// The content of this struct must be treated as read-only.
#[repr(C)]
pub struct TCUuidList {
/// number of uuids in items
len: libc::size_t,
/// total size of items (internal use only)
_capacity: libc::size_t,
/// array of uuids. these remain owned by the TCUuidList instance and will be freed by
/// tc_uuid_list_free. This pointer is never NULL for a valid TCUuidList.
items: *mut TCUuid,
}
impl CList for TCUuidList {
type Element = TCUuid;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCUuidList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
/// Write the string representation of a TCUuid into the given buffer, which must be
/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_to_buf(tcuuid: TCUuid, buf: *mut libc::c_char) {
debug_assert!(!buf.is_null());
// SAFETY:
// - buf is valid for len bytes (by C convention)
// - (no alignment requirements for a byte slice)
// - content of buf will not be mutated during the lifetime of this slice (lifetime
// does not outlive this function call)
// - the length of the buffer is less than isize::MAX (promised by caller)
let buf: &mut [u8] =
unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, TC_UUID_STRING_BYTES) };
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
uuid.as_hyphenated().encode_lower(buf);
}
/// Return the hyphenated string representation of a TCUuid. The returned string
/// must be freed with tc_string_free.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> TCString {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
let s = uuid.to_string();
// SAFETY:
// - caller promises to free this value.
unsafe { TCString::return_val(s.into()) }
}
/// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given
/// string is not valid.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_from_str(s: TCString, uuid_out: *mut TCUuid) -> TCResult {
debug_assert!(!s.is_null());
debug_assert!(!uuid_out.is_null());
// SAFETY:
// - s is valid (promised by caller)
// - caller will not use s after this call (convention)
let mut s = unsafe { TCString::val_from_arg(s) };
if let Ok(s) = s.as_str() {
if let Ok(u) = Uuid::parse_str(s) {
// SAFETY:
// - uuid_out is not NULL (promised by caller)
// - alignment is not required
unsafe { TCUuid::val_to_arg_out(u, uuid_out) };
return TCResult::Ok;
}
}
TCResult::Error
}
/// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList.
#[no_mangle]
pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) {
// SAFETY:
// - tcuuids is not NULL and points to a valid TCUuidList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcuuids) };
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tcuuids = unsafe { TCUuidList::return_val(Vec::new()) };
assert!(!tcuuids.items.is_null());
assert_eq!(tcuuids.len, 0);
assert_eq!(tcuuids._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcuuids = unsafe { TCUuidList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_uuid_list_free(&mut tcuuids) };
assert!(tcuuids.items.is_null());
assert_eq!(tcuuids.len, 0);
assert_eq!(tcuuids._capacity, 0);
}
}

View file

@ -0,0 +1,103 @@
use crate::traits::*;
use crate::types::*;
use taskchampion::{Uuid, WorkingSet};
/// A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically
/// updated based on changes in the replica. Its lifetime is independent of the replica and it can
/// be freed at any time.
///
/// To iterate over a working set, search indexes 1 through largest_index.
///
/// # Safety
///
/// The `*TCWorkingSet` returned from `tc_replica_working_set` is owned by the caller and
/// must later be freed to avoid a memory leak. Its lifetime is independent of the replica
/// from which it was generated.
///
/// Any function taking a `*TCWorkingSet` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from `tc_replica_working_set`
/// - the memory referenced by the pointer must never be accessed by C code; and
/// - except for `tc_replica_free`, ownership of a `*TCWorkingSet` remains with the caller.
///
/// Once passed to `tc_replica_free`, a `*TCWorkingSet` becomes invalid and must not be used again.
///
/// TCWorkingSet is not threadsafe.
pub struct TCWorkingSet(WorkingSet);
impl PassByPointer for TCWorkingSet {}
impl From<WorkingSet> for TCWorkingSet {
fn from(ws: WorkingSet) -> TCWorkingSet {
TCWorkingSet(ws)
}
}
/// Utility function to get a shared reference to the underlying WorkingSet.
fn wrap<T, F>(ws: *mut TCWorkingSet, f: F) -> T
where
F: FnOnce(&WorkingSet) -> T,
{
// SAFETY:
// - ws is not null (promised by caller)
// - ws outlives 'a (promised by caller)
let tcws: &TCWorkingSet = unsafe { TCWorkingSet::from_ptr_arg_ref(ws) };
f(&tcws.0)
}
/// Get the working set's length, or the number of UUIDs it contains.
#[no_mangle]
pub unsafe extern "C" fn tc_working_set_len(ws: *mut TCWorkingSet) -> usize {
wrap(ws, |ws| ws.len())
}
/// Get the working set's largest index.
#[no_mangle]
pub unsafe extern "C" fn tc_working_set_largest_index(ws: *mut TCWorkingSet) -> usize {
wrap(ws, |ws| ws.largest_index())
}
/// Get the UUID for the task at the given index. Returns true if the UUID exists in the working
/// set. If not, returns false and does not change uuid_out.
#[no_mangle]
pub unsafe extern "C" fn tc_working_set_by_index(
ws: *mut TCWorkingSet,
index: usize,
uuid_out: *mut TCUuid,
) -> bool {
debug_assert!(!uuid_out.is_null());
wrap(ws, |ws| {
if let Some(uuid) = ws.by_index(index) {
// SAFETY:
// - uuid_out is not NULL (promised by caller)
// - alignment is not required
unsafe { TCUuid::val_to_arg_out(uuid, uuid_out) };
true
} else {
false
}
})
}
/// Get the working set index for the task with the given UUID. Returns 0 if the task is not in
/// the working set.
#[no_mangle]
pub unsafe extern "C" fn tc_working_set_by_uuid(ws: *mut TCWorkingSet, uuid: TCUuid) -> usize {
wrap(ws, |ws| {
// SAFETY:
// - tcuuid is a valid TCUuid (all byte patterns are valid)
let uuid: Uuid = unsafe { TCUuid::val_from_arg(uuid) };
ws.by_uuid(uuid).unwrap_or(0)
})
}
/// Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this
/// function returns, and must not be freed more than once.
#[no_mangle]
pub unsafe extern "C" fn tc_working_set_free(ws: *mut TCWorkingSet) {
// SAFETY:
// - rep is not NULL (promised by caller)
// - caller will not use the TCWorkingSet after this (promised by caller)
let ws = unsafe { TCWorkingSet::take_from_ptr_arg(ws) };
drop(ws);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
#!/usr/bin/env python3
import os
import argparse
import datetime
import subprocess
from typing import List
def ymd():
return datetime.datetime.now().strftime("%Y-%m-%d")
def git_current_branch() -> str :
out = subprocess.check_output(["git", "branch", "--show-current"])
return out.strip().decode("utf-8")
def get_dir() -> str:
here = os.path.dirname(os.path.abspath(__file__))
return os.path.join(
here,
"../.changelogs")
def get_changefiles() -> List[str]:
changedir = get_dir()
changefiles = []
for f in os.listdir(changedir):
if f.endswith(".md") and not f.startswith("."):
changefiles.append(os.path.join(changedir, f))
return changefiles
def cmd_add(args):
text = args.text.strip()
if not text.startswith("- "):
text = "- %s" % text
timestamp = ymd()
branchname = git_current_branch()
fname = os.path.join(get_dir(), "%s-%s.md" % (timestamp, branchname))
with open(fname, "a") as f:
f.write(text)
f.write("\n")
def cmd_build(args):
print("## x.y.z - %s" % (ymd()))
for e in get_changefiles():
print(open(e).read().strip())
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='Sub commands', dest='command')
subparsers.required = True
parser_add = subparsers.add_parser('add')
parser_add.add_argument("text")
parser_add.set_defaults(func=cmd_add)
parser_build = subparsers.add_parser('build')
parser_build.set_defaults(func=cmd_build)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,27 @@
[package]
name = "taskchampion-sync-server"
version = "0.4.1"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2018"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
uuid = { version = "^1.1.2", features = ["serde", "v4"] }
actix-web = "^3.3.2"
anyhow = "1.0"
thiserror = "1.0"
futures = "^0.3.8"
serde = "^1.0.140"
serde_json = "^1.0"
clap = "^3.2.10"
log = "^0.4.17"
env_logger = "^0.9.0"
rusqlite = { version = "0.28", features = ["bundled"] }
chrono = { version = "^0.4.10", features = ["serde"] }
[dev-dependencies]
actix-rt = "^1.1.1"
tempfile = "3"
pretty_assertions = "1"

View file

@ -0,0 +1,204 @@
use crate::api::{client_key_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE};
use crate::server::{add_snapshot, VersionId, NIL_VERSION_ID};
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
use futures::StreamExt;
use std::sync::Arc;
/// Max snapshot size: 100MB
const MAX_SIZE: usize = 100 * 1024 * 1024;
/// Add a new snapshot, after checking prerequisites. The snapshot should be transmitted in the
/// request entity body and must have content-type `application/vnd.taskchampion.snapshot`. The
/// content can be encoded in any of the formats supported by actix-web.
///
/// On success, the response is a 200 OK. Even in a 200 OK, the snapshot may not appear in a
/// subsequent `GetSnapshot` call.
///
/// Returns other 4xx or 5xx responses on other errors.
#[post("/v1/client/add-snapshot/{version_id}")]
pub(crate) async fn service(
req: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
web::Path((version_id,)): web::Path<(VersionId,)>,
mut payload: web::Payload,
) -> Result<HttpResponse> {
// check content-type
if req.content_type() != SNAPSHOT_CONTENT_TYPE {
return Err(error::ErrorBadRequest("Bad content-type"));
}
let client_key = client_key_header(&req)?;
// read the body in its entirety
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("Snapshot over maximum allowed size"));
}
body.extend_from_slice(&chunk);
}
if body.is_empty() {
return Err(error::ErrorBadRequest("No snapshot supplied"));
}
// note that we do not open the transaction until the body has been read
// completely, to avoid blocking other storage access while that data is
// in transit.
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
// get, or create, the client
let client = match txn.get_client(client_key).map_err(failure_to_ise)? {
Some(client) => client,
None => {
txn.new_client(client_key, NIL_VERSION_ID)
.map_err(failure_to_ise)?;
txn.get_client(client_key).map_err(failure_to_ise)?.unwrap()
}
};
add_snapshot(
txn,
&server_state.config,
client_key,
client,
version_id,
body.to_vec(),
)
.map_err(failure_to_ise)?;
Ok(HttpResponse::Ok().body(""))
}
#[cfg(test)]
mod test {
use super::*;
use crate::storage::{InMemoryStorage, Storage};
use crate::Server;
use actix_web::{http::StatusCode, test, App};
use pretty_assertions::assert_eq;
use uuid::Uuid;
#[actix_rt::test]
async fn test_success() -> anyhow::Result<()> {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents..
{
let mut txn = storage.txn().unwrap();
txn.new_client(client_key, version_id).unwrap();
txn.add_version(client_key, version_id, NIL_VERSION_ID, vec![])?;
}
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-snapshot/{}", version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header("Content-Type", "application/vnd.taskchampion.snapshot")
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
// read back that snapshot
let uri = "/v1/client/snapshot";
let req = test::TestRequest::get()
.uri(uri)
.header("X-Client-Key", client_key.to_string())
.to_request();
let mut resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
use futures::StreamExt;
let (bytes, _) = resp.take_body().into_future().await;
assert_eq!(bytes.unwrap().unwrap().as_ref(), b"abcd");
Ok(())
}
#[actix_rt::test]
async fn test_not_added_200() {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents..
{
let mut txn = storage.txn().unwrap();
txn.new_client(client_key, NIL_VERSION_ID).unwrap();
}
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
// add a snapshot for a nonexistent version
let uri = format!("/v1/client/add-snapshot/{}", version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header("Content-Type", "application/vnd.taskchampion.snapshot")
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
// read back, seeing no snapshot
let uri = "/v1/client/snapshot";
let req = test::TestRequest::get()
.uri(uri)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_bad_content_type() {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-snapshot/{}", version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header("Content-Type", "not/correct")
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_empty_body() {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-snapshot/{}", version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}

View file

@ -0,0 +1,230 @@
use crate::api::{
client_key_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
PARENT_VERSION_ID_HEADER, SNAPSHOT_REQUEST_HEADER, VERSION_ID_HEADER,
};
use crate::server::{add_version, AddVersionResult, SnapshotUrgency, VersionId, NIL_VERSION_ID};
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
use futures::StreamExt;
use std::sync::Arc;
/// Max history segment size: 100MB
const MAX_SIZE: usize = 100 * 1024 * 1024;
/// Add a new version, after checking prerequisites. The history segment should be transmitted in
/// the request entity body and must have content-type
/// `application/vnd.taskchampion.history-segment`. The content can be encoded in any of the
/// formats supported by actix-web.
///
/// On success, the response is a 200 OK with the new version ID in the `X-Version-Id` header. If
/// the version cannot be added due to a conflict, the response is a 409 CONFLICT with the expected
/// parent version ID in the `X-Parent-Version-Id` header.
///
/// If included, a snapshot request appears in the `X-Snapshot-Request` header with value
/// `urgency=low` or `urgency=high`.
///
/// Returns other 4xx or 5xx responses on other errors.
#[post("/v1/client/add-version/{parent_version_id}")]
pub(crate) async fn service(
req: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
web::Path((parent_version_id,)): web::Path<(VersionId,)>,
mut payload: web::Payload,
) -> Result<HttpResponse> {
// check content-type
if req.content_type() != HISTORY_SEGMENT_CONTENT_TYPE {
return Err(error::ErrorBadRequest("Bad content-type"));
}
let client_key = client_key_header(&req)?;
// read the body in its entirety
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
if body.is_empty() {
return Err(error::ErrorBadRequest("Empty body"));
}
// note that we do not open the transaction until the body has been read
// completely, to avoid blocking other storage access while that data is
// in transit.
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
// get, or create, the client
let client = match txn.get_client(client_key).map_err(failure_to_ise)? {
Some(client) => client,
None => {
txn.new_client(client_key, NIL_VERSION_ID)
.map_err(failure_to_ise)?;
txn.get_client(client_key).map_err(failure_to_ise)?.unwrap()
}
};
let (result, snap_urgency) = add_version(
txn,
&server_state.config,
client_key,
client,
parent_version_id,
body.to_vec(),
)
.map_err(failure_to_ise)?;
Ok(match result {
AddVersionResult::Ok(version_id) => {
let mut rb = HttpResponse::Ok();
rb.header(VERSION_ID_HEADER, version_id.to_string());
match snap_urgency {
SnapshotUrgency::None => {}
SnapshotUrgency::Low => {
rb.header(SNAPSHOT_REQUEST_HEADER, "urgency=low");
}
SnapshotUrgency::High => {
rb.header(SNAPSHOT_REQUEST_HEADER, "urgency=high");
}
};
rb.finish()
}
AddVersionResult::ExpectedParentVersion(parent_version_id) => {
let mut rb = HttpResponse::Conflict();
rb.header(PARENT_VERSION_ID_HEADER, parent_version_id.to_string());
rb.finish()
}
})
}
#[cfg(test)]
mod test {
use crate::storage::{InMemoryStorage, Storage};
use crate::Server;
use actix_web::{http::StatusCode, test, App};
use pretty_assertions::assert_eq;
use uuid::Uuid;
#[actix_rt::test]
async fn test_success() {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents..
{
let mut txn = storage.txn().unwrap();
txn.new_client(client_key, Uuid::nil()).unwrap();
}
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
// the returned version ID is random, but let's check that it's not
// the passed parent version ID, at least
let new_version_id = resp.headers().get("X-Version-Id").unwrap();
assert!(new_version_id != &version_id.to_string());
// Shapshot should be requested, since there is no existing snapshot
let snapshot_request = resp.headers().get("X-Snapshot-Request").unwrap();
assert_eq!(snapshot_request, "urgency=high");
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
}
#[actix_rt::test]
async fn test_conflict() {
let client_key = Uuid::new_v4();
let version_id = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
// set up the storage contents..
{
let mut txn = storage.txn().unwrap();
txn.new_client(client_key, version_id).unwrap();
}
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::CONFLICT);
assert_eq!(resp.headers().get("X-Version-Id"), None);
assert_eq!(
resp.headers().get("X-Parent-Version-Id").unwrap(),
&version_id.to_string()
);
}
#[actix_rt::test]
async fn test_bad_content_type() {
let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header("Content-Type", "not/correct")
.header("X-Client-Key", client_key.to_string())
.set_payload(b"abcd".to_vec())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_empty_body() {
let client_key = Uuid::new_v4();
let parent_version_id = Uuid::new_v4();
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
let server = Server::new(Default::default(), storage);
let app = App::new().configure(|sc| server.config(sc));
let mut app = test::init_service(app).await;
let uri = format!("/v1/client/add-version/{}", parent_version_id);
let req = test::TestRequest::post()
.uri(&uri)
.header(
"Content-Type",
"application/vnd.taskchampion.history-segment",
)
.header("X-Client-Key", client_key.to_string())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}

Some files were not shown because too many files have changed in this diff Show more