Remove taskchampion source from this repo (#3427)

* move taskchampion-lib to src/tc/lib, remove the rest
* update references to taskchampion
* Use a top-level Cargo.toml so everything is consistent
* apply comments from ryneeverett
This commit is contained in:
Dustin J. Mitchell 2024-05-01 22:45:11 -04:00 committed by GitHub
parent ef9613e2d6
commit 94b3e301d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
157 changed files with 62 additions and 16265 deletions

3
.github/CODEOWNERS vendored
View file

@ -1,3 +0,0 @@
taskchampion/* @dbr @djmitche
Cargo.toml @dbr @djmitche
Cargo.lock @dbr @djmitche

View file

@ -44,22 +44,6 @@ jobs:
args: --all-features
name: "Clippy Results"
mdbook:
runs-on: ubuntu-latest
name: "Documentation"
steps:
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
# if this changes, change it in .github/workflows/publish-docs.yml as well
mdbook-version: '0.4.10'
- run: mdbook test taskchampion/docs
- run: mdbook build taskchampion/docs
fmt:
runs-on: ubuntu-latest
name: "Formatting"

View file

@ -1,31 +0,0 @@
name: docs
on:
push:
branches:
- develop
permissions:
contents: write
jobs:
mdbook-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
# if this changes, change it in .github/workflows/checks.yml as well
mdbook-version: '0.4.10'
- run: mdbook build taskchampion/docs
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./taskchampion/docs/book
destination_dir: taskchampion

View file

@ -1,83 +0,0 @@
name: tests - rust
on:
push:
branches:
- develop
pull_request:
types: [opened, reopened, synchronize]
jobs:
## Run the `taskchampion` crate's tests with various combinations of features.
features:
strategy:
matrix:
features:
- ""
- "server-sync"
name: "taskchampion ${{ matrix.features == '' && 'with no features' || format('with features {0}', matrix.features) }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ubuntu-latest-stable-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ubuntu-latest-stable-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: test
run: cargo test -p taskchampion --no-default-features --features "${{ matrix.features }}"
## Run all TaskChampion crate tests, using both the minimum supported rust version
## and the latest stable Rust.
test:
strategy:
matrix:
rust:
- "1.70.0" # MSRV
- "stable"
os:
- ubuntu-latest
- macOS-latest
- windows-latest
name: "rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: "${{ matrix.rust }}"
override: true
- name: test
run: cargo test

319
Cargo.lock generated
View file

@ -124,21 +124,6 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -288,27 +273,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "errno"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@ -321,15 +285,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
dependencies = [
"instant",
]
[[package]]
name = "ffizz-header"
version = "0.5.0"
@ -378,21 +333,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.25"
@ -400,7 +340,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -409,17 +348,6 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.25"
@ -449,19 +377,12 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]]
name = "futures-timer"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
[[package]]
name = "futures-util"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@ -631,12 +552,6 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hex"
version = "0.4.3"
@ -758,39 +673,6 @@ dependencies = [
"hashbrown 0.11.2",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "integration-tests"
version = "0.4.1"
dependencies = [
"anyhow",
"cc",
"lazy_static",
"pretty_assertions",
"taskchampion",
"taskchampion-lib",
"tempfile",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -847,12 +729,6 @@ version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
[[package]]
name = "libm"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
@ -884,12 +760,6 @@ dependencies = [
"syn 1.0.104",
]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "lock_api"
version = "0.4.7"
@ -988,7 +858,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -997,7 +866,7 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi",
"libc",
]
@ -1034,7 +903,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.2.13",
"redox_syscall",
"smallvec",
"windows-sys 0.45.0",
]
@ -1091,12 +960,6 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
@ -1116,32 +979,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
dependencies = [
"bit-set",
"bit-vec",
"bitflags 2.0.2",
"lazy_static",
"num-traits",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.28"
@ -1151,45 +988,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.13"
@ -1199,15 +997,6 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.10.2"
@ -1308,32 +1097,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rstest"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"rustc_version",
"syn 1.0.104",
"unicode-ident",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
@ -1354,29 +1117,6 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.37.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "rustls"
version = "0.21.11"
@ -1414,18 +1154,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "ryu"
version = "1.0.10"
@ -1448,12 +1176,6 @@ dependencies = [
"untrusted 0.7.1",
]
[[package]]
name = "semver"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
[[package]]
name = "serde"
version = "1.0.147"
@ -1617,7 +1339,9 @@ dependencies = [
[[package]]
name = "taskchampion"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9e2d64086cc515f801ba0e1f366e5e029dd47db3c499a9e173a60b62145410"
dependencies = [
"anyhow",
"byteorder",
@ -1625,16 +1349,12 @@ dependencies = [
"flate2",
"google-cloud-storage",
"log",
"pretty_assertions",
"proptest",
"ring 0.17.3",
"rstest",
"rusqlite",
"serde",
"serde_json",
"strum",
"strum_macros",
"tempfile",
"thiserror",
"tokio",
"ureq",
@ -1653,20 +1373,6 @@ dependencies = [
"taskchampion",
]
[[package]]
name = "tempfile"
version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
dependencies = [
"autocfg",
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@ -1842,12 +1548,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicase"
version = "2.7.0"
@ -1945,15 +1645,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.1"

View file

@ -1,17 +1,12 @@
[workspace]
members = [
"taskchampion/taskchampion",
"taskchampion/lib",
"taskchampion/integration-tests",
"taskchampion/xtask",
"src/tc/lib",
"xtask",
]
resolver = "2"
# src/tc/rust is just part of the TW build and not a public crate
exclude = [ "src/tc/rust" ]
# All Rust dependencies are defined here, and then referenced by the
# Cargo.toml's in the members with `foo.workspace = true`.
[workspace.dependencies]
@ -40,3 +35,4 @@ thiserror = "1.0"
ureq = { version = "^2.9.0", features = ["tls"] }
uuid = { version = "^1.8.0", features = ["serde", "v4"] }
url = { version = "2" }
taskchampion = "0.5"

View file

@ -11,10 +11,6 @@ For all other documenation, see https://taskwarrior.org.
As of the 3.0 release, Taskwarrior uses TaskChampion to manage task data.
Find documentation of TaskChampion here:
* [TaskChampion README](../../taskchampion)
* [TaskChampion CONTRIBUTING guide](../../taskchampion/CONTRIBUTING.md)
* [TaskChampion Book](../../taskchampion/docs/src/SUMMARY.md)
* [TaskChampion Repository](https://github.com/GothenburgBitFactory/taskchampion/)
* [TaskChampion Book](https://gothenburgbitfactory.github.io/taskchampion/)
* [TaskChampion API Documentation](https://docs.rs/taskchampion)
TaskChampion will [become its own
project](https://github.com/GothenburgBitFactory/taskwarrior/issues/3209) soon.

View file

@ -1,12 +1,5 @@
# Developing Taskwarrior
The following describes the process for developing Taskwarrior. If you are only
changing TaskChampion (Rust code), you can simply treat it like any other Rust
project: modify the source under `taskchampion/` and use `cargo test` to run
the TaskChampion tests.
See the [TaskChampion CONTRIBUTING guide](../../../taskchampion/CONTRIBUTING.md) for more.
## Satisfy the Requirements:
* CMake 3.0 or later

View file

@ -11,7 +11,6 @@ To release Taskwarrior, follow this process:
- On `develop` after that PR merges, create a release tarball:
- `git clone . release-tarball`
- `cd release-tarball/`
- edit `Cargo.toml` to contain only `taskchampion` and `taskchampion-lib` in `members` (see https://github.com/GothenburgBitFactory/taskwarrior/issues/3294).
- `cmake -S. -Bbuild`
- `make -Cbuild package_source`
- copy build/task-*.tar.gz elsewhere and delete the `release-tarball` dir

View file

@ -1,42 +1,21 @@
# Rust and C++
Taskwarrior has historically been a C++ project, but as part of an [ongoing effort to replace its storage backend](https://github.com/GothenburgBitFactory/taskwarrior/issues/2770) it now includes a Rust library called TaskChampion.
To develop Taskwarrior, you will need both a [C++ compiler and tools](./development.md), and a [Rust toolchain](https://www.rust-lang.org/tools/install).
However, most tasks will only require you to be familiar with one of the two languages.
Taskwarrior has historically been a C++ project, but as of taskwarrior-3.0.0, the storage backend is now provided by a Rust library called TaskChampion.
## TaskChampion
TaskChampion implements storage and access to "replicas" containing a user's tasks.
It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas.
It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server.
TaskChampion provides a C interface via the `taskchampion-lib` crate.
TaskChampion provides a C interface via the `taskchampion-lib` crate, at `src/tc/lib`.
Other applications, besides Taskwarrior, can use TaskChampion to manage tasks.
Applications written in Rust can use the `taskchampion` crate, while those in other languages may use the `taskchampion-lib` crate.
Taskwarrior is just one application using the TaskChampion interface.
You can build TaskChampion locally by simply running `cargo build` in the root of this repository.
The implementation, including more documentation, is in the [`rust`](../../rust) subdirectory.
## Taskwarrior's use of TaskChampion
Taskwarrior's interface to TaskChampion has a few layers:
* The skeletal Rust crate in [`src/tc/rust`](../../src/tc/rust) brings the symbols from `taskchampion-lib` under CMake's management.
The corresponding header file is included from [`taskchampion/lib`](../../taskchampion/lib).
All of these symbols are placed in the C++ namespace, `tc::ffi`.
* C++ wrappers for the types from `taskchampion-lib` are defined in [`src/tc`](../../src/tc), ensuring memory safety (with `unique_ptr`) and adding methods corresponding to the Rust API's methods.
* A Rust library, `takschampion-lib`, that presents `extern "C"` functions for use from C++, essentially defining a C interface to TaskChampion.
* C++ wrappers for the types from `taskchampion-lib`, defined in [`src/tc`](../../src/tc), ensuring memory safety (with `unique_ptr`) and adding methods corresponding to the Rust API's methods.
The wrapper types are in the C++ namespace, `tc`.
## WARNING About Dependency Tracking
CMake cannot detect changes to Rust files in under the `taskchampion/` directory.
Running `make` after these files are changed will not incorporate the changes into the resulting executables.
To force re-compilation of the Rust dependencies:
```
rm -rf src/tc/rust/x86_64-unknown-linux-gnu/debug/libtc_rust.a
make
```
You may need to adjust the `x86_64-unknown-linux-gnu` part of that command depending on what system you are using for development.

View file

@ -2,10 +2,10 @@ cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src
${CMAKE_SOURCE_DIR}/taskchampion/lib
${TASK_INCLUDE_DIRS})
add_library (task STATIC CLI2.cpp CLI2.h

View file

@ -2,10 +2,10 @@ cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src
${CMAKE_SOURCE_DIR}/taskchampion/lib
${TASK_INCLUDE_DIRS})
set (columns_SRCS Column.cpp Column.h

View file

@ -2,10 +2,10 @@ cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src
${CMAKE_SOURCE_DIR}/taskchampion/lib
${TASK_INCLUDE_DIRS})
set (commands_SRCS Command.cpp Command.h

View file

@ -8,15 +8,18 @@ corrosion_import_crate(
LOCKED
CRATES "taskchampion-lib")
# TODO(#3425): figure out how to create taskchampion.h
include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/libshared/src
${CMAKE_SOURCE_DIR}/taskchampion/lib
${TASK_INCLUDE_DIRS})
set (tc_SRCS
ffi.h
lib/taskchampion.h
util.cpp util.h
Replica.cpp Replica.h
Server.cpp Server.h

View file

@ -2,6 +2,7 @@
name = "taskchampion-lib"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["staticlib", "rlib"]
@ -10,8 +11,7 @@ crate-type = ["staticlib", "rlib"]
libc.workspace = true
anyhow.workspace = true
ffizz-header.workspace = true
taskchampion = { path = "../taskchampion" }
taskchampion.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View file

View file

@ -1,2 +0,0 @@
- 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

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

View file

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

View file

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

View file

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

View file

@ -1,26 +0,0 @@
# 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

@ -1,76 +0,0 @@
# 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

@ -1,62 +0,0 @@
# 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).
## Running Tests
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 important 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-lib` implements a C API for `taskchampion`, used by Taskwarrior
* `integration-tests` contains some tests for integrations between multiple crates.
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.

View file

@ -1,21 +0,0 @@
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.

View file

@ -1,45 +0,0 @@
# 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).

View file

@ -1,50 +0,0 @@
TaskChampion
------------
TaskChampion implements the task storage and synchronization behind Taskwarrior.
It includes an implementation with Rust and C APIs, allowing any application to maintain and manipulate its own replica.
It also includes a specification for tasks and how they are synchronized, inviting alternative implementations of replicas or task servers.
See the [documentation](https://gothenburgbitfactory.github.io/taskwarrior/taskchampion/) for more!
NOTE: Taskwarrior is currently in the midst of a change to use TaskChampion as its storage.
Until that is complete, the information here may be out-of-date.
## Structure
There are four crates here:
* [taskchampion](./taskchampion) - the core of the tool
* [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C
* [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion_ and _taskchampion-lib_.
* [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command
## 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`.
## Rust API
The Rust API, as defined in [the docs](https://docs.rs/taskchampion/latest/taskchampion/), supports simple creation and manipulation of replicas and the tasks they contain.
The Rust API follows semantic versioning.
As this is still in the `0.x` phase, so breaking changes may occur but will be indicated with a change to the minor version.
## C API
The `taskchampion-lib` crate generates libraries suitable for use from C (or any C-compatible language).
It is a "normal" Cargo crate that happens to export a number of `extern "C"` symbols, and also contains a `taskchampion.h` defining those symbols.
*WARNING: the C API is not yet stable!*
It is your responsibility to link this into a form usable in your own build process.
For example, in a typical CMake C++ project, CMakeRust can do this for you.
In many cases, this is as simple as a rust crate with `src/lib.rs` containing
```rust
pub use taskchampion_lib::*;
```
Arrange to use the header file, `lib/taskchampion.h`, by copying it or adding its directory to your include search path.
[Future work](https://github.com/GothenburgBitFactory/taskwarrior/issues/2870) will provide better automation for this process.

View file

@ -1,16 +0,0 @@
# 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 `(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

View file

@ -1,11 +0,0 @@
# 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.

View file

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

View file

@ -1,3 +0,0 @@
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

@ -1,2 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View file

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

View file

@ -1,17 +0,0 @@
# Summary
- [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)
* [Encryption](./encryption.md)
* [HTTP Implementation](./http.md)
* [Object-Store Implementation](./object-store.md)
* [Planned Functionality](./plans.md)

View file

@ -1,5 +0,0 @@
# 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.

View file

@ -1,38 +0,0 @@
# Encryption
The client configuration includes an encryption secret of arbitrary length.
This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots).
Encryption is not used for local (on-disk) sync, but is used for all cases where data is sent from the local host.
## Key Derivation
The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 600,000 iterations.
The salt value depends on the implementation of the protocol, as described in subsequent chapters.
## 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.

View file

@ -1,65 +0,0 @@
# HTTP Representation
The transactions in the sync protocol 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 `client_id` in the form of a UUID.
This value is passed with every request in the `X-Client-Id` header, in its dashed-hex format.
The salt used in key derivation is the 16-byte client ID.
## 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

View file

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

View file

@ -1,5 +0,0 @@
# 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

@ -1,9 +0,0 @@
# Object Store Representation
TaskChampion also supports use of a generic key-value store to synchronize replicas.
In this case, the salt used in key derivation is a random 16-byte value, stored
in the object store and retrieved as needed.
The details of the mapping from this protocol to keys and values are private to the implementation.
Other applications should not access the key-value store directly.

View file

@ -1,35 +0,0 @@
# 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

@ -1,11 +0,0 @@
# 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

@ -1,39 +0,0 @@
# 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 (the nil UUID) 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.

View file

@ -1,83 +0,0 @@
# 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

@ -1,141 +0,0 @@
# 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.
Critically, though, operations cannot merge; in effect, the only option is rebasing.
Furthermore, once an operation has been sent to the server it cannot be changed; in effect, the server does not permit "force push".
### 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.

View file

@ -1,115 +0,0 @@
# Server-Replica Protocol
The server-replica protocol is defined abstractly in terms of request/response transactions.
The protocol builds on the model presented in the previous chapters, and in particular on the synchronization process.
## Clients
From the protocol'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.
## Server
A server implements the requests and responses described below.
Where the logic is implemented depends on the specific implementation of the protocol.
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.
From the server's perspective, snapshots and versions are opaque byte sequences.
## Version Invariant
The following invariant must always hold:
> All versions are linked by parent-child relationships to form a single chain.
> That is, each version must have no more than one parent and one child, and no more than one version may have zero parents or zero children.
## Data Formats
Task data sent to the server is encrypted by the client, using the scheme described in the "Encryption" chapter.
### 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
All interactions between the client and server are defined in terms of request/response transactions, as described here.
### 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, and
* encrypted version data.
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 has no children, thereby maintaining the version invariant.
If the version is accepted, the server generates a new version ID for it.
The version is added to the chain of versions for the client, and 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 ID of the version which has no children.
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
* encrypted version data.
If not found, it returns an indication that no such version exists.
### 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, and
* encrypted snapshot data.
The server may 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.

View file

@ -1,7 +0,0 @@
# 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

@ -1,32 +0,0 @@
# 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

@ -1,58 +0,0 @@
# 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, `D` for deleted, or `R` for recurring
* `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 ignored)
* `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; for example, `annotation_1693329505`.
* `dep_<uuid>` - indicates this task depends on another task identified by `<uuid>`; the value is ignored; for example, `dep_8c4fed9c-c0d2-40c2-936d-36fc44e084a0`
Note that while TaskChampion recognizes "recurring" as a status, it does not implement recurrence directly.
### 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

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

View file

@ -1,20 +0,0 @@
[package]
name = "integration-tests"
version = "0.4.1"
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
edition = "2021"
publish = false
build = "build.rs"
[dependencies]
taskchampion = { path = "../taskchampion" }
taskchampion-lib = { path = "../lib" }
[dev-dependencies]
anyhow.workspace = true
tempfile.workspace = true
pretty_assertions.workspace = true
lazy_static.workspace = true
[build-dependencies]
cc.workspace = true

View file

@ -1,30 +0,0 @@
# 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

@ -1,51 +0,0 @@
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)",
);
let mut files = vec![
"src/bindings_tests/test.c".into(),
"src/bindings_tests/unity/unity.c".into(),
];
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);
}
println!("cargo:rerun-if-changed=../lib/taskchampion.h");
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

@ -1,30 +0,0 @@
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

@ -1,330 +0,0 @@
#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"), true, 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;
TCReplicaOpList undo_ops = tc_replica_get_undo_ops(rep);
int rv = tc_replica_commit_undo_ops(rep, undo_ops, &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(18, tc_replica_num_local_operations(rep));
tc_replica_free(rep);
}
// When tc_replica_commit_undo_ops 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);
TCReplicaOpList undo_ops = tc_replica_get_undo_ops(rep);
int rv = tc_replica_commit_undo_ops(rep, undo_ops, 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);
}
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);
}
static void test_replica_remote_server(void) {
TCString err;
TCServer *server = tc_server_new_sync(
tc_string_borrow("http://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

@ -1,125 +0,0 @@
#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

@ -1,717 +0,0 @@
#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 arbitrary attributes on a task works
static void test_task_get_set_attribute(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 foo;
foo = tc_task_get_value(task, tc_string_borrow("foo"));
TEST_ASSERT_NULL(foo.ptr);
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_value(task,
tc_string_borrow("foo"),
tc_string_borrow("updated")));
foo = tc_task_get_value(task, tc_string_borrow("foo"));
TEST_ASSERT_NOT_NULL(foo.ptr);
TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&foo));
tc_string_free(&foo);
tc_task_to_immut(task);
foo = tc_task_get_value(task, tc_string_borrow("foo"));
TEST_ASSERT_NOT_NULL(foo.ptr);
TEST_ASSERT_EQUAL_STRING("updated", tc_string_content(&foo));
tc_string_free(&foo);
TCString null = { .ptr = NULL };
tc_task_to_mut(task, rep);
TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_value(task,
tc_string_borrow("foo"),
null));
foo = tc_task_get_value(task, tc_string_borrow("foo"));
TEST_ASSERT_NULL(foo.ptr);
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_attribute);
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

@ -1,30 +0,0 @@
#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

@ -1,21 +0,0 @@
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

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

File diff suppressed because it is too large Load diff

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