Review round 1: list numbers combines search and before as AND, its limit defaults to the count asked and said names a cut; README names brief, numbers and worker (issues 1070, 1071)
This commit is contained in:
@@ -18,8 +18,8 @@ what it refuses.
|
||||
| verb | takes | does |
|
||||
|---|---|---|
|
||||
| `open` | `title`, `report`, optionally `located-in`, `record` | checks the issue, then takes the next number and keeps it under that number; answers the number |
|
||||
| `get` | `number` | the issue whole: fields, notes, links |
|
||||
| `list` | optionally `status`, `owner`, `phrase`, `limit`, `severity`, `needs`, `ready`, `flag` (`delivered-not-resolved` or `abandoned`), `search` (words, ranked), `before` (a number, the cursor) | short rows, newest first, or best first for a `search`: number, status, owner, title, and each flag that holds; `next`, the `before` that reads on, when more match |
|
||||
| `get` | `number`, optionally `brief` | the issue whole: fields, notes, links; with `brief`, a short answer with only its last note |
|
||||
| `list` | optionally `status`, `owner`, `phrase`, `limit`, `severity`, `needs`, `ready`, `flag` (`delivered-not-resolved` or `abandoned`), `search` (words, ranked), `before` (a number, the cursor), `numbers` (only these issues, in the order given, naming any not an issue in `missing`), `worker` (only that worker's held issues) | short rows, newest first, or best first for a `search`, or in the order given for `numbers`: number, status, owner, title, and each flag that holds; `next`, the `before` that reads on, when more match |
|
||||
| `move` | `number`, `status`, and what that status needs (`by` for `diagnosing` and `fixing`, optionally `take-over`) | changes the status under the development cycle's rules |
|
||||
| `note` | `number`, `text` | appends a note; a note is never edited |
|
||||
| `link` | `number`, `kind`, `target`, optionally `remove` | adds or removes a typed link |
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// list numbers combines search and before as AND filters, never ignoring them (review round 1, issue 1070).
|
||||
func TestListNumbersAppliesSearchAndBeforeAsFilters(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
a := h.open(t, "alpha widget", "hq")
|
||||
b := h.open(t, "beta widget", "hq")
|
||||
c := h.open(t, "gamma widget", "hq")
|
||||
d := h.open(t, "delta nomatch", "hq")
|
||||
|
||||
got, err := h.call(t, "list", map[string]any{
|
||||
"numbers": []any{float64(d), float64(c), float64(b), float64(a)}, "search": "widget"})
|
||||
if err != nil {
|
||||
t.Fatalf("list numbers+search: %v", err)
|
||||
}
|
||||
l := got.(*Listed)
|
||||
if len(l.Rows) != 3 || l.Rows[0].Number != c || l.Rows[1].Number != b || l.Rows[2].Number != a {
|
||||
t.Fatalf("want c, b, a (widget only, in the order given), got %+v", l.Rows)
|
||||
}
|
||||
|
||||
got, err = h.call(t, "list", map[string]any{"numbers": []any{float64(c), float64(b), float64(a)}, "before": float64(c)})
|
||||
if err != nil {
|
||||
t.Fatalf("list numbers+before: %v", err)
|
||||
}
|
||||
l = got.(*Listed)
|
||||
if len(l.Rows) != 2 || l.Rows[0].Number != b || l.Rows[1].Number != a {
|
||||
t.Fatalf("want b, a (below c, in the order given), got %+v", l.Rows)
|
||||
}
|
||||
}
|
||||
|
||||
// list numbers' limit defaults to the count of numbers asked, still capped, and said names a cut (review round
|
||||
// 1, issue 1070).
|
||||
func TestListNumbersLimitDefaultsAndSaysWhenCut(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
a := h.open(t, "one", "hq")
|
||||
b := h.open(t, "two", "hq")
|
||||
c := h.open(t, "three", "hq")
|
||||
|
||||
got, err := h.call(t, "list", map[string]any{"numbers": []any{float64(c), float64(b), float64(a)}})
|
||||
if err != nil {
|
||||
t.Fatalf("list numbers, default limit: %v", err)
|
||||
}
|
||||
l := got.(*Listed)
|
||||
if len(l.Rows) != 3 {
|
||||
t.Fatalf("want the limit to default to len(numbers)=3, got %d rows", len(l.Rows))
|
||||
}
|
||||
|
||||
got, err = h.call(t, "list", map[string]any{"numbers": []any{float64(c), float64(b), float64(a)}, "limit": float64(2)})
|
||||
if err != nil {
|
||||
t.Fatalf("list numbers, limit 2: %v", err)
|
||||
}
|
||||
l = got.(*Listed)
|
||||
if len(l.Rows) != 2 || l.Matched != 3 {
|
||||
t.Fatalf("want 2 rows of 3 matched, got rows %d matched %d", len(l.Rows), l.Matched)
|
||||
}
|
||||
if !strings.Contains(l.Said, "first 2") {
|
||||
t.Fatalf("want said to name the cut, got %q", l.Said)
|
||||
}
|
||||
}
|
||||
@@ -746,6 +746,19 @@ type Listed struct {
|
||||
Said string `json:"said"`
|
||||
}
|
||||
|
||||
// searchHits ranks the issues whose title, report or notes hold search's words, best first, from memory once it
|
||||
// is loaded on start, else from a fresh index (novox/hq ADR 0300 §5).
|
||||
func (t *Tracker) searchHits(all map[uint64]*Issue, search string) []hit {
|
||||
if v := t.view(); v.ready() {
|
||||
return v.search(search)
|
||||
}
|
||||
x := newIndex()
|
||||
for _, is := range all {
|
||||
x.add(is)
|
||||
}
|
||||
return x.search(search)
|
||||
}
|
||||
|
||||
// List is the issues that match, newest first, or ranked by a search: from memory once it is loaded on start
|
||||
// (novox/hq ADR 0300 §5).
|
||||
func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
@@ -782,6 +795,10 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
usingNumbers := len(a.Numbers) > 0
|
||||
if len(a.Numbers) > MostNumbers {
|
||||
return nil, fmt.Errorf("numbers names %d entries; at most %d", len(a.Numbers), MostNumbers)
|
||||
}
|
||||
search := ""
|
||||
if strings.TrimSpace(a.Search) != "" {
|
||||
if search, err = singleLine("search", a.Search, MostPhrase); err != nil {
|
||||
@@ -790,14 +807,11 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
if len(tokens(search)) == 0 {
|
||||
return nil, fmt.Errorf("search %q holds no word of two letters or digits", search)
|
||||
}
|
||||
if a.Before != 0 {
|
||||
if a.Before != 0 && !usingNumbers {
|
||||
return nil, errors.New("before pages the newest-first list; a search is ranked best first, so give limit, " +
|
||||
"or more words, instead")
|
||||
}
|
||||
}
|
||||
if len(a.Numbers) > MostNumbers {
|
||||
return nil, fmt.Errorf("numbers names %d entries; at most %d", len(a.Numbers), MostNumbers)
|
||||
}
|
||||
worker := ""
|
||||
if strings.TrimSpace(a.Worker) != "" {
|
||||
if worker, err = checkWorkerField("worker", a.Worker); err != nil {
|
||||
@@ -806,7 +820,11 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
}
|
||||
limit := a.Limit
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
if usingNumbers {
|
||||
limit = len(a.Numbers)
|
||||
} else {
|
||||
limit = 50
|
||||
}
|
||||
}
|
||||
limit = min(limit, MostListResult)
|
||||
all, err := t.allIssues()
|
||||
@@ -814,22 +832,18 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
return nil, err
|
||||
}
|
||||
now, staleAfter := t.now(), t.workerStaleAfter()
|
||||
usingNumbers := len(a.Numbers) > 0
|
||||
var numbers, missing []uint64
|
||||
var searchSet map[uint64]bool
|
||||
if usingNumbers {
|
||||
numbers = a.Numbers
|
||||
} else if search != "" {
|
||||
var hits []hit
|
||||
if v := t.view(); v.ready() {
|
||||
hits = v.search(search)
|
||||
} else {
|
||||
x := newIndex()
|
||||
for _, is := range all {
|
||||
x.add(is)
|
||||
if search != "" {
|
||||
searchSet = map[uint64]bool{}
|
||||
for _, h := range t.searchHits(all, search) {
|
||||
searchSet[h.Number] = true
|
||||
}
|
||||
hits = x.search(search)
|
||||
}
|
||||
for _, h := range hits {
|
||||
} else if search != "" {
|
||||
for _, h := range t.searchHits(all, search) {
|
||||
if all[h.Number] != nil {
|
||||
numbers = append(numbers, h.Number)
|
||||
}
|
||||
@@ -852,6 +866,14 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if usingNumbers {
|
||||
if a.Before != 0 && n >= a.Before {
|
||||
continue
|
||||
}
|
||||
if searchSet != nil && !searchSet[n] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if status != "" && is.Status != status {
|
||||
continue
|
||||
}
|
||||
@@ -900,6 +922,9 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
out.Said = "No issue asked matches."
|
||||
case usingNumbers:
|
||||
out.Said = fmt.Sprintf("%d of %d asked match, in the order given.", out.Matched, len(a.Numbers))
|
||||
if out.Matched > len(out.Rows) {
|
||||
out.Said += fmt.Sprintf(" The first %d are shown.", len(out.Rows))
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
out.Said += fmt.Sprintf(" %d of the numbers asked are not an issue.", len(missing))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user