mirror of
https://github.com/samuelncui/yatm.git
synced 2026-11-08 12:26:33 +00:00
Unify file queries and Scan execution, retain indexed Identical results, improve Job and Preview workflows, and verify release packaging.
138 lines
5.9 KiB
Go
138 lines
5.9 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/samuelncui/yatm/entity"
|
|
"github.com/samuelncui/yatm/internal/executor/observation"
|
|
"github.com/samuelncui/yatm/internal/library"
|
|
"github.com/samuelncui/yatm/internal/resource"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func TestSelectionStageAndCaptureReuseBatchFacts(t *testing.T) {
|
|
// Seed known original/native facts so the measured path does not need content hashing.
|
|
ctx := context.Background()
|
|
exe := setupTestExecutor(t)
|
|
require.NoError(t, exe.lib.AutoMigrate())
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
require.NoError(t, err)
|
|
exe = New(exe.db, exe.lib, nil, Paths{Access: []AccessRange{{Root: root}}}, Scripts{}, nil)
|
|
location := &library.Location{Name: "batch", RootPath: root, ExecutorID: "local"}
|
|
require.NoError(t, exe.lib.CreateLocation(ctx, location))
|
|
var ids []int64
|
|
var items []*observation.Item
|
|
hash := sha256.Sum256([]byte("content"))
|
|
for i := 0; i < 201; i++ {
|
|
file := &library.File{Name: fmt.Sprintf("file-%03d", i)}
|
|
require.NoError(t, exe.lib.SaveFile(ctx, file))
|
|
name := filepath.Join(root, file.Name)
|
|
require.NoError(t, os.WriteFile(name, []byte("content"), 0644))
|
|
info, err := os.Stat(name)
|
|
require.NoError(t, err)
|
|
original := &library.FileLocation{FileID: file.ID, LocationID: location.ID, Path: file.Name, Mode: uint32(info.Mode()), Size: info.Size(), MtimeNS: info.ModTime().UnixNano(), Hash: hash[:], Signature: []byte("opaque")}
|
|
require.NoError(t, exe.db.Create(original).Error)
|
|
keys := ObserveTracking(location, info)
|
|
for _, key := range keys {
|
|
key.FileID, key.LocationID = file.ID, location.ID
|
|
require.NoError(t, exe.db.Create(key).Error)
|
|
}
|
|
entry, err := locationEntry(location.ID, file.Name, info)
|
|
require.NoError(t, err)
|
|
item, err := selectedFileObservation(location, entry.Reference, info)
|
|
require.NoError(t, err)
|
|
items, ids = append(items, item), append(ids, file.ID)
|
|
}
|
|
queries := 0
|
|
require.NoError(t, exe.db.Callback().Query().After("gorm:query").Register("selection-batch-cost", func(*gorm.DB) { queries++ }))
|
|
staged, err := resource.OpenTemporaryDB(t.TempDir(), "stage-")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, staged.Close()) })
|
|
require.NoError(t, staged.DB.AutoMigrate(&observation.Item{}))
|
|
require.NoError(t, exe.stageSelectedFiles(ctx, staged.DB, location, items))
|
|
require.LessOrEqual(t, queries, 6, "association and tracking reads are bounded batches")
|
|
for _, item := range items {
|
|
require.Equal(t, []byte("opaque"), item.Signature)
|
|
require.Equal(t, hash[:], item.Hash)
|
|
}
|
|
|
|
// Capture uses the same observation-matching rule and preserves order and opaque signatures.
|
|
queries = 0
|
|
var captured []int64
|
|
require.NoError(t, exe.CaptureOriginals(ctx, append(ids, ids[len(ids)-1]+1), func(id int64, name string, expected *entity.ExpectedFile) error {
|
|
require.Equal(t, filepath.Join(root, fmt.Sprintf("file-%03d", len(captured))), name)
|
|
require.Equal(t, []byte("opaque"), expected.Signature)
|
|
require.Equal(t, hash[:], expected.Sha256)
|
|
captured = append(captured, id)
|
|
return nil
|
|
}))
|
|
require.Equal(t, ids, captured)
|
|
require.LessOrEqual(t, queries, 12, "unchanged admission cannot issue per-row catalog queries")
|
|
|
|
// Full physical preparation freezes the same identities and emits only identity and logical paths.
|
|
queries = 0
|
|
require.NoError(t, exe.stageLiveSelections(ctx, staged.DB, location.ID, []*entity.LocationSelection{{LocationId: location.ID}}))
|
|
var selected []int64
|
|
require.NoError(t, exe.yieldStagedSelections(ctx, staged.DB, func(file *library.File, target string) error {
|
|
require.Equal(t, file.Name, target)
|
|
require.Zero(t, file.Size)
|
|
require.Nil(t, file.ContentSummary)
|
|
selected = append(selected, file.ID)
|
|
return nil
|
|
}))
|
|
require.Equal(t, ids, selected)
|
|
require.LessOrEqual(t, queries, 90, "publication and final identity/path projection also batch their reads")
|
|
|
|
// Duplicate complete roots must not multiply observations before staging deduplication.
|
|
observed := 0
|
|
require.NoError(t, staged.DB.Callback().Create().Before("gorm:create").Register("selection-observation-count", func(tx *gorm.DB) {
|
|
conflict, _ := tx.Statement.Clauses["ON CONFLICT"].Expression.(clause.OnConflict)
|
|
rows, ok := tx.Statement.Dest.(*[]*observation.Item)
|
|
if ok && conflict.DoNothing {
|
|
observed += len(*rows)
|
|
require.LessOrEqual(t, len(*rows), observation.BatchSize)
|
|
}
|
|
}))
|
|
var roots []*entity.LocationSelection
|
|
for i := 0; i < 40; i++ {
|
|
roots = append(roots, &entity.LocationSelection{LocationId: location.ID})
|
|
}
|
|
roots = append(roots, &entity.LocationSelection{LocationId: location.ID, Path: "file-000"})
|
|
require.NoError(t, exe.stageLiveSelections(ctx, staged.DB, location.ID, roots))
|
|
require.Equal(t, len(ids)+1, observed, "one observation per File plus the root directory")
|
|
|
|
// Changed admissions also share transaction-local validation facts; actual row writes remain atomic.
|
|
matches := make([]*library.ObservationAdmission, 0, len(items))
|
|
for i, item := range items {
|
|
position := item.Position()
|
|
position.FileID, position.Size = ids[i], position.Size+1
|
|
matches = append(matches, &library.ObservationAdmission{Observation: position})
|
|
}
|
|
queries = 0
|
|
admitted, err := exe.lib.AdmitObservations(ctx, location.ID, matches)
|
|
require.NoError(t, err)
|
|
require.Len(t, admitted, len(items))
|
|
require.LessOrEqual(t, queries, 25, "changed admission cannot reload original/identity/native facts per File")
|
|
for _, original := range admitted {
|
|
require.EqualValues(t, 8, original.Size)
|
|
}
|
|
|
|
// Single-file missing identity remains typed, and cancellation cannot emit partial captures.
|
|
_, _, err = exe.CaptureOriginal(ctx, 0)
|
|
require.ErrorIs(t, err, library.ErrFileNotFound)
|
|
cancelled, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
err = exe.CaptureOriginals(cancelled, ids, func(int64, string, *entity.ExpectedFile) error {
|
|
t.Fatal("cancelled capture emitted a row")
|
|
return nil
|
|
})
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|