mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
merge ot with rask
This commit is contained in:
commit
2d000fdeb9
20 changed files with 849 additions and 48 deletions
29
.taskcluster.yml
Normal file
29
.taskcluster.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
version: 0
|
||||
tasks:
|
||||
- provisionerId: '{{ taskcluster.docker.provisionerId }}'
|
||||
workerType: '{{ taskcluster.docker.workerType }}'
|
||||
extra:
|
||||
github:
|
||||
events:
|
||||
- pull_request.opened
|
||||
- pull_request.reopened
|
||||
- pull_request.synchronize
|
||||
- push
|
||||
payload:
|
||||
maxRunTime: 3600
|
||||
image: 'rust:latest'
|
||||
command:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- >-
|
||||
git clone {{event.head.repo.url}} repo &&
|
||||
cd repo &&
|
||||
git config advice.detachedHead false &&
|
||||
git checkout {{event.head.sha}} &&
|
||||
cargo test
|
||||
metadata:
|
||||
name: Test
|
||||
description: 'Run tests'
|
||||
owner: '{{ event.head.user.email }}'
|
||||
source: '{{ event.head.repo.url }}'
|
||||
allowPullRequests: collaborators
|
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -130,9 +130,9 @@ name = "failure_derive"
|
|||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
|
@ -188,19 +188,6 @@ dependencies = [
|
|||
"autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proptest 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.6"
|
||||
|
@ -208,7 +195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -243,7 +230,7 @@ name = "quote"
|
|||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -379,6 +366,19 @@ dependencies = [
|
|||
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proptest 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
|
@ -439,9 +439,9 @@ name = "serde_derive"
|
|||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -461,10 +461,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.11"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -474,9 +474,9 @@ name = "synstructure"
|
|||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
|
@ -594,7 +594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
|
||||
"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
|
||||
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
||||
"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
|
||||
"checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc"
|
||||
"checksum proptest 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cf147e022eacf0c8a054ab864914a7602618adba841d800a9a9868a5237a529f"
|
||||
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
||||
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
|
||||
|
@ -623,7 +623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
|
||||
"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
|
||||
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
"checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
|
||||
"checksum syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ddc157159e2a7df58cd67b1cace10b8ed256a404fb0070593f137d8ba6bef4de"
|
||||
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
|
||||
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
|
||||
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
[package]
|
||||
name = "ot"
|
||||
name = "rask"
|
||||
version = "0.1.0"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
||||
serde = "1.0.104"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<<<<<<< HEAD
|
||||
Sketch for a Taskd Replacement
|
||||
------------------------------
|
||||
|
||||
|
@ -107,3 +108,9 @@ TBD
|
|||
|
||||
TBD
|
||||
|
||||
=======
|
||||
At the moment, this is sort of an make-work project to re-implement Taskwarrior in Rust.
|
||||
|
||||
There's no great reason to do so, and lots of reasons not to.
|
||||
But it's a nice way to practice some "basic" Rust that does not exercise all of the language's more esoteric features.
|
||||
>>>>>>> 93ce28ed15d11ba601765933f95756e7fb76d2e3
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
extern crate clap;
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use ot::{Replica, DB};
|
||||
use rask::{Replica, DB};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -4,4 +4,7 @@ use failure::Fail;
|
|||
pub enum Error {
|
||||
#[fail(display = "Task Database Error: {}", _0)]
|
||||
DBError(String),
|
||||
|
||||
#[fail(display = "TDB2 Error: {}", _0)]
|
||||
TDB2Error(String),
|
||||
}
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -1,13 +1,27 @@
|
|||
// TODO: remove this eventually when there's an API
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
|
||||
mod errors;
|
||||
mod operation;
|
||||
mod replica;
|
||||
mod server;
|
||||
mod task;
|
||||
mod taskdb;
|
||||
mod tdb2;
|
||||
|
||||
pub use operation::Operation;
|
||||
pub use replica::Replica;
|
||||
pub use server::Server;
|
||||
pub use task::Task;
|
||||
pub use taskdb::DB;
|
||||
|
||||
use failure::Fallible;
|
||||
use std::io::BufRead;
|
||||
|
||||
// TODO: remove (artifact of merging projects)
|
||||
pub fn parse(filename: &str, reader: impl BufRead) -> Fallible<Vec<Task>> {
|
||||
Ok(tdb2::parse(filename, reader)?)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::errors::Error;
|
||||
use crate::operation::Operation;
|
||||
use crate::taskdb::DB;
|
||||
use chrono::Utc;
|
||||
use failure::Fallible;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -16,12 +16,12 @@ impl Replica {
|
|||
}
|
||||
|
||||
/// Create a new task. The task must not already exist.
|
||||
pub fn create_task(&mut self, uuid: Uuid) -> Result<(), Error> {
|
||||
pub fn create_task(&mut self, uuid: Uuid) -> Fallible<()> {
|
||||
self.taskdb.apply(Operation::Create { uuid })
|
||||
}
|
||||
|
||||
/// Delete a task. The task must exist.
|
||||
pub fn delete_task(&mut self, uuid: Uuid) -> Result<(), Error> {
|
||||
pub fn delete_task(&mut self, uuid: Uuid) -> Fallible<()> {
|
||||
self.taskdb.apply(Operation::Delete { uuid })
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ impl Replica {
|
|||
uuid: Uuid,
|
||||
property: S1,
|
||||
value: Option<S2>,
|
||||
) -> Result<(), Error>
|
||||
) -> Fallible<()>
|
||||
where
|
||||
S1: Into<String>,
|
||||
S2: Into<String>,
|
||||
|
|
7
src/task/mod.rs
Normal file
7
src/task/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod task;
|
||||
mod taskbuilder;
|
||||
|
||||
pub use self::taskbuilder::TaskBuilder;
|
||||
pub use self::task::{Task, Priority, Status, Timestamp, Annotation};
|
||||
pub use self::task::Priority::*;
|
||||
pub use self::task::Status::*;
|
55
src/task/task.rs
Normal file
55
src/task/task.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use chrono::prelude::*;
|
||||
|
||||
pub type Timestamp = DateTime<Utc>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Priority {
|
||||
L,
|
||||
M,
|
||||
H,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Completed,
|
||||
Deleted,
|
||||
Recurring,
|
||||
Waiting,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Annotation {
|
||||
pub entry: Timestamp,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// A task, the fundamental business object of this tool.
|
||||
///
|
||||
/// This structure is based on https://taskwarrior.org/docs/design/task.html
|
||||
#[derive(Debug)]
|
||||
pub struct Task {
|
||||
pub status: Status,
|
||||
pub uuid: Uuid,
|
||||
pub entry: Timestamp,
|
||||
pub description: String,
|
||||
pub start: Option<Timestamp>,
|
||||
pub end: Option<Timestamp>,
|
||||
pub due: Option<Timestamp>,
|
||||
pub until: Option<Timestamp>,
|
||||
pub wait: Option<Timestamp>,
|
||||
pub modified: Timestamp,
|
||||
pub scheduled: Option<Timestamp>,
|
||||
pub recur: Option<String>,
|
||||
pub mask: Option<String>,
|
||||
pub imask: Option<u64>,
|
||||
pub parent: Option<Uuid>,
|
||||
pub project: Option<String>,
|
||||
pub priority: Option<Priority>,
|
||||
pub depends: Vec<Uuid>,
|
||||
pub tags: Vec<String>,
|
||||
pub annotations: Vec<Annotation>,
|
||||
pub udas: HashMap<String, String>,
|
||||
}
|
196
src/task/taskbuilder.rs
Normal file
196
src/task/taskbuilder.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use crate::task::{Annotation, Priority, Status, Task, Timestamp};
|
||||
use chrono::prelude::*;
|
||||
use failure::Fallible;
|
||||
use std::collections::HashMap;
|
||||
use std::str;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TaskBuilder {
|
||||
status: Option<Status>,
|
||||
uuid: Option<Uuid>,
|
||||
entry: Option<Timestamp>,
|
||||
description: Option<String>,
|
||||
start: Option<Timestamp>,
|
||||
end: Option<Timestamp>,
|
||||
due: Option<Timestamp>,
|
||||
until: Option<Timestamp>,
|
||||
wait: Option<Timestamp>,
|
||||
modified: Option<Timestamp>,
|
||||
scheduled: Option<Timestamp>,
|
||||
recur: Option<String>,
|
||||
mask: Option<String>,
|
||||
imask: Option<u64>,
|
||||
parent: Option<Uuid>,
|
||||
project: Option<String>,
|
||||
priority: Option<Priority>,
|
||||
depends: Vec<Uuid>,
|
||||
tags: Vec<String>,
|
||||
annotations: Vec<Annotation>,
|
||||
udas: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Parse an "integer", allowing for occasional integers with trailing decimal zeroes
|
||||
fn parse_int<T>(value: &str) -> Result<T, <T as str::FromStr>::Err>
|
||||
where
|
||||
T: str::FromStr,
|
||||
{
|
||||
// some integers are rendered with following decimal zeroes
|
||||
if let Some(i) = value.find('.') {
|
||||
let mut nonzero = false;
|
||||
for c in value[i + 1..].chars() {
|
||||
if c != '0' {
|
||||
nonzero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !nonzero {
|
||||
return value[..i].parse();
|
||||
}
|
||||
}
|
||||
value.parse()
|
||||
}
|
||||
|
||||
/// Parse a status into a Status enum value
|
||||
fn parse_status(value: &str) -> Fallible<Status> {
|
||||
match value {
|
||||
"pending" => Ok(Status::Pending),
|
||||
"completed" => Ok(Status::Completed),
|
||||
"deleted" => Ok(Status::Deleted),
|
||||
"recurring" => Ok(Status::Recurring),
|
||||
"waiting" => Ok(Status::Waiting),
|
||||
_ => Err(format_err!("invalid status {}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse "L", "M", "H" into the Priority enum
|
||||
|
||||
fn parse_priority(value: &str) -> Fallible<Priority> {
|
||||
match value {
|
||||
"L" => Ok(Priority::L),
|
||||
"M" => Ok(Priority::M),
|
||||
"H" => Ok(Priority::H),
|
||||
_ => Err(format_err!("invalid priority {}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a UNIX timestamp into a UTC DateTime
|
||||
fn parse_timestamp(value: &str) -> Result<Timestamp, <i64 as str::FromStr>::Err> {
|
||||
Ok(Utc.timestamp(parse_int::<i64>(value)?, 0))
|
||||
}
|
||||
|
||||
/// Parse depends, as a list of ,-separated UUIDs
|
||||
fn parse_depends(value: &str) -> Result<Vec<Uuid>, uuid::Error> {
|
||||
value.split(',').map(|s| Uuid::parse_str(s)).collect()
|
||||
}
|
||||
|
||||
/// Parse tags, as a list of ,-separated strings
|
||||
fn parse_tags(value: &str) -> Vec<String> {
|
||||
value.split(',').map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
impl TaskBuilder {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn set(mut self, name: &str, value: String) -> Self {
|
||||
const ANNOTATION_PREFIX: &str = "annotation_";
|
||||
if name.starts_with(ANNOTATION_PREFIX) {
|
||||
let entry = parse_timestamp(&name[ANNOTATION_PREFIX.len()..]).unwrap();
|
||||
// TODO: sort by entry time
|
||||
self.annotations.push(Annotation {
|
||||
entry,
|
||||
description: value.to_string(),
|
||||
});
|
||||
return self;
|
||||
}
|
||||
match name {
|
||||
"status" => self.status = Some(parse_status(&value).unwrap()),
|
||||
"uuid" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
|
||||
"entry" => self.entry = Some(parse_timestamp(&value).unwrap()),
|
||||
"description" => self.description = Some(value),
|
||||
"start" => self.start = Some(parse_timestamp(&value).unwrap()),
|
||||
"end" => self.end = Some(parse_timestamp(&value).unwrap()),
|
||||
"due" => self.due = Some(parse_timestamp(&value).unwrap()),
|
||||
"until" => self.until = Some(parse_timestamp(&value).unwrap()),
|
||||
"wait" => self.wait = Some(parse_timestamp(&value).unwrap()),
|
||||
"modified" => self.modified = Some(parse_timestamp(&value).unwrap()),
|
||||
"scheduled" => self.scheduled = Some(parse_timestamp(&value).unwrap()),
|
||||
"recur" => self.recur = Some(value),
|
||||
"mask" => self.mask = Some(value),
|
||||
"imask" => self.imask = Some(parse_int::<u64>(&value).unwrap()),
|
||||
"parent" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
|
||||
"project" => self.project = Some(value),
|
||||
"priority" => self.priority = Some(parse_priority(&value).unwrap()),
|
||||
"depends" => self.depends = parse_depends(&value).unwrap(),
|
||||
"tags" => self.tags = parse_tags(&value),
|
||||
_ => {
|
||||
self.udas.insert(name.to_string(), value);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Task {
|
||||
Task {
|
||||
status: self.status.unwrap(),
|
||||
uuid: self.uuid.unwrap(),
|
||||
description: self.description.unwrap(),
|
||||
entry: self.entry.unwrap(),
|
||||
start: self.start,
|
||||
end: self.end,
|
||||
due: self.due,
|
||||
until: self.until,
|
||||
wait: self.wait,
|
||||
modified: self.modified.unwrap(),
|
||||
scheduled: self.scheduled,
|
||||
recur: self.recur,
|
||||
mask: self.mask,
|
||||
imask: self.imask,
|
||||
parent: self.parent,
|
||||
project: self.project,
|
||||
priority: self.priority,
|
||||
depends: self.depends,
|
||||
tags: self.tags,
|
||||
annotations: self.annotations,
|
||||
udas: self.udas,
|
||||
}
|
||||
|
||||
// TODO: check validity per https://taskwarrior.org/docs/design/task.html
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{parse_depends, parse_int};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_parse_int() {
|
||||
assert_eq!(parse_int::<u8>("123").unwrap(), 123u8);
|
||||
assert_eq!(parse_int::<u32>("123000000").unwrap(), 123000000u32);
|
||||
assert_eq!(parse_int::<i32>("-123000000").unwrap(), -123000000i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_int_decimals() {
|
||||
assert_eq!(parse_int::<u8>("123.00").unwrap(), 123u8);
|
||||
assert_eq!(parse_int::<u32>("123.0000").unwrap(), 123u32);
|
||||
assert_eq!(parse_int::<i32>("-123.").unwrap(), -123i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_depends() {
|
||||
let u1 = "123e4567-e89b-12d3-a456-426655440000";
|
||||
let u2 = "123e4567-e89b-12d3-a456-999999990000";
|
||||
assert_eq!(
|
||||
parse_depends(u1).unwrap(),
|
||||
vec![Uuid::parse_str(u1).unwrap()]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_depends(&format!("{},{}", u1, u2)).unwrap(),
|
||||
vec![Uuid::parse_str(u1).unwrap(), Uuid::parse_str(u2).unwrap()]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use crate::errors::Error;
|
||||
use crate::operation::Operation;
|
||||
use crate::server::{Server, VersionAdd};
|
||||
use failure::Fallible;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
@ -42,7 +43,7 @@ impl DB {
|
|||
/// Apply an operation to the DB. Aside from synchronization operations, this is the only way
|
||||
/// to modify the DB. In cases where an operation does not make sense, this function will do
|
||||
/// nothing and return an error (but leave the DB in a consistent state).
|
||||
pub fn apply(&mut self, op: Operation) -> Result<(), Error> {
|
||||
pub fn apply(&mut self, op: Operation) -> Fallible<()> {
|
||||
if let err @ Err(_) = self.apply_op(&op) {
|
||||
return err;
|
||||
}
|
||||
|
@ -50,19 +51,19 @@ impl DB {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_op(&mut self, op: &Operation) -> Result<(), Error> {
|
||||
fn apply_op(&mut self, op: &Operation) -> Fallible<()> {
|
||||
match op {
|
||||
&Operation::Create { uuid } => {
|
||||
// insert if the task does not already exist
|
||||
if let ent @ Entry::Vacant(_) = self.tasks.entry(uuid) {
|
||||
ent.or_insert(HashMap::new());
|
||||
} else {
|
||||
return Err(Error::DBError(format!("Task {} already exists", uuid)));
|
||||
return Err(Error::DBError(format!("Task {} already exists", uuid)).into());
|
||||
}
|
||||
}
|
||||
&Operation::Delete { ref uuid } => {
|
||||
if let None = self.tasks.remove(uuid) {
|
||||
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
|
||||
return Err(Error::DBError(format!("Task {} does not exist", uuid)).into());
|
||||
}
|
||||
}
|
||||
&Operation::Update {
|
||||
|
@ -78,7 +79,7 @@ impl DB {
|
|||
None => task.remove(property),
|
||||
};
|
||||
} else {
|
||||
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
|
||||
return Err(Error::DBError(format!("Task {} does not exist", uuid)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,8 +204,8 @@ mod tests {
|
|||
let op = Operation::Create { uuid };
|
||||
db.apply(op.clone()).unwrap();
|
||||
assert_eq!(
|
||||
db.apply(op.clone()).err().unwrap(),
|
||||
Error::DBError(format!("Task {} already exists", uuid))
|
||||
db.apply(op.clone()).err().unwrap().to_string(),
|
||||
format!("Task Database Error: Task {} already exists", uuid)
|
||||
);
|
||||
|
||||
let mut exp = HashMap::new();
|
||||
|
@ -285,8 +286,8 @@ mod tests {
|
|||
timestamp: Utc::now(),
|
||||
};
|
||||
assert_eq!(
|
||||
db.apply(op).err().unwrap(),
|
||||
Error::DBError(format!("Task {} does not exist", uuid))
|
||||
db.apply(op).err().unwrap().to_string(),
|
||||
format!("Task Database Error: Task {} does not exist", uuid)
|
||||
);
|
||||
|
||||
assert_eq!(db.tasks(), &HashMap::new());
|
||||
|
@ -314,8 +315,8 @@ mod tests {
|
|||
|
||||
let op1 = Operation::Delete { uuid };
|
||||
assert_eq!(
|
||||
db.apply(op1).err().unwrap(),
|
||||
Error::DBError(format!("Task {} does not exist", uuid))
|
||||
db.apply(op1).err().unwrap().to_string(),
|
||||
format!("Task Database Error: Task {} does not exist", uuid)
|
||||
);
|
||||
|
||||
assert_eq!(db.tasks(), &HashMap::new());
|
||||
|
|
244
src/tdb2/ff4.rs
Normal file
244
src/tdb2/ff4.rs
Normal file
|
@ -0,0 +1,244 @@
|
|||
use std::str;
|
||||
|
||||
use super::pig::Pig;
|
||||
use crate::task::{Task, TaskBuilder};
|
||||
use failure::Fallible;
|
||||
|
||||
/// Rust implementation of part of utf8_codepoint from Taskwarrior's src/utf8.cpp
|
||||
///
|
||||
/// Note that the original function will return garbage for invalid hex sequences;
|
||||
/// this panics instead.
|
||||
fn hex_to_unicode(value: &[u8]) -> Fallible<String> {
|
||||
if value.len() < 4 {
|
||||
bail!("too short");
|
||||
}
|
||||
|
||||
fn nyb(c: u8) -> Fallible<u16> {
|
||||
match c {
|
||||
b'0'..=b'9' => Ok((c - b'0') as u16),
|
||||
b'a'..=b'f' => Ok((c - b'a' + 10) as u16),
|
||||
b'A'..=b'F' => Ok((c - b'A' + 10) as u16),
|
||||
_ => bail!("invalid hex character"),
|
||||
}
|
||||
};
|
||||
|
||||
let words = [nyb(value[0])? << 12 | nyb(value[1])? << 8 | nyb(value[2])? << 4 | nyb(value[3])?];
|
||||
Ok(String::from_utf16(&words[..])?)
|
||||
}
|
||||
|
||||
/// Rust implementation of JSON::decode in Taskwarrior's src/JSON.cpp
|
||||
///
|
||||
/// Decode the given byte slice into a string using Taskwarrior JSON's escaping The slice is
|
||||
/// assumed to be ASCII; unicode escapes within it will be expanded.
|
||||
fn json_decode(value: &[u8]) -> Fallible<String> {
|
||||
let length = value.len();
|
||||
let mut rv = String::with_capacity(length);
|
||||
|
||||
let mut pos = 0;
|
||||
while pos < length {
|
||||
let v = value[pos];
|
||||
if v == b'\\' {
|
||||
pos += 1;
|
||||
if pos == length {
|
||||
rv.push(v as char);
|
||||
break;
|
||||
}
|
||||
let v = value[pos];
|
||||
match v {
|
||||
b'"' | b'\\' | b'/' => rv.push(v as char),
|
||||
b'b' => rv.push(8 as char),
|
||||
b'f' => rv.push(12 as char),
|
||||
b'n' => rv.push('\n' as char),
|
||||
b'r' => rv.push('\r' as char),
|
||||
b't' => rv.push('\t' as char),
|
||||
b'u' => {
|
||||
let unicode = hex_to_unicode(&value[pos + 1..pos + 5]).map_err(|_| {
|
||||
let esc = &value[pos - 1..pos + 5];
|
||||
match str::from_utf8(esc) {
|
||||
Ok(s) => format_err!("invalid unicode escape `{}`", s),
|
||||
Err(_) => format_err!("invalid unicode escape bytes {:?}", esc),
|
||||
}
|
||||
})?;
|
||||
rv.push_str(&unicode);
|
||||
pos += 4;
|
||||
}
|
||||
_ => {
|
||||
rv.push(b'\\' as char);
|
||||
rv.push(v as char);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rv.push(v as char)
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
Ok(rv)
|
||||
}
|
||||
|
||||
/// Rust implementation of Task::decode in Taskwarrior's src/Task.cpp
|
||||
///
|
||||
/// Note that the docstring for the C++ function does not match the
|
||||
/// implementation!
|
||||
fn decode(value: String) -> String {
|
||||
if let Some(_) = value.find('&') {
|
||||
return value.replace("&open;", "[").replace("&close;", "]");
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Parse an "FF4" formatted task line. From Task::parse in Taskwarrior's src/Task.cpp.
|
||||
///
|
||||
/// While Taskwarrior supports additional formats, this is the only format supported by rask.
|
||||
pub(super) fn parse_ff4(line: &str) -> Fallible<Task> {
|
||||
let mut pig = Pig::new(line.as_bytes());
|
||||
let mut builder = TaskBuilder::new();
|
||||
|
||||
pig.skip(b'[')?;
|
||||
let line = pig.get_until(b']')?;
|
||||
let mut subpig = Pig::new(line);
|
||||
while !subpig.depleted() {
|
||||
let name = subpig.get_until(b':')?;
|
||||
let name = str::from_utf8(name)?;
|
||||
subpig.skip(b':')?;
|
||||
let value = subpig.get_quoted(b'"')?;
|
||||
let value = json_decode(value)?;
|
||||
let value = decode(value);
|
||||
builder = builder.set(name, value);
|
||||
subpig.skip(b' ').ok(); // ignore if not found..
|
||||
}
|
||||
pig.skip(b']')?;
|
||||
if !pig.depleted() {
|
||||
bail!("trailing characters on line");
|
||||
}
|
||||
Ok(builder.finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode, hex_to_unicode, json_decode, parse_ff4};
|
||||
use crate::task::Pending;
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_unicode_digits() {
|
||||
assert_eq!(hex_to_unicode(b"1234").unwrap(), "\u{1234}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_unicode_lower() {
|
||||
assert_eq!(hex_to_unicode(b"abcd").unwrap(), "\u{abcd}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_unicode_upper() {
|
||||
assert_eq!(hex_to_unicode(b"ABCD").unwrap(), "\u{abcd}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_unicode_too_short() {
|
||||
assert!(hex_to_unicode(b"AB").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_unicode_invalid() {
|
||||
assert!(hex_to_unicode(b"defg").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_no_change() {
|
||||
assert_eq!(json_decode(b"abcd").unwrap(), "abcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_quote() {
|
||||
assert_eq!(json_decode(b"ab\\\"cd").unwrap(), "ab\"cd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_backslash() {
|
||||
assert_eq!(json_decode(b"ab\\\\cd").unwrap(), "ab\\cd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_frontslash() {
|
||||
assert_eq!(json_decode(b"ab\\/cd").unwrap(), "ab/cd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_b() {
|
||||
assert_eq!(json_decode(b"ab\\bcd").unwrap(), "ab\x08cd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_f() {
|
||||
assert_eq!(json_decode(b"ab\\fcd").unwrap(), "ab\x0ccd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_n() {
|
||||
assert_eq!(json_decode(b"ab\\ncd").unwrap(), "ab\ncd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_r() {
|
||||
assert_eq!(json_decode(b"ab\\rcd").unwrap(), "ab\rcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_t() {
|
||||
assert_eq!(json_decode(b"ab\\tcd").unwrap(), "ab\tcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_other() {
|
||||
assert_eq!(json_decode(b"ab\\xcd").unwrap(), "ab\\xcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_eos() {
|
||||
assert_eq!(json_decode(b"ab\\").unwrap(), "ab\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_unicode() {
|
||||
assert_eq!(json_decode(b"ab\\u1234").unwrap(), "ab\u{1234}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_decode_escape_unicode_bad() {
|
||||
let rv = json_decode(b"ab\\uwxyz");
|
||||
assert_eq!(
|
||||
rv.unwrap_err().to_string(),
|
||||
"invalid unicode escape `\\uwxyz`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_no_change() {
|
||||
let s = "abcd " efgh &".to_string();
|
||||
assert_eq!(decode(s.clone()), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_multi() {
|
||||
let s = "abcd &open; efgh &close; &open".to_string();
|
||||
assert_eq!(decode(s), "abcd [ efgh ] &open".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ff4() {
|
||||
let s = "[description:\"desc\" entry:\"1437855511\" modified:\"1479480556\" \
|
||||
priority:\"L\" project:\"lists\" status:\"pending\" tags:\"watch\" \
|
||||
uuid:\"83ce989e-8634-4d62-841c-eb309383ff1f\"]";
|
||||
let task = parse_ff4(s).unwrap();
|
||||
assert_eq!(task.status, Pending);
|
||||
assert_eq!(task.description, "desc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ff4_fail() {
|
||||
assert!(parse_ff4("abc:10]").is_err());
|
||||
assert!(parse_ff4("[abc:10").is_err());
|
||||
assert!(parse_ff4("[abc:10 123:123]").is_err());
|
||||
}
|
||||
}
|
25
src/tdb2/mod.rs
Normal file
25
src/tdb2/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
//! TDB2 is Taskwarrior's on-disk database format. This module implements
|
||||
//! support for the data structure as a compatibility layer.
|
||||
|
||||
mod ff4;
|
||||
mod pig;
|
||||
|
||||
use self::ff4::parse_ff4;
|
||||
use crate::task::Task;
|
||||
use failure::Fallible;
|
||||
use std::io::BufRead;
|
||||
|
||||
pub(crate) fn parse(filename: &str, reader: impl BufRead) -> Fallible<Vec<Task>> {
|
||||
let mut tasks = vec![];
|
||||
for (i, line) in reader.lines().enumerate() {
|
||||
tasks.push(parse_ff4(&line?).map_err(|e| {
|
||||
format_err!(
|
||||
"TDB2 Error at {}:{}: {}",
|
||||
filename.to_string(),
|
||||
i as u64 + 1,
|
||||
e
|
||||
)
|
||||
})?);
|
||||
}
|
||||
Ok(tasks)
|
||||
}
|
201
src/tdb2/pig.rs
Normal file
201
src/tdb2/pig.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
//! A minimal implementation of the "Pig" parsing utility from the Taskwarrior
|
||||
//! source. This is just enough to parse FF4 lines.
|
||||
|
||||
use failure::Fallible;
|
||||
|
||||
pub struct Pig<'a> {
|
||||
input: &'a [u8],
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl<'a> Pig<'a> {
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
Pig {
|
||||
input: input,
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_until(&mut self, c: u8) -> Fallible<&'a [u8]> {
|
||||
if self.cursor >= self.input.len() {
|
||||
bail!("input truncated");
|
||||
}
|
||||
|
||||
let mut i = self.cursor;
|
||||
while i < self.input.len() {
|
||||
if self.input[i] == c {
|
||||
let rv = &self.input[self.cursor..i];
|
||||
self.cursor = i;
|
||||
return Ok(rv);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let rv = &self.input[self.cursor..];
|
||||
self.cursor = self.input.len();
|
||||
Ok(rv)
|
||||
}
|
||||
|
||||
pub fn get_quoted(&mut self, c: u8) -> Fallible<&'a [u8]> {
|
||||
let length = self.input.len();
|
||||
if self.cursor >= length || self.input[self.cursor] != c {
|
||||
bail!("quoted string does not begin with quote character");
|
||||
}
|
||||
|
||||
let start = self.cursor + 1;
|
||||
let mut i = start;
|
||||
|
||||
while i < length {
|
||||
while i < length && self.input[i] != c {
|
||||
i += 1
|
||||
}
|
||||
if i == length {
|
||||
bail!("unclosed quote");
|
||||
}
|
||||
if i == start {
|
||||
return Ok(&self.input[i..i]);
|
||||
}
|
||||
|
||||
if self.input[i - 1] == b'\\' {
|
||||
// work backward looking for escaped backslashes
|
||||
let mut j = i - 2;
|
||||
let mut quote = true;
|
||||
while j >= start && self.input[j] == b'\\' {
|
||||
quote = !quote;
|
||||
j -= 1;
|
||||
}
|
||||
|
||||
if quote {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// none of the above matched, so we are at the end
|
||||
self.cursor = i + 1;
|
||||
return Ok(&self.input[start..i]);
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
pub fn skip(&mut self, c: u8) -> Fallible<()> {
|
||||
if self.cursor < self.input.len() && self.input[self.cursor] == c {
|
||||
self.cursor += 1;
|
||||
return Ok(());
|
||||
}
|
||||
bail!(
|
||||
"expected character `{}`",
|
||||
String::from_utf8(vec![c]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn depleted(&self) -> bool {
|
||||
self.cursor >= self.input.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Pig;
|
||||
|
||||
#[test]
|
||||
fn test_get_until() {
|
||||
let s = b"abc:123";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_until(b':').unwrap(), &s[..3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_until_empty() {
|
||||
let s = b"abc:123";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_until(b'a').unwrap(), &s[..0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_until_not_found() {
|
||||
let s = b"abc:123";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_until(b'/').unwrap(), &s[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted() {
|
||||
let s = b"'abcd'efg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..5]);
|
||||
assert_eq!(pig.cursor, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_unopened() {
|
||||
let s = b"abcd'efg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert!(pig.get_quoted(b'\'').is_err());
|
||||
assert_eq!(pig.cursor, 0); // nothing consumed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_unclosed() {
|
||||
let s = b"'abcdefg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert!(pig.get_quoted(b'\'').is_err());
|
||||
assert_eq!(pig.cursor, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_escaped() {
|
||||
let s = b"'abc\\'de'fg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..8]);
|
||||
assert_eq!(pig.cursor, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_double_escaped() {
|
||||
let s = b"'abc\\\\'de'fg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..6]);
|
||||
assert_eq!(pig.cursor, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_triple_escaped() {
|
||||
let s = b"'abc\\\\\\'de'fg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..10]);
|
||||
assert_eq!(pig.cursor, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_quoted_all_escapes() {
|
||||
let s = b"'\\\\\\'\\\\'fg";
|
||||
let mut pig = Pig::new(s);
|
||||
assert_eq!(pig.get_quoted(b'\'').unwrap(), &s[1..7]);
|
||||
assert_eq!(pig.cursor, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_match() {
|
||||
let s = b"foo";
|
||||
let mut pig = Pig::new(s);
|
||||
assert!(pig.skip(b'f').is_ok());
|
||||
assert_eq!(pig.cursor, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_no_match() {
|
||||
let s = b"foo";
|
||||
let mut pig = Pig::new(s);
|
||||
assert!(pig.skip(b'x').is_err());
|
||||
assert_eq!(pig.cursor, 0); // nothing consumed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_eos() {
|
||||
let s = b"f";
|
||||
let mut pig = Pig::new(s);
|
||||
assert!(pig.skip(b'f').is_ok());
|
||||
assert!(pig.skip(b'f').is_err());
|
||||
}
|
||||
}
|
2
tests/data/tdb2-test.data
Normal file
2
tests/data/tdb2-test.data
Normal file
|
@ -0,0 +1,2 @@
|
|||
[description:"https:\/\/phabricator.services.example.com\/D7364 &open;taskgraph&close; Download debian packages" end:"1541705209" entry:"1538520624" modified:"1541705209" phabricatorid:"D7364" priority:"M" project:"moz" status:"completed" tags:"phabricator,respond" uuid:"ca33f6d6-1688-4503-90be-3b3526a32b5a" wait:"1570118809"]
|
||||
[annotation_1541461824:"https:\/\/github.com\/taskcluster\/taskcluster-worker-manager\/pull\/3" description:"https:\/\/github.com\/taskcluster\/taskcluster-worker-manager\/pull\/3 More changes" end:"1541702602" entry:"1541451283" githubbody:"some notes:\n\n1. This is a huge PR, so I'm not expecting a quick turn around at all. If you have questions, let me know and I can hope on Vidyo.\n1. Data persistence is written in a semi-janky way. My intention is to use the time while this is under review to make progress on postgres stuff so that the next review cycle will be using new Postgres things. Which means.... There's a lot of bugs in the concurrency because there's no synchronisation at all in this janky-ish model.\n1. The API is the minimum api required to get provisioning-ish things working\n1. I intend to write a system for automatically testing provider and bidding strategy implementations, so that you can do instantiate a provider\/strategy, stub\/spy it as needed then run a test suite against it and have it do its thing. The idea is that each provider will need to mock their underlying api system in their own way, but the set of tests we run for Provider API conformance would be pretty standardized. This should make writing tests for new providers a lot easier.\n1. The provider\/strategy loading system is intentionally simple. The idea is that these aren't general purpose plugins, but rather special ones. The idea is that the config files would essentially declare instances and then provide constructor arguments to initialize them all... This would make enabling\/disabling providers\/strategies fairly trivial\n1. I decided to drop fake implementations of providers and strategies for testing the provisioning logic and instead opt for Sinon stubs, which I think give us a better testing story\n1. I still intend to have fake providers and bidding strategies for doing API testing.\n\nLet me know, and again, I don't expect or need a quick turn around on this PR.\n" githubcreatedon:"1541451283" githubnamespace:"djmitche" githubnumber:"3.000000" githubrepo:"taskcluster\/taskcluster-worker-manager" githubtitle:"More changes" githubtype:"pull_request" githubupdatedat:"1541699191" githuburl:"https:\/\/github.com\/taskcluster\/taskcluster-worker-manager\/pull\/3" githubuser:"jhford" modified:"1541702602" priority:"H" project:"moz" status:"completed" tags:"respond" uuid:"2186f981-d1f5-4642-b833-5b16b3a2d334"]
|
|
@ -1,7 +1,6 @@
|
|||
use chrono::Utc;
|
||||
use ot::Operation;
|
||||
use ot::DB;
|
||||
use proptest::prelude::*;
|
||||
use rask::{Operation, DB};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn uuid_strategy() -> impl Strategy<Value = Uuid> {
|
||||
|
|
20
tests/parse.rs
Normal file
20
tests/parse.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use chrono::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let filename = "tests/data/tdb2-test.data";
|
||||
let file = File::open(filename).unwrap();
|
||||
let tasks = rask::parse(filename, BufReader::new(file)).unwrap();
|
||||
assert_eq!(
|
||||
tasks[0].description,
|
||||
"https://phabricator.services.example.com/D7364 [taskgraph] Download debian packages"
|
||||
);
|
||||
assert_eq!(tasks[0].entry, Utc.timestamp(1538520624, 0));
|
||||
assert_eq!(tasks[0].udas.get("phabricatorid").unwrap(), "D7364");
|
||||
assert_eq!(tasks[1].annotations[0].entry, Utc.timestamp(1541461824, 0));
|
||||
assert!(tasks[1].annotations[0]
|
||||
.description
|
||||
.starts_with("https://github.com",));
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use chrono::Utc;
|
||||
use ot::{Operation, Server, DB};
|
||||
use rask::{Operation, Server, DB};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::Utc;
|
||||
use ot::{Operation, Server, DB};
|
||||
use proptest::prelude::*;
|
||||
use rask::{Operation, Server, DB};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue