mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
167 lines
7.1 KiB
Go
167 lines
7.1 KiB
Go
package ai
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
// stPkg builds a model package carrying the given metadata, with each path
|
|
// recorded as a primary-evidence location.
|
|
func stPkg(md pkg.SafeTensorsModelInfo, paths ...string) pkg.Package {
|
|
locs := make([]file.Location, 0, len(paths))
|
|
for _, p := range paths {
|
|
locs = append(locs, file.NewLocation(p).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
|
}
|
|
return pkg.Package{Type: pkg.ModelPkg, Metadata: md, Locations: file.NewLocationSet(locs...)}
|
|
}
|
|
|
|
// shardMeta is a content-derived shard entry: it carries a MetadataHash, which is
|
|
// what marks a group member as a shard (vs. a hash-less aggregate config blob).
|
|
func shardMeta(hash string, tensorCount uint64) pkg.SafeTensorsModelInfo {
|
|
return pkg.SafeTensorsModelInfo{
|
|
Format: "safetensors",
|
|
TensorCount: tensorCount,
|
|
Quantization: "BF16",
|
|
Parameters: 1000,
|
|
MetadataHash: hash,
|
|
}
|
|
}
|
|
|
|
// TestMergeSafeTensorsGroup exercises the rollup contract directly (the cataloger
|
|
// tests cover it only as a side effect of the merge processor). It locks how a
|
|
// group's per-member metadata folds into one package: tensor-count summing,
|
|
// aggregate-over-shard field precedence, UserMetadata dedup + sorting, Parts
|
|
// rollup, ShardCount derivation, and the content MetadataHash rollup.
|
|
func TestMergeSafeTensorsGroup(t *testing.T) {
|
|
t.Run("single shard: hash passes through, ShardCount 1, no Parts", func(t *testing.T) {
|
|
out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 5), "/m/a.safetensors")})
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, pkg.ModelPkg, out.Type)
|
|
assert.Equal(t, 1, md.ShardCount)
|
|
assert.Equal(t, uint64(5), md.TensorCount)
|
|
assert.Equal(t, "aaaa", md.MetadataHash, "a single shard's hash passes through unchanged")
|
|
assert.Nil(t, md.Parts, "single-shard models do not populate Parts")
|
|
})
|
|
|
|
t.Run("multi-shard: tensors summed, Parts sorted by hash, rollup is order-independent", func(t *testing.T) {
|
|
in := []pkg.Package{
|
|
stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
|
|
stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
|
|
stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
|
|
}
|
|
out := mergeSafeTensorsGroup(in)
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, 3, md.ShardCount)
|
|
assert.Equal(t, uint64(9), md.TensorCount, "tensor counts are summed across shards")
|
|
assert.Equal(t, uint64(3000), md.Parameters, "parameter counts are summed across shards")
|
|
require.Len(t, md.Parts, 3)
|
|
assert.Equal(t,
|
|
[]string{"aaaa", "bbbb", "cccc"},
|
|
[]string{md.Parts[0].MetadataHash, md.Parts[1].MetadataHash, md.Parts[2].MetadataHash},
|
|
"Parts are sorted by metadata hash",
|
|
)
|
|
assert.Equal(t, rollupHash([]string{"aaaa", "bbbb", "cccc"}), md.MetadataHash)
|
|
|
|
// the rollup hash must not depend on the order members arrive in
|
|
shuffled := []pkg.Package{
|
|
stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
|
|
stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
|
|
stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
|
|
}
|
|
out2 := mergeSafeTensorsGroup(shuffled)
|
|
assert.Equal(t, md.MetadataHash, out2.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash)
|
|
})
|
|
|
|
t.Run("aggregate fields win over shard-derived fields", func(t *testing.T) {
|
|
// an aggregate (OCI config blob) carries no MetadataHash but declares the
|
|
// authoritative totals.
|
|
aggregate := pkg.SafeTensorsModelInfo{
|
|
Format: "safetensors",
|
|
TensorCount: 999,
|
|
TotalSize: "5.00GB",
|
|
Quantization: "Q4_K_M",
|
|
}
|
|
in := []pkg.Package{
|
|
stPkg(aggregate, "/"),
|
|
stPkg(shardMeta("aaaa", 3), "/"),
|
|
stPkg(shardMeta("bbbb", 3), "/"),
|
|
}
|
|
out := mergeSafeTensorsGroup(in)
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, uint64(999), md.TensorCount, "aggregate TensorCount is authoritative; shard counts are not summed in")
|
|
assert.Equal(t, "5.00GB", md.TotalSize)
|
|
assert.Equal(t, uint64(2000), md.Parameters, "parameters are always measured from the shards (summed), not taken from the aggregate")
|
|
assert.Equal(t, "Q4_K_M", md.Quantization, "aggregate quantization wins over the shard dtype")
|
|
assert.Equal(t, 2, md.ShardCount, "ShardCount comes from the number of shards, not the aggregate")
|
|
assert.Equal(t, rollupHash([]string{"aaaa", "bbbb"}), md.MetadataHash, "the content hash still rolls up the shard hashes")
|
|
})
|
|
|
|
t.Run("aggregate-only group: ShardCount 1, empty hash, no Parts", func(t *testing.T) {
|
|
aggregate := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 42, TotalSize: "1.00GB"}
|
|
out := mergeSafeTensorsGroup([]pkg.Package{stPkg(aggregate, "/")})
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, uint64(42), md.TensorCount)
|
|
assert.Equal(t, 1, md.ShardCount, "a group with no shards still reports a single shard")
|
|
assert.Equal(t, "", md.MetadataHash, "there are no shard hashes to roll up")
|
|
assert.Nil(t, md.Parts)
|
|
})
|
|
|
|
t.Run("UserMetadata: keys merged and sorted, first value wins on conflict", func(t *testing.T) {
|
|
// keys are intentionally unsorted within each shard so the assertion proves
|
|
// the merge re-sorts globally; "format" appears in both shards so dedup
|
|
// precedence (first wins) is exercised too.
|
|
s1 := shardMeta("aaaa", 1)
|
|
s1.UserMetadata = pkg.KeyValues{{Key: "format", Value: "pt"}, {Key: "author", Value: "alice"}}
|
|
s2 := shardMeta("bbbb", 1)
|
|
s2.UserMetadata = pkg.KeyValues{{Key: "format", Value: "gguf"}, {Key: "license", Value: "mit"}}
|
|
|
|
out := mergeSafeTensorsGroup([]pkg.Package{stPkg(s1, "/m/a.safetensors"), stPkg(s2, "/m/b.safetensors")})
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, pkg.KeyValues{
|
|
{Key: "author", Value: "alice"},
|
|
{Key: "format", Value: "pt"}, // first shard's value wins over s2's "gguf"
|
|
{Key: "license", Value: "mit"},
|
|
}, md.UserMetadata)
|
|
})
|
|
|
|
t.Run("members without safetensors metadata are ignored in the rollup", func(t *testing.T) {
|
|
notST := pkg.Package{
|
|
Type: pkg.ModelPkg,
|
|
Metadata: pkg.GGUFFileHeader{},
|
|
Locations: file.NewLocationSet(file.NewLocation("/m/x.gguf")),
|
|
}
|
|
out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 2), "/m/a.safetensors"), notST})
|
|
|
|
md := out.Metadata.(pkg.SafeTensorsModelInfo)
|
|
assert.Equal(t, uint64(2), md.TensorCount, "only the safetensors shard contributes")
|
|
assert.Equal(t, 1, md.ShardCount)
|
|
assert.Equal(t, "aaaa", md.MetadataHash)
|
|
})
|
|
}
|
|
|
|
// TestRollupHash locks the cross-source content-fingerprint rollup: empty input
|
|
// yields no hash, a lone shard's hash passes through unchanged (so a single-shard
|
|
// model fingerprints identically across directory and OCI sources), and multiple
|
|
// shards fold into one order-independent digest.
|
|
func TestRollupHash(t *testing.T) {
|
|
assert.Equal(t, "", rollupHash(nil), "no hashes → empty")
|
|
assert.Equal(t, "solo", rollupHash([]string{"solo"}), "a single hash passes through unchanged")
|
|
|
|
ab := rollupHash([]string{"a", "b"})
|
|
ba := rollupHash([]string{"b", "a"})
|
|
assert.Equal(t, ab, ba, "the rollup is independent of input order")
|
|
assert.Len(t, ab, 16, "a multi-hash rollup is a 16-char xxhash")
|
|
assert.NotEqual(t, "a", ab)
|
|
assert.NotEqual(t, "b", ab)
|
|
}
|