411 lines
11 KiB
Go
411 lines
11 KiB
Go
package source
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidateDocumentValid(t *testing.T) {
|
|
doc := validDocument()
|
|
|
|
if err := ValidateDocument(doc); err != nil {
|
|
t.Fatalf("ValidateDocument() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateDocumentNil(t *testing.T) {
|
|
err := ValidateDocument(nil)
|
|
|
|
requireErrorFragments(t, err, "source document", "nil")
|
|
}
|
|
|
|
func TestValidateDocumentMissingFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*SourceDocument)
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "id",
|
|
mutate: func(doc *SourceDocument) { doc.ID = " \t" },
|
|
fragments: []string{"source document id", "must not be empty"},
|
|
},
|
|
{
|
|
name: "id surrounding whitespace",
|
|
mutate: func(doc *SourceDocument) { doc.ID = " source-1 " },
|
|
fragments: []string{"source document id", "leading or trailing whitespace"},
|
|
},
|
|
{
|
|
name: "kind",
|
|
mutate: func(doc *SourceDocument) { doc.Kind = "" },
|
|
fragments: []string{"source document kind", "must not be empty"},
|
|
},
|
|
{
|
|
name: "format",
|
|
mutate: func(doc *SourceDocument) { doc.Format = "\n" },
|
|
fragments: []string{"source document format", "must not be empty"},
|
|
},
|
|
{
|
|
name: "digest",
|
|
mutate: func(doc *SourceDocument) { doc.Digest = "" },
|
|
fragments: []string{"source document digest", "must not be empty"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
doc := validDocument()
|
|
tt.mutate(doc)
|
|
|
|
err := ValidateDocument(doc)
|
|
|
|
requireErrorFragments(t, err, tt.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDocumentEmptyUnits(t *testing.T) {
|
|
doc := validDocument()
|
|
doc.Units = nil
|
|
|
|
err := ValidateDocument(doc)
|
|
|
|
requireErrorFragments(t, err, "source document units", "must not be empty")
|
|
}
|
|
|
|
func TestValidateDocumentMissingUnitFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*SourceDocument)
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "id",
|
|
mutate: func(doc *SourceDocument) { doc.Units[1].ID = 0 },
|
|
fragments: []string{"source unit[1].id", "must be positive"},
|
|
},
|
|
{
|
|
name: "kind",
|
|
mutate: func(doc *SourceDocument) { doc.Units[1].Kind = " " },
|
|
fragments: []string{"source unit[1].kind", "must not be empty"},
|
|
},
|
|
{
|
|
name: "text",
|
|
mutate: func(doc *SourceDocument) { doc.Units[1].Text = "\n\t" },
|
|
fragments: []string{"source unit[1].text", "must not be empty"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
doc := validDocument()
|
|
tt.mutate(doc)
|
|
|
|
err := ValidateDocument(doc)
|
|
|
|
requireErrorFragments(t, err, tt.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDocumentDuplicateUnitIDs(t *testing.T) {
|
|
doc := validDocument()
|
|
doc.Units[1].ID = 1
|
|
|
|
err := ValidateDocument(doc)
|
|
|
|
requireErrorFragments(t, err, "source unit id 1", "duplicated")
|
|
}
|
|
|
|
func TestValidateDocumentUnitReferences(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*SourceDocument)
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "nested reference context",
|
|
mutate: func(doc *SourceDocument) { doc.Units[0].Ref.SourceID = "source-2" },
|
|
fragments: []string{"source unit[0].ref", "source_id", "does not match"},
|
|
},
|
|
{
|
|
name: "document unit self-reference",
|
|
mutate: func(doc *SourceDocument) {
|
|
doc.Units[0].Ref.StartUnitID = 2
|
|
doc.Units[0].Ref.EndUnitID = 2
|
|
},
|
|
fragments: []string{"source unit[0].ref", "must identify source unit id 1"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
doc := validDocument()
|
|
tt.mutate(doc)
|
|
|
|
err := ValidateDocument(doc)
|
|
requireErrorFragments(t, err, tt.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDigestDocumentIsDeterministicAndIncludesUnitReference(t *testing.T) {
|
|
doc := validDocument()
|
|
doc.Metadata = map[string]any{"second": "value", "first": true}
|
|
first, err := DigestDocument(doc)
|
|
if err != nil {
|
|
t.Fatalf("DigestDocument() error = %v, want nil", err)
|
|
}
|
|
|
|
reordered := validDocument()
|
|
reordered.Metadata = map[string]any{"first": true, "second": "value"}
|
|
second, err := DigestDocument(reordered)
|
|
if err != nil {
|
|
t.Fatalf("DigestDocument(reordered) error = %v, want nil", err)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second)
|
|
}
|
|
|
|
changed := validDocument()
|
|
changed.Metadata = map[string]any{"first": true, "second": "value"}
|
|
changed.Units[0].Ref.SourceID = "different-source"
|
|
changedDigest, err := DigestDocument(changed)
|
|
if err != nil {
|
|
t.Fatalf("DigestDocument(changed) error = %v, want nil", err)
|
|
}
|
|
if first == changedDigest {
|
|
t.Fatalf("digest = %q after reference change, want different digest", changedDigest)
|
|
}
|
|
}
|
|
|
|
func TestDigestChunkIsDeterministicAndIncludesReference(t *testing.T) {
|
|
doc := validDocument()
|
|
chunk := Chunk{
|
|
ID: "chunk-1",
|
|
SourceID: doc.ID,
|
|
Index: 0,
|
|
Ref: SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2},
|
|
Content: []byte("chunk content"),
|
|
MediaType: "text/plain",
|
|
Units: doc.Units,
|
|
Metadata: map[string]any{"second": "value", "first": true},
|
|
}
|
|
first, err := DigestChunk(chunk)
|
|
if err != nil {
|
|
t.Fatalf("DigestChunk() error = %v, want nil", err)
|
|
}
|
|
|
|
chunk.Metadata = map[string]any{"first": true, "second": "value"}
|
|
second, err := DigestChunk(chunk)
|
|
if err != nil {
|
|
t.Fatalf("DigestChunk(reordered metadata) error = %v, want nil", err)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second)
|
|
}
|
|
|
|
chunk.Ref.EndUnitID = 1
|
|
changed, err := DigestChunk(chunk)
|
|
if err != nil {
|
|
t.Fatalf("DigestChunk(changed ref) error = %v, want nil", err)
|
|
}
|
|
if first == changed {
|
|
t.Fatalf("digest = %q after reference change, want different digest", changed)
|
|
}
|
|
}
|
|
|
|
func TestDigestChunkIncludesAnnotationScopes(t *testing.T) {
|
|
doc := validDocument()
|
|
chunk := Chunk{
|
|
ID: "chunk-1", SourceID: doc.ID, Ref: SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2},
|
|
Content: []byte("content"), MediaType: "text/plain", Units: doc.Units,
|
|
Annotations: ChunkAnnotations{"scope": json.RawMessage(`{"value":1}`)},
|
|
PlanAnnotations: ChunkAnnotations{"scope": json.RawMessage(`{"value":2}`)},
|
|
}
|
|
base, err := DigestChunk(chunk)
|
|
if err != nil {
|
|
t.Fatalf("DigestChunk() error = %v", err)
|
|
}
|
|
chunk.Annotations["scope"] = json.RawMessage(`{"value":3}`)
|
|
rangeChanged, _ := DigestChunk(chunk)
|
|
chunk.Annotations["scope"] = json.RawMessage(`{"value":1}`)
|
|
chunk.PlanAnnotations["scope"] = json.RawMessage(`{"value":3}`)
|
|
planChanged, _ := DigestChunk(chunk)
|
|
if base == rangeChanged || base == planChanged || rangeChanged == planChanged {
|
|
t.Fatalf("annotation scope digests did not change distinctly: %q %q %q", base, rangeChanged, planChanged)
|
|
}
|
|
}
|
|
|
|
func TestValidateRefValid(t *testing.T) {
|
|
doc := validDocument()
|
|
ref := SourceRef{
|
|
SourceID: "source-1",
|
|
StartUnitID: 1,
|
|
EndUnitID: 2,
|
|
}
|
|
|
|
if err := ValidateRef(doc, ref); err != nil {
|
|
t.Fatalf("ValidateRef() error = %v, want nil", err)
|
|
}
|
|
if err := NewDocumentIndex(doc).ValidateRef(ref); err != nil {
|
|
t.Fatalf("DocumentIndex.ValidateRef() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRefRejectsMalformedReferences(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ref SourceRef
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "missing source id",
|
|
ref: SourceRef{StartUnitID: 1, EndUnitID: 2},
|
|
fragments: []string{"source_id", "must not be empty"},
|
|
},
|
|
{
|
|
name: "source id surrounding whitespace",
|
|
ref: SourceRef{SourceID: " source-1 ", StartUnitID: 1, EndUnitID: 2},
|
|
fragments: []string{"source_id", "leading or trailing whitespace"},
|
|
},
|
|
{
|
|
name: "missing start id",
|
|
ref: SourceRef{SourceID: "source-1", EndUnitID: 2},
|
|
fragments: []string{"start_unit_id", "must be positive"},
|
|
},
|
|
{
|
|
name: "missing end id",
|
|
ref: SourceRef{SourceID: "source-1", StartUnitID: 1},
|
|
fragments: []string{"end_unit_id", "must be positive"},
|
|
},
|
|
{
|
|
name: "unknown start id",
|
|
ref: SourceRef{SourceID: "source-1", StartUnitID: 9, EndUnitID: 2},
|
|
fragments: []string{"start_unit_id", "was not found"},
|
|
},
|
|
{
|
|
name: "unknown end id",
|
|
ref: SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 9},
|
|
fragments: []string{"end_unit_id", "was not found"},
|
|
},
|
|
{
|
|
name: "source id mismatch",
|
|
ref: SourceRef{SourceID: "source-2", StartUnitID: 1, EndUnitID: 2},
|
|
fragments: []string{"source_id", "does not match"},
|
|
},
|
|
{
|
|
name: "reversed unit order",
|
|
ref: SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 1},
|
|
fragments: []string{"start_unit_id", "appears after"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
doc := validDocument()
|
|
validators := []struct {
|
|
name string
|
|
validate func(SourceRef) error
|
|
}{
|
|
{name: "document", validate: func(ref SourceRef) error { return ValidateRef(doc, ref) }},
|
|
{name: "index", validate: NewDocumentIndex(doc).ValidateRef},
|
|
}
|
|
for _, validator := range validators {
|
|
t.Run(validator.name, func(t *testing.T) {
|
|
requireErrorFragments(t, validator.validate(tt.ref), tt.fragments...)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocumentIndexSnapshotsIdentityAndUnitPositions(t *testing.T) {
|
|
doc := &SourceDocument{
|
|
ID: "source-1",
|
|
Units: []SourceUnit{
|
|
{ID: 30},
|
|
{ID: 10},
|
|
{ID: 30},
|
|
},
|
|
}
|
|
index := NewDocumentIndex(doc)
|
|
doc.ID = "changed"
|
|
doc.Units[0].ID = 99
|
|
|
|
if position, ok := index.Position(30); !ok || position != 0 {
|
|
t.Fatalf("Position(30) = %d, %t, want 0, true", position, ok)
|
|
}
|
|
if position, ok := index.Position(10); !ok || position != 1 {
|
|
t.Fatalf("Position(10) = %d, %t, want 1, true", position, ok)
|
|
}
|
|
ref := SourceRef{SourceID: "source-1", StartUnitID: 30, EndUnitID: 10}
|
|
if err := index.ValidateRef(ref); err != nil {
|
|
t.Fatalf("ValidateRef() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestZeroDocumentIndexIsSafe(t *testing.T) {
|
|
var index DocumentIndex
|
|
if position, ok := index.Position(1); ok || position != 0 {
|
|
t.Fatalf("Position(1) = %d, %t, want 0, false", position, ok)
|
|
}
|
|
requireErrorFragments(t, index.ValidateRef(SourceRef{}), "source document must not be nil")
|
|
}
|
|
|
|
func TestUnitIndex(t *testing.T) {
|
|
doc := validDocument()
|
|
|
|
index, ok := UnitIndex(doc, 2)
|
|
if !ok {
|
|
t.Fatal("UnitIndex() ok = false, want true")
|
|
}
|
|
if index != 1 {
|
|
t.Fatalf("UnitIndex() index = %d, want 1", index)
|
|
}
|
|
|
|
index, ok = UnitIndex(doc, 9)
|
|
if ok {
|
|
t.Fatal("UnitIndex() ok = true, want false")
|
|
}
|
|
if index != 0 {
|
|
t.Fatalf("UnitIndex() index = %d, want 0", index)
|
|
}
|
|
}
|
|
|
|
func validDocument() *SourceDocument {
|
|
return &SourceDocument{
|
|
ID: "source-1",
|
|
Kind: "document",
|
|
Format: "text/plain",
|
|
Digest: "sha256:abc123",
|
|
Units: []SourceUnit{
|
|
{
|
|
ID: 1,
|
|
Kind: "paragraph",
|
|
Text: "First unit.",
|
|
Ref: SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1},
|
|
},
|
|
{
|
|
ID: 2,
|
|
Kind: "paragraph",
|
|
Text: "Second unit.",
|
|
Ref: SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 2},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func requireErrorFragments(t *testing.T, err error, fragments ...string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Fatalf("error = nil, want fragments %q", fragments)
|
|
}
|
|
for _, fragment := range fragments {
|
|
if !strings.Contains(err.Error(), fragment) {
|
|
t.Fatalf("error = %q, want fragment %q", err.Error(), fragment)
|
|
}
|
|
}
|
|
}
|