├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── agenda-report │ ├── README.md │ ├── agenda-report.go │ ├── agenda-report.service │ └── agenda-report.timer └── basics │ └── main.go ├── fixtures ├── data_1 │ ├── completed.data │ └── pending.data └── taskrc │ ├── err_paths_1 │ ├── err_paths_2 │ ├── err_paths_3 │ ├── err_permissions_1 │ ├── redundant_values_1 │ └── simple_1 ├── task.go ├── taskrc.go ├── taskrc_test.go ├── taskwarrior.go └── taskwarrior_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.5.x 5 | - 1.6.x 6 | - 1.7.x 7 | - 1.8.x 8 | - 1.9.x 9 | - 1.10.x 10 | - tip # The latest version of Go. 11 | 12 | before_install: 13 | - sudo apt-get -qq update 14 | - sudo apt-get install -y task 15 | 16 | script: 17 | - go get golang.org/x/tools/cmd/cover 18 | - cd $HOME/gopath/src/github.com/jubnzv/go-taskwarrior 19 | - yes "yes" | task || true # $? == 0 20 | - chmod 000 ./fixtures/taskrc/err_permissions_1 21 | - go test -v -cover -race -coverprofile=coverage.txt -covermode=atomic 22 | 23 | after_success: 24 | - bash <(curl -s https://codecov.io/bash) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (C) 2018 Georgy Komarov 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-taskwarrior 2 | 3 | [![Build Status](https://travis-ci.org/jubnzv/go-taskwarrior.svg?branch=master)](https://travis-ci.org/jubnzv/go-taskwarrior) 4 | [![codecov](https://codecov.io/gh/jubnzv/go-taskwarrior/branch/master/graph/badge.svg)](https://codecov.io/gh/jubnzv/go-taskwarrior) 5 | [![GoDoc](https://godoc.org/github.com/jubnzv/go-taskwarrior?status.svg)](https://godoc.org/github.com/jubnzv/go-taskwarrior) 6 | 7 | Golang API for [taskwarrior](https://taskwarrior.org/) database. 8 | 9 | ## Features 10 | 11 | * Custom parser for `.taskrc` configuration files 12 | * Read access to taskwarrior database 13 | * Adding/modifying existing tasks 14 | 15 | ## Quickstart 16 | 17 | Example program to read the current user's tasks: 18 | 19 | ``` 20 | package main 21 | 22 | import ( 23 | "github.com/jubnzv/go-taskwarrior" 24 | ) 25 | 26 | func main() { 27 | 28 | tw, _ := taskwarrior.NewTaskWarrior("~/.taskrc") 29 | tw.FetchAllTasks() 30 | tw.PrintTasks() 31 | } 32 | ``` 33 | 34 | To add new task initialize `Task` object with desired values and use: 35 | 36 | ``` 37 | tw.Add(newTask) 38 | tw.Commit() // Save changes 39 | ``` 40 | 41 | For more samples see `examples` directory and package tests. 42 | -------------------------------------------------------------------------------- /examples/agenda-report/README.md: -------------------------------------------------------------------------------- 1 | # agenda-report 2 | 3 | In this simple case I just send list of my pending tasks on my localhost's 4 | mailbox for every morning. 5 | 6 | To achieve this: 7 | 8 | 1. Build `agenda-report.go`. You can edit mail address to send. 9 | 10 | 2. Move `agenda-report.service` and `agenda-report.timer` in 11 | `~/.config/systemd/user/`. You should specify path to builded binary in 12 | `ExecStart` section of `agenda-report.service`. 13 | 14 | 3. Use following command to set up [systemd timers](https://www.freedesktop.org/software/systemd/man/systemd.timer.html): 15 | ``` 16 | systemctl --user enable agenda-report.timer 17 | systemctl --user start agenda-report.timer 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /examples/agenda-report/agenda-report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/jubnzv/go-taskwarrior" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | var LOCAL_MAIL = "user@localhost" 14 | 15 | func sendMail(report string) { 16 | c1 := exec.Command( 17 | "echo", "-e", 18 | "Content-Type: text/plain; charset=\"utf-8\";\nSubject: Agenda report", 19 | "\n\n", report) 20 | c2 := exec.Command("/usr/sbin/sendmail", LOCAL_MAIL) 21 | 22 | r, w := io.Pipe() 23 | c1.Stdout = w 24 | c2.Stdin = r 25 | 26 | var b2 bytes.Buffer 27 | c2.Stdout = &b2 28 | 29 | c1.Start() 30 | c2.Start() 31 | c1.Wait() 32 | w.Close() 33 | c2.Wait() 34 | io.Copy(os.Stdout, &b2) 35 | } 36 | 37 | func getPendingTasks(ss []taskwarrior.Task, test func(taskwarrior.Task) bool) (ret []string) { 38 | for _, s := range ss { 39 | if test(s) { 40 | project := s.Project 41 | if len(project) == 0 { 42 | project = "" 43 | } 44 | entry := fmt.Sprintf("%-12.12s :: %s", project, s.Description) 45 | fmt.Printf("%+v\n", s) 46 | ret = append(ret, entry) 47 | } 48 | } 49 | return 50 | } 51 | 52 | func main() { 53 | tw, _ := taskwarrior.NewTaskWarrior("~/.taskrc") 54 | tw.FetchAllTasks() 55 | mytest := func(s taskwarrior.Task) bool { return s.Status == "pending" } 56 | pending := getPendingTasks(tw.Tasks, mytest) 57 | 58 | title := fmt.Sprintf("\nThere are %d pending taskwarrior tasks:\n\n", len(pending)) 59 | tasks_str := strings.Join(pending, "\n") 60 | report := title + tasks_str 61 | 62 | sendMail(report) 63 | } 64 | -------------------------------------------------------------------------------- /examples/agenda-report/agenda-report.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=agenda-report 3 | 4 | [Service] 5 | Environment="DISPLAY=:0" 6 | ExecStart=/home/jubnzv/.local/scripts/agenda-report 7 | Type=oneshot 8 | 9 | [Install] 10 | WantedBy=default.target 11 | -------------------------------------------------------------------------------- /examples/agenda-report/agenda-report.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run daily agenda-reports 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /examples/basics/main.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | // 4 | // Simple example that reads tasks from system install taskwarrior. 5 | 6 | package main 7 | 8 | import ( 9 | "github.com/jubnzv/go-taskwarrior" 10 | ) 11 | 12 | func main() { 13 | // Initialize new TaskWarrior instance. 14 | tw, err := taskwarrior.NewTaskWarrior("~/.taskrc"); if err != nil { 15 | panic(err) 16 | } 17 | 18 | // Get all available tasks 19 | tw.FetchAllTasks() 20 | // Now you can access to their JSON entries 21 | //fmt.Println("Your taskwarrior tasks:\n", tw.Tasks) 22 | 23 | // Pretty print for current taskwarrior instance 24 | tw.PrintTasks() 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/data_1/completed.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/go-taskwarrior/0ea4f466b47c693624a46574534470560fff3779/fixtures/data_1/completed.data -------------------------------------------------------------------------------- /fixtures/data_1/pending.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/go-taskwarrior/0ea4f466b47c693624a46574534470560fff3779/fixtures/data_1/pending.data -------------------------------------------------------------------------------- /fixtures/taskrc/err_paths_1: -------------------------------------------------------------------------------- 1 | data.location=./completely/random/33/19 2 | -------------------------------------------------------------------------------- /fixtures/taskrc/err_paths_2: -------------------------------------------------------------------------------- 1 | data.location=./fixtures/data_no_pending/ 2 | -------------------------------------------------------------------------------- /fixtures/taskrc/err_paths_3: -------------------------------------------------------------------------------- 1 | data.location=./fixtures/data_no_completed/ 2 | -------------------------------------------------------------------------------- /fixtures/taskrc/err_permissions_1: -------------------------------------------------------------------------------- 1 | # Can't read. -------------------------------------------------------------------------------- /fixtures/taskrc/redundant_values_1: -------------------------------------------------------------------------------- 1 | dontknow=33 2 | data.location=./fixtures/data_1 3 | useless=1911 4 | data.location=./fixtures/data_2 5 | -------------------------------------------------------------------------------- /fixtures/taskrc/simple_1: -------------------------------------------------------------------------------- 1 | data.location=./fixtures/data_1 2 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | // 4 | // Implementation for taskwarrior's Task entries. 5 | 6 | package taskwarrior 7 | 8 | // Task representation. 9 | type Task struct { 10 | Id int32 `json:"id"` 11 | Description string `json:"description"` 12 | Project string `json:"project"` 13 | Status string `json:"status"` 14 | Uuid string `json:"uuid"` 15 | Urgency float32 `json:"urgency"` 16 | Priority string `json:"priority"` 17 | Due string `json:"due"` 18 | End string `json:"end"` 19 | Entry string `json:"entry"` 20 | Modified string `json:"modified"` 21 | } 22 | -------------------------------------------------------------------------------- /taskrc.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | // 4 | // Taskwarrior configuration files parser. 5 | // 6 | // Here is a taskrc syntax rules from `taskrc(5)`: 7 | // 8 | // The .taskrc file follows a very simply syntax defining name/value pairs: = 9 | // There may be whitespace around , '=' and , and it is ignored. Whitespace within the is left 10 | // intact. Whitespace is not permitted in comma-separated lists. The entry must be on a single line, no continuations. 11 | // Values support UTF8 as well as JSON encoding, such as \uNNNN. 12 | // 13 | // Note that Taskwarrior is flexible about the values used to represent Boolean items. You can use "on", "yes", "y", 14 | // "1" and "true". Anything else means "off". 15 | // 16 | // include 17 | // There may be whitespace around 'include' and . The file may be an absolute or relative path, and the special 18 | // character '~' is expanded to mean $HOME. The entry must be on a single line, no continuations. 19 | // 20 | // # 21 | // A comment consists of the character '#', and extends from the '#' to the end of the line. There is no way to 22 | // comment a multi-line block. 23 | // 24 | // There may be blank lines. 25 | // 26 | // Almost every value has a default setting, and an empty .taskrc file is one that makes use of every default. 27 | // The contents of the .taskrc file therefore represent overrides of the default values. To remove a default value 28 | // completely there must be an entry like this: 29 | // = 30 | // This entry overrides the default value with a blank value. 31 | 32 | package taskwarrior 33 | 34 | import ( 35 | "io/ioutil" 36 | "os" 37 | "os/user" 38 | "path/filepath" 39 | "reflect" 40 | "regexp" 41 | "strings" 42 | ) 43 | 44 | // Default configuration path. 45 | var TASKRC = PathExpandTilda("~/.taskrc") 46 | 47 | // Describes configuration file entries that currently supported by this library. 48 | type TaskRC struct { 49 | ConfigPath string // Location of this .taskrc 50 | DataLocation string `taskwarrior:"data.location"` 51 | } 52 | 53 | // Regular expressions that describes parser rules. 54 | var reEntry = regexp.MustCompile(`^\s*([a-zA-Z\.]+)\s*=\s*(.*)\s*$`) 55 | var reInclude = regexp.MustCompile(`^\s*include\s*(.*)\s*$`) 56 | 57 | // Expand tilda in filepath as $HOME of current user. 58 | func PathExpandTilda(path string) string { 59 | if len(path) < 2 { 60 | return path 61 | } 62 | 63 | fixedPath := path 64 | if fixedPath[:2] == "~/" { 65 | userDir, _ := user.Current() 66 | homeDir := userDir.HomeDir 67 | fixedPath = filepath.Join(homeDir, fixedPath[2:]) 68 | } 69 | 70 | return fixedPath 71 | } 72 | 73 | // Return new TaskRC struct instance that contains fields with given configuration file values. 74 | func ParseTaskRC(configPath string) (*TaskRC, error) { 75 | // Fix '~' in a path 76 | configPath = PathExpandTilda(configPath) 77 | 78 | // Use default configuration file as we need 79 | if configPath == "" { 80 | configPath = TASKRC 81 | } else if _, err := os.Stat(configPath); os.IsNotExist(err) { 82 | return nil, err 83 | } 84 | 85 | // Read the given configuration file content in temporary buffer 86 | file, err := os.Open(configPath) 87 | if err != nil { 88 | return nil, err 89 | } 90 | defer file.Close() 91 | buf, err := ioutil.ReadAll(file) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // Map content in new TaskRC instance 97 | task := TaskRC{ConfigPath: configPath} 98 | task.MapTaskRC(string(buf[:])) 99 | 100 | return &task, nil 101 | } 102 | 103 | // Map buffer values to given TaskRC struct. 104 | func (c *TaskRC) MapTaskRC(buf string) error { 105 | // Since we need a little part of all available configuration values we can just traverse line-by-line and check 106 | // that key from this line represents in out structure. Otherwise skip this line and continue. 107 | avaialbleKeys := GetAvailableKeys() 108 | lines := strings.Split(buf, `\n`) 109 | for _, line := range lines { 110 | // Remove comments 111 | line = StripComments(line) 112 | 113 | // Here is an empty line: continue 114 | if len(line) == 0 { 115 | continue 116 | } 117 | 118 | // Is there include pattern? 119 | res := reInclude.FindStringSubmatch(line) 120 | if len(res) >= 2 { 121 | continue // TODO: Not implemented 122 | } 123 | 124 | // Is there regular configuration entry? 125 | res = reEntry.FindStringSubmatch(line) 126 | if len(res) >= 3 { 127 | // Fill the structure 128 | keyTag, val := res[1], res[2] 129 | for _, k := range avaialbleKeys { 130 | // Check field tag 131 | field, _ := reflect.TypeOf(c).Elem().FieldByName(k) 132 | tag := field.Tag 133 | if tag.Get("taskwarrior") != keyTag { 134 | continue 135 | } 136 | 137 | // Set the value 138 | ps := reflect.ValueOf(c) 139 | s := ps.Elem() 140 | if s.Kind() == reflect.Struct { 141 | f := s.FieldByName(k) 142 | if f.IsValid() { 143 | if f.CanSet() { 144 | if f.Kind() == reflect.String { 145 | f.SetString(val) 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // Return list of available configuration options represented by TaskRC structure fields. 158 | func GetAvailableKeys() []string { 159 | var availableKeys []string 160 | t := &TaskRC{} 161 | s := reflect.ValueOf(t).Elem() 162 | typeOf := s.Type() 163 | for i := 0; i < s.NumField(); i++ { 164 | availableKeys = append(availableKeys, typeOf.Field(i).Name) 165 | } 166 | return availableKeys 167 | } 168 | 169 | // Remove commented part of input string. 170 | func StripComments(line string) string { 171 | newLine := line 172 | for i, a := range line { 173 | if a == '#' { 174 | newLine = newLine[:i] 175 | break 176 | } 177 | } 178 | return newLine 179 | } 180 | -------------------------------------------------------------------------------- /taskrc_test.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | 4 | package taskwarrior 5 | 6 | import ( 7 | "os/user" 8 | "testing" 9 | ) 10 | 11 | func TestPathExpandTilda(t *testing.T) { 12 | userDir, _ := user.Current() 13 | expectOrig := userDir.HomeDir 14 | result := PathExpandTilda("~/") 15 | if result != expectOrig { 16 | t.Errorf("ExpectOrig: %s\tGot: %s", expectOrig, result) 17 | } 18 | 19 | expect1 := expectOrig + "/something/else" 20 | result = PathExpandTilda("~/something/else") 21 | if result != expect1 { 22 | t.Errorf("Expect1: %s\tGot: %s", expect1, result) 23 | } 24 | 25 | expect2 := expectOrig + "/something/else" 26 | result = PathExpandTilda("~/something/else/") 27 | if result != expect2 { 28 | t.Errorf("Expect2: %s\tGot: %s", expect2, result) 29 | } 30 | } 31 | 32 | func TestGetAvailableKeys(t *testing.T) { 33 | expected := []string{"DataLocation"} 34 | result := GetAvailableKeys() 35 | found := false 36 | for _, vE := range expected { 37 | found = false 38 | for _, vR := range result { 39 | if vR == vE { 40 | found = true 41 | break 42 | } 43 | } 44 | if found == false { 45 | t.Errorf("Key %s can not be found in avaialable keys!", vE) 46 | } 47 | } 48 | } 49 | 50 | func TestStripComments(t *testing.T) { 51 | orig1 := "Foo#123" 52 | expected1 := "Foo" 53 | result1 := StripComments(orig1) 54 | if result1 != expected1 { 55 | t.Errorf("Incorrect strip comment: expected '%s' got '%s'", expected1, result1) 56 | } 57 | 58 | orig2 := "#123Foo" 59 | expected2 := "" 60 | result2 := StripComments(orig2) 61 | if result2 != expected2 { 62 | t.Errorf("Incorrect strip comment: expected '%s' got '%s'", expected2, result2) 63 | } 64 | } 65 | 66 | func TestTaskRC_MapTaskRC(t *testing.T) { 67 | // Simple configuration entry 68 | orig1 := "data.location=/home/tester/data" 69 | expected1 := "/home/tester/data" 70 | taskrc1 := &TaskRC{DataLocation: orig1} 71 | taskrc1.MapTaskRC(orig1) 72 | if taskrc1.DataLocation != expected1 { 73 | t.Errorf("Incorrect map for DataLocation: expected '%s' got '%s'", expected1, taskrc1.DataLocation) 74 | } 75 | } 76 | 77 | func TestParseTaskRC(t *testing.T) { 78 | // Simple configuration file 79 | config1 := "./fixtures/taskrc/simple_1" 80 | orig1 := "./fixtures/data_1" 81 | expected1 := &TaskRC{DataLocation: orig1} 82 | result1, err := ParseTaskRC(config1) 83 | if err != nil { 84 | t.Errorf("Can't parse configuration file %s with following error: %v", 85 | config1, err) 86 | } 87 | if expected1.DataLocation != result1.DataLocation { 88 | t.Errorf("There are some problems to set DataLocation: expected '%s' got '%s'", 89 | expected1.DataLocation, result1.DataLocation) 90 | } 91 | 92 | // Use default config location (~/.taskrc) 93 | config2 := "" 94 | result2, err := ParseTaskRC(config2) 95 | if err != nil { 96 | t.Errorf("Can't parse configuration file %s with following error: %v", 97 | result2.ConfigPath, err) 98 | } 99 | if result2.ConfigPath != TASKRC { 100 | t.Errorf("There are some problems to set ConfigPath: expected '%s' got '%s'", 101 | result2.ConfigPath, TASKRC) 102 | } 103 | 104 | // Incorrect permissions 105 | config3 := "./fixtures/taskrc/err_permissions_1" 106 | _, err = ParseTaskRC(config3) 107 | if err == nil { 108 | t.Errorf("Read configuration file '%s' content without permissions?", config3) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /taskwarrior.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | // 4 | // Most general definitions to manage list of tasks and taskwarrior instance configuration. 5 | // 6 | // To interact with taskwarrior I decided to use their command-line interface, instead manually parse .data files 7 | // from `data.location` option. This solution looks better because there are few unique .data formats depending of 8 | // taskwarrior version. For more detailed explanations see: https://taskwarrior.org/docs/3rd-party.html. 9 | 10 | package taskwarrior 11 | 12 | import ( 13 | "bytes" 14 | "encoding/json" 15 | "fmt" 16 | "os" 17 | "os/exec" 18 | ) 19 | 20 | // Represents a single taskwarrior instance. 21 | type TaskWarrior struct { 22 | Config *TaskRC // Configuration options 23 | Tasks []Task // Task JSON entries 24 | } 25 | 26 | // Create new empty TaskWarrior instance. 27 | func NewTaskWarrior(configPath string) (*TaskWarrior, error) { 28 | // Read the configuration file. 29 | taskRC, err := ParseTaskRC(configPath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // Create new TaskWarrior instance. 35 | tw := &TaskWarrior{Config: taskRC} 36 | return tw, nil 37 | } 38 | 39 | // Fetch all tasks for given TaskWarrior with system `taskwarrior` command call. 40 | func (tw *TaskWarrior) FetchAllTasks() error { 41 | if tw == nil { 42 | return fmt.Errorf("Uninitialized taskwarrior database!") 43 | } 44 | 45 | rcOpt := "rc:" + tw.Config.ConfigPath 46 | out, err := exec.Command("task", rcOpt, "export").Output() 47 | if err != nil { 48 | return err 49 | } 50 | err = json.Unmarshal([]byte(out), &tw.Tasks) 51 | if err != nil { 52 | fmt.Println(err) 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | // Pretty print for all tasks represented in given TaskWarrior. 59 | func (tw *TaskWarrior) PrintTasks() { 60 | out, _ := json.MarshalIndent(tw.Tasks, "", "\t") 61 | os.Stdout.Write(out) 62 | } 63 | 64 | // Add new Task entry to given TaskWarrior. 65 | func (tw *TaskWarrior) AddTask(task *Task) { 66 | tw.Tasks = append(tw.Tasks, *task) 67 | } 68 | 69 | // Save current changes of given TaskWarrior instance. 70 | func (tw *TaskWarrior) Commit() error { 71 | tasks, err := json.Marshal(tw.Tasks) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | cmd := exec.Command("task", "import", "-") 77 | cmd.Stdin = bytes.NewBuffer(tasks) 78 | err = cmd.Run() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /taskwarrior_test.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright (C) 2018 Georgy Komarov 3 | 4 | package taskwarrior 5 | 6 | import ( 7 | "encoding/json" 8 | "os/exec" 9 | "testing" 10 | ) 11 | 12 | // Helper that executes `task` with selected config path and return result as new TaskRC instances array. 13 | func UtilTaskCmd(configPath string) ([]Task, error) { 14 | var out []byte 15 | if configPath != "" { 16 | rcOpt := "rc:" + configPath 17 | out, _ = exec.Command("task", rcOpt, "export").Output() 18 | } else { 19 | out, _ = exec.Command("task", "export").Output() 20 | } 21 | 22 | // Initialize new tasks 23 | tasks := []Task{} 24 | err := json.Unmarshal([]byte(out), &tasks) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return tasks, nil 29 | } 30 | 31 | func TestNewTaskWarrior(t *testing.T) { 32 | config1 := "./fixtures/taskrc/simple_1" 33 | taskrc1 := &TaskRC{ConfigPath: config1} 34 | expected1 := &TaskWarrior{Config: taskrc1} 35 | result1, err := NewTaskWarrior(config1) 36 | if err != nil { 37 | t.Errorf("NewTaskWarrior fails with following error: %s", err) 38 | } 39 | if expected1.Config.ConfigPath != result1.Config.ConfigPath { 40 | t.Errorf("Incorrect taskrc path in NewTaskWarrior: expected '%s' got '%s'", 41 | expected1.Config.ConfigPath, result1.Config.ConfigPath) 42 | } 43 | 44 | // Incorrect config path 45 | config2 := "./fixtures/not_exists/33" 46 | _, err = NewTaskWarrior(config2) 47 | if err == nil { 48 | t.Errorf("NewTaskWarrior works with non-existent config '%s'", config2) 49 | } 50 | } 51 | 52 | func TestTaskWarrior_FetchAllTasks(t *testing.T) { 53 | // Looks that there are no way for use relative path in .taskrc. So I get real tasks from .taskrc and compare 54 | // their number. 55 | config1 := "" // Use default ~/.taskrc 56 | tasks, _ := UtilTaskCmd(config1) 57 | tw1, err := NewTaskWarrior(config1) 58 | if err != nil { 59 | t.Errorf("NewTaskWarrior fails with following error: %s", err) 60 | } 61 | tw1.FetchAllTasks() 62 | if len(tasks) != len(tw1.Tasks) { 63 | t.Errorf("FetchAllTasks response mismatch: expect %d got %d", 64 | len(tasks), len(tw1.Tasks)) 65 | } 66 | 67 | // Uninitilized database error handling 68 | tw_buggy, _ := NewTaskWarrior("/tmp/does/not/exists") 69 | err = tw_buggy.FetchAllTasks() 70 | if err == nil { 71 | t.Errorf("Incorrect uninitilized database case handling") 72 | } 73 | } 74 | 75 | func TestTaskWarrior_AddTask(t *testing.T) { 76 | config1 := "~/.taskrc" 77 | taskrc1 := &TaskRC{ConfigPath: config1} 78 | expected1 := &TaskWarrior{Config: taskrc1} 79 | result1, _ := NewTaskWarrior(config1) 80 | t1 := &Task{} 81 | result1.AddTask(t1) 82 | if len(expected1.Tasks)+1 != len(result1.Tasks) { 83 | t.Errorf("Incorrect AddTask: wait tasks count '%d' got '%d'", 84 | len(expected1.Tasks)+1, len(result1.Tasks)) 85 | } 86 | } 87 | --------------------------------------------------------------------------------