2026-08-19 12:47:50 +02:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// The point of the whole thing: a #tag in a note is the same tag as in a task.
|
|
|
|
|
// Not a second kind that looks alike and has to be kept in step by hand.
|
|
|
|
|
func TestATagInANoteIsTheSameTagAsInATask(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
n := &Note{
|
|
|
|
|
CreatedBy: userID,
|
|
|
|
|
Body: "Mødet med #Firma gik fint. Følg op sammen med #Regnskab.",
|
|
|
|
|
}
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var projects []string
|
|
|
|
|
for _, l := range n.Links {
|
|
|
|
|
if l.Kind == "project" {
|
|
|
|
|
projects = append(projects, l.TargetID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(projects) != 2 {
|
|
|
|
|
t.Fatalf("read %v out of the note, want the two projects", n.Links)
|
|
|
|
|
}
|
2026-08-19 18:48:00 +02:00
|
|
|
// Folded: the key is a key, not a label. #firma and #Firma are the same project
|
|
|
|
|
// to everybody except a database, and the project's own spelling is on the
|
|
|
|
|
// project, which is what the interface shows.
|
|
|
|
|
if projects[0] != "firma" || projects[1] != "regnskab" {
|
2026-08-19 12:47:50 +02:00
|
|
|
t.Errorf("got %v", projects)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Backwards is the direction that makes it a second brain rather than a folder.
|
|
|
|
|
func TestANoteIsFoundFromWhatItPointsAt(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
const taskID = "01a01694-a25c-7000-a50f-d72ab33f9b6a"
|
|
|
|
|
notes := []*Note{
|
|
|
|
|
{CreatedBy: userID, Body: "Aftalt på mødet, se /opgave/" + taskID},
|
|
|
|
|
{CreatedBy: userID, Body: "Intet at gøre med den sag."},
|
|
|
|
|
}
|
|
|
|
|
for _, n := range notes {
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
found, err := db.NotesLinking(ctx, "task", taskID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(found) != 1 {
|
|
|
|
|
t.Fatalf("%d notes point at the task, want 1", len(found))
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(found[0].Title, "Aftalt") {
|
|
|
|
|
t.Errorf("the wrong note came back: %q", found[0].Title)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The links are an index over the text, so editing the text must move them.
|
|
|
|
|
// A stale link is worse than none: it says a connection exists that does not.
|
|
|
|
|
func TestEditingANoteRewritesWhatItPointsAt(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
n := &Note{CreatedBy: userID, Body: "Handler om #Firma"}
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if before, _ := db.NotesLinking(ctx, "project", "Firma"); len(before) != 1 {
|
|
|
|
|
t.Fatal("the first save did not record the link")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
n.Body = "Handler om #Regnskab i stedet"
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stale, err := db.NotesLinking(ctx, "project", "Firma")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(stale) != 0 {
|
|
|
|
|
t.Errorf("the note still points at Firma after the mention was removed")
|
|
|
|
|
}
|
|
|
|
|
if fresh, _ := db.NotesLinking(ctx, "project", "Regnskab"); len(fresh) != 1 {
|
|
|
|
|
t.Errorf("the new mention was not recorded")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A list of untitled notes is a list nobody can read.
|
|
|
|
|
func TestANoteTakesItsTitleFromItsFirstLine(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
for _, c := range []struct{ body, want string }{
|
|
|
|
|
{"# Møde med Anders\n\nHan ville gerne...", "Møde med Anders"},
|
|
|
|
|
{"\n\n Løse tanker \nmere", "Løse tanker"},
|
|
|
|
|
{"", ""},
|
|
|
|
|
} {
|
|
|
|
|
n := &Note{CreatedBy: userID, Body: c.body}
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n.Title != c.want {
|
|
|
|
|
t.Errorf("body %q gave title %q, want %q", c.body, n.Title, c.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Danish spelling, both ways round — the same generosity tasks already get.
|
|
|
|
|
func TestNotesAreFoundInEitherSpelling(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
2026-08-19 18:36:31 +02:00
|
|
|
// The title is the first line now, so that is where it goes.
|
2026-08-19 12:47:50 +02:00
|
|
|
if err := db.SaveNote(ctx, &Note{
|
|
|
|
|
CreatedBy: userID,
|
2026-08-19 18:36:31 +02:00
|
|
|
Body: "Grønt regnskab\n\nMålt på årsbasis for Århus.",
|
2026-08-19 12:47:50 +02:00
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, q := range []string{"grønt", "gront", "århus", "aarhus", "regnskab"} {
|
|
|
|
|
found, err := db.SearchNotes(ctx, q, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(found) != 1 {
|
|
|
|
|
t.Errorf("%q found %d notes, want 1", q, len(found))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deleting is the trash, the way it is everywhere else in this program. A note is
|
|
|
|
|
// the one place somebody can lose an hour of writing to one keystroke.
|
|
|
|
|
func TestADeletedNoteCanComeBack(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
n := &Note{CreatedBy: userID, Body: "Vigtigt"}
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.DeleteNote(ctx, n.ID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if gone, _ := db.Note(ctx, n.ID); gone != nil {
|
|
|
|
|
t.Error("a deleted note is still returned")
|
|
|
|
|
}
|
|
|
|
|
if err := db.RestoreNote(ctx, n.ID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
back, err := db.Note(ctx, n.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if back == nil || back.Body != "Vigtigt" {
|
|
|
|
|
t.Error("the note did not come back whole")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWhatIsReadOutOfABody(t *testing.T) {
|
|
|
|
|
for _, c := range []struct {
|
|
|
|
|
name, body string
|
|
|
|
|
want []NoteLink
|
|
|
|
|
}{
|
|
|
|
|
{"a hash inside a word is not a tag", "C#Sharp og x#y", nil},
|
|
|
|
|
{"a note by title", "se [[Møde med Anders]]", []NoteLink{{"note", "Møde med Anders"}}},
|
|
|
|
|
{"the same tag twice is one link", "#Firma og #Firma igen",
|
2026-08-19 18:48:00 +02:00
|
|
|
[]NoteLink{{"project", "firma"}}},
|
|
|
|
|
{"two spellings are one tag", "#Firma og #firma",
|
|
|
|
|
[]NoteLink{{"project", "firma"}}},
|
2026-08-19 12:47:50 +02:00
|
|
|
{"a heading is not a tag", "# Overskrift", nil},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
got := LinksIn(c.body)
|
|
|
|
|
if len(got) != len(c.want) {
|
|
|
|
|
t.Fatalf("got %v, want %v", got, c.want)
|
|
|
|
|
}
|
|
|
|
|
for i := range got {
|
|
|
|
|
if got[i] != c.want[i] {
|
|
|
|
|
t.Errorf("got %v, want %v", got[i], c.want[i])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The number beside a project is what is left to do: not finished, not deleted.
|
|
|
|
|
// It replaced a count of people, which said 2 on an empty project.
|
|
|
|
|
func TestTheProjectCountIsWhatIsLeft(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
p := &Project{Name: "Tallene", OwnerID: userID}
|
|
|
|
|
if err := db.CreateProject(ctx, p); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
count := func() int {
|
|
|
|
|
list, err := db.ListProjects(ctx, userID, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, x := range list {
|
|
|
|
|
if x.ID == p.ID {
|
|
|
|
|
return x.OpenCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
t.Fatal("the project is not in the list")
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if n := count(); n != 0 {
|
|
|
|
|
t.Errorf("an empty project counts %d", n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var ids []string
|
|
|
|
|
for _, c := range []string{"en", "to", "tre"} {
|
|
|
|
|
task := &Task{ProjectID: p.ID, Content: c, CreatedBy: userID}
|
|
|
|
|
if err := db.CreateTask(ctx, task, nil); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
ids = append(ids, task.ID)
|
|
|
|
|
}
|
|
|
|
|
if n := count(); n != 3 {
|
|
|
|
|
t.Errorf("three tasks count %d", n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := db.CompleteTask(ctx, ids[0], userID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n := count(); n != 2 {
|
|
|
|
|
t.Errorf("after finishing one, the count is %d, want 2", n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.DeleteTask(ctx, ids[1]); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n := count(); n != 1 {
|
|
|
|
|
t.Errorf("after deleting one, the count is %d, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 18:36:31 +02:00
|
|
|
|
|
|
|
|
// The title follows the first line, always — not only the first time.
|
|
|
|
|
//
|
|
|
|
|
// Derived once and then left alone, it goes stale the moment somebody rewrites
|
|
|
|
|
// the opening, and the list ends up calling a note by a name that is nowhere in
|
|
|
|
|
// it. That is exactly what happened: a note headed "Ny note" was listed as
|
|
|
|
|
// "fdgfgfgh", which was what its first line had been some edits ago.
|
|
|
|
|
func TestTheTitleFollowsTheFirstLine(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
n := &Note{CreatedBy: userID, Body: "Første udgave\n\nnoget tekst"}
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n.Title != "Første udgave" {
|
|
|
|
|
t.Fatalf("title started as %q", n.Title)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
n.Body = "# Ny note\n\nDette er mit nye noteprogram"
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n.Title != "Ny note" {
|
|
|
|
|
t.Errorf("after rewriting the first line the title is %q", n.Title)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
back, err := db.Note(ctx, n.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if back.Title != "Ny note" {
|
|
|
|
|
t.Errorf("the stored title is %q", back.Title)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// And a title cannot be set beside the text, because then the two can disagree.
|
|
|
|
|
n.Title = "noget andet"
|
|
|
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if n.Title != "Ny note" {
|
|
|
|
|
t.Errorf("a title set by hand survived as %q", n.Title)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 18:48:00 +02:00
|
|
|
|
|
|
|
|
// A note that names a project in the other case must still turn up on it.
|
|
|
|
|
//
|
|
|
|
|
// It did not, and nothing said why: the key was stored exactly as typed and
|
|
|
|
|
// looked up the same way, so #garageristeriet and #GarageRisteriet were two
|
|
|
|
|
// different things that looked identical to a person.
|
|
|
|
|
func TestATagFindsItsProjectWhateverTheCase(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
if err := db.SaveNote(ctx, &Note{
|
|
|
|
|
CreatedBy: userID,
|
|
|
|
|
Body: "Bønnerne kommer fredag #garageristeriet",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, spelling := range []string{"GarageRisteriet", "garageristeriet", "GARAGERISTERIET"} {
|
|
|
|
|
found, err := db.NotesLinking(ctx, "project", spelling)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(found) != 1 {
|
|
|
|
|
t.Errorf("looking up %q found %d notes, want 1", spelling, len(found))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A note title is a title, not a key: two notes called "Møde" and "møde" are
|
|
|
|
|
// two notes, and folding them together would merge things somebody kept apart.
|
|
|
|
|
if err := db.SaveNote(ctx, &Note{CreatedBy: userID, Body: "se [[Møde]]"}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if found, _ := db.NotesLinking(ctx, "note", "møde"); len(found) != 0 {
|
|
|
|
|
t.Error("a note title was folded")
|
|
|
|
|
}
|
|
|
|
|
if found, _ := db.NotesLinking(ctx, "note", "Møde"); len(found) != 1 {
|
|
|
|
|
t.Error("a note title did not match itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-20 09:58:41 +02:00
|
|
|
|
|
|
|
|
// En søgning skal svare med det, der passer bedst, øverst.
|
|
|
|
|
//
|
|
|
|
|
// Den sorterede efter `updated_at`, hvilket var usynligt med ti noter og
|
|
|
|
|
// ubrugeligt med tolv hundrede: de blev alle importeret samme aften, så datoen
|
|
|
|
|
// siger ingenting, og loftet skar så listen af ved halvtreds tilfældige rækker.
|
|
|
|
|
// At søge "Tesla" svarede med en note om nginx først og noten, der hedder "Tesla
|
|
|
|
|
// Model Y", bagefter — hvilket læses som en søgning, der ikke virker, fordi det
|
|
|
|
|
// fra brugerens plads er præcis dét.
|
|
|
|
|
func TestSearchAnswersWithTheBestMatchFirst(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
// Skrevet i den rækkefølge, der ville give det forkerte svar: den, der kun
|
|
|
|
|
// nævner ordet i kroppen, gemmes sidst og har derfor den nyeste dato.
|
|
|
|
|
for _, body := range []string{
|
|
|
|
|
"# Tesla Model Y\n\nBilen selv.\n",
|
|
|
|
|
"# Ladestandere\n\nNoter om Tesla og andre mærker.\n",
|
|
|
|
|
"# nginx proxy manager backup\n\nEn konfiguration. Tesla nævnes i forbifarten.\n",
|
|
|
|
|
} {
|
|
|
|
|
if err := db.SaveNote(ctx, &Note{CreatedBy: userID, Body: body}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
found, err := db.SearchNotes(ctx, "Tesla", 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(found) != 3 {
|
|
|
|
|
t.Fatalf("fandt %d noter, ventede 3", len(found))
|
|
|
|
|
}
|
|
|
|
|
if found[0].Title != "Tesla Model Y" {
|
|
|
|
|
t.Errorf("øverst stod %q; den note, der hedder det, man søgte efter, skal stå først", found[0].Title)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-20 18:33:22 +02:00
|
|
|
|
|
|
|
|
// A word inside a word.
|
|
|
|
|
//
|
|
|
|
|
// Reported as "søgning har ikke det store held": a note holding
|
|
|
|
|
// `gamerule keepInventory true` could not be found by typing "keep inventory".
|
|
|
|
|
// The index is built of tokens, and `keepInventory` is one token — so the two
|
|
|
|
|
// words are nowhere, and the search says there is nothing while the note is right
|
|
|
|
|
// there.
|
|
|
|
|
//
|
|
|
|
|
// A decade of notes is full of these: commands, identifiers, file names. Every one
|
|
|
|
|
// of them is several words the index thinks is one.
|
|
|
|
|
func TestAWordInsideAWordIsStillFound(t *testing.T) {
|
|
|
|
|
db, userID := sealedStore(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
if err := db.SaveNote(ctx, &Note{
|
|
|
|
|
CreatedBy: userID,
|
|
|
|
|
Body: "Minecraft Bedrock\n\ngamerule keepInventory true",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
// A second note, so a fallback that simply returns everything would be caught.
|
|
|
|
|
if err := db.SaveNote(ctx, &Note{
|
|
|
|
|
CreatedBy: userID,
|
|
|
|
|
Body: "Indkøb\n\nKaffe og filtre",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, q := range []string{
|
|
|
|
|
"keep inventory", // the two words, which is how somebody looks for it
|
|
|
|
|
"inventory", // the tail on its own — no prefix query can reach this
|
|
|
|
|
"keepinventory", // written as one, which the index does hold
|
|
|
|
|
"KEEP INVENTORY", // and case is not a question anybody is asking
|
|
|
|
|
"vacuum", // not in either note
|
|
|
|
|
} {
|
|
|
|
|
found, err := db.SearchNotes(ctx, q, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("%q: %v", q, err)
|
|
|
|
|
}
|
|
|
|
|
want := 1
|
|
|
|
|
if q == "vacuum" {
|
|
|
|
|
want = 0
|
|
|
|
|
}
|
|
|
|
|
if len(found) != want {
|
|
|
|
|
t.Errorf("%q found %d notes, want %d", q, len(found), want)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if want == 1 && found[0].Title != "Minecraft Bedrock" {
|
|
|
|
|
t.Errorf("%q found %q", q, found[0].Title)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The fast path is still the fast path: a word the index does hold must not be
|
|
|
|
|
// answered by the scan, or the ranking that was built for twelve hundred notes
|
|
|
|
|
// is quietly gone.
|
|
|
|
|
found, err := db.SearchNotes(ctx, "Bedrock", 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(found) != 1 {
|
|
|
|
|
t.Fatalf("an ordinary word found %d notes", len(found))
|
|
|
|
|
}
|
|
|
|
|
}
|