x/model: add roundtrip for String test

This commit is contained in:
Blake Mizerany 2024-04-07 16:08:46 -07:00
parent 2100129e83
commit bdff89bc4c
3 changed files with 55 additions and 59 deletions

View File

@ -49,5 +49,10 @@ func TestDigestString(t *testing.T) {
if got != want { if got != want {
t.Errorf("ParseDigest(%q).String() = %q; want %q", s, got, want) t.Errorf("ParseDigest(%q).String() = %q; want %q", s, got, want)
} }
got = ParseDigest(s).String()
if got != want {
t.Errorf("roundtrip ParseDigest(%q).String() = %q; want %q", s, got, want)
}
} }
} }

View File

@ -44,18 +44,16 @@ const (
// //
// It should be kept as the last part in the list. // It should be kept as the last part in the list.
PartInvalid PartInvalid
NumParts = PartInvalid
) )
var kindNames = map[NamePart]string{ var kindNames = map[NamePart]string{
PartInvalid: "Invalid",
PartHost: "Host", PartHost: "Host",
PartNamespace: "Namespace", PartNamespace: "Namespace",
PartModel: "Name", PartModel: "Name",
PartTag: "Tag", PartTag: "Tag",
PartBuild: "Build", PartBuild: "Build",
PartDigest: "Digest", PartDigest: "Digest",
PartInvalid: "Invalid",
} }
func (k NamePart) String() string { func (k NamePart) String() string {
@ -93,8 +91,9 @@ func (k NamePart) String() string {
// //
// To make a Name by filling in missing parts from another Name, use [Fill]. // To make a Name by filling in missing parts from another Name, use [Fill].
type Name struct { type Name struct {
_ structs.Incomparable _ structs.Incomparable
parts [NumParts]string parts [5]string // host, namespace, model, tag, build
digest Digest // digest is a special part
// TODO(bmizerany): track offsets and hold s (raw string) here? We // TODO(bmizerany): track offsets and hold s (raw string) here? We
// could pack the offests all into a single uint64 since the first // could pack the offests all into a single uint64 since the first
@ -141,6 +140,13 @@ func ParseName(s string) Name {
if kind == PartInvalid { if kind == PartInvalid {
return Name{} return Name{}
} }
if kind == PartDigest {
r.digest = ParseDigest(part)
if !r.digest.Valid() {
return Name{}
}
continue
}
r.parts[kind] = part r.parts[kind] = part
} }
if r.Valid() || r.Resolved() { if r.Valid() || r.Resolved() {
@ -233,8 +239,7 @@ var seps = [...]string{
PartNamespace: "/", PartNamespace: "/",
PartModel: ":", PartModel: ":",
PartTag: "+", PartTag: "+",
PartBuild: "@", PartBuild: "",
PartDigest: "",
} }
// WriteTo implements io.WriterTo. It writes the fullest possible display // WriteTo implements io.WriterTo. It writes the fullest possible display
@ -247,25 +252,22 @@ var seps = [...]string{
// The full digest is always prefixed with "@". That is if [Name.Valid] // The full digest is always prefixed with "@". That is if [Name.Valid]
// reports false and [Name.Resolved] reports true, then the string is // reports false and [Name.Resolved] reports true, then the string is
// returned as "@<digest-type>-<digest>". // returned as "@<digest-type>-<digest>".
func (r Name) WriteTo(w io.Writer) (n int64, err error) { func (r Name) writeTo(w io.StringWriter) {
var partsWritten int
for i := range r.parts { for i := range r.parts {
if r.parts[i] == "" { if r.parts[i] == "" {
continue continue
} }
if n > 0 || NamePart(i) == PartDigest { if partsWritten > 0 {
n1, err := io.WriteString(w, seps[i-1]) w.WriteString(seps[i-1])
n += int64(n1)
if err != nil {
return n, err
}
}
n1, err := io.WriteString(w, r.parts[i])
n += int64(n1)
if err != nil {
return n, err
} }
w.WriteString(r.parts[i])
partsWritten++
}
if r.Resolved() {
w.WriteString("@")
w.WriteString(r.digest.String())
} }
return n, nil
} }
var builderPool = sync.Pool{ var builderPool = sync.Pool{
@ -287,7 +289,7 @@ func (r Name) String() string {
defer builderPool.Put(b) defer builderPool.Put(b)
b.Reset() b.Reset()
b.Grow(50) // arbitrarily long enough for most names b.Grow(50) // arbitrarily long enough for most names
_, _ = r.WriteTo(b) r.writeTo(b)
return b.String() return b.String()
} }
@ -296,11 +298,13 @@ func (r Name) String() string {
// returns a string that includes all parts of the Name, with missing parts // returns a string that includes all parts of the Name, with missing parts
// replaced with a ("?"). // replaced with a ("?").
func (r Name) GoString() string { func (r Name) GoString() string {
var v Name
for i := range r.parts { for i := range r.parts {
v.parts[i] = cmp.Or(r.parts[i], "?") r.parts[i] = cmp.Or(r.parts[i], "?")
} }
return v.String() if !r.Resolved() {
r.digest = Digest{"?", "?"}
}
return r.String()
} }
// LogValue implements slog.Valuer. // LogValue implements slog.Valuer.
@ -320,10 +324,7 @@ func (r Name) MarshalText() ([]byte, error) {
b.Reset() b.Reset()
b.Grow(50) // arbitrarily long enough for most names b.Grow(50) // arbitrarily long enough for most names
defer bufPool.Put(b) defer bufPool.Put(b)
_, err := r.WriteTo(b) r.writeTo(b)
if err != nil {
return nil, err
}
// TODO: We can remove this alloc if/when // TODO: We can remove this alloc if/when
// https://github.com/golang/go/issues/62384 lands. // https://github.com/golang/go/issues/62384 lands.
return b.Bytes(), nil return b.Bytes(), nil
@ -393,15 +394,15 @@ func (r Name) CompleteNoBuild() bool {
// It is possible to have a valid Name, or a complete Name that is not // It is possible to have a valid Name, or a complete Name that is not
// resolved. // resolved.
func (r Name) Resolved() bool { func (r Name) Resolved() bool {
return r.parts[PartDigest] != "" return r.digest.Valid()
} }
// Digest returns the digest part of the Name, if any. // Digest returns the digest part of the Name, if any.
// //
// If Digest returns a non-empty string, then [Name.Resolved] will return // If Digest returns a non-empty string, then [Name.Resolved] will return
// true, and digest is considered valid. // true, and digest is considered valid.
func (r Name) Digest() string { func (r Name) Digest() Digest {
return r.parts[PartDigest] return r.digest
} }
// EqualFold reports whether r and o are equivalent model names, ignoring // EqualFold reports whether r and o are equivalent model names, ignoring
@ -479,24 +480,14 @@ func Parts(s string) iter.Seq2[NamePart, string] {
case '@': case '@':
switch state { switch state {
case PartDigest: case PartDigest:
part := s[i+1:] if !yieldValid(PartDigest, s[i+1:j]) {
if isValidDigest(part) { return
if !yield(PartDigest, part) { }
return if i == 0 {
} // This is the form
if i == 0 { // "@<digest>" which is valid.
// The name is in //
// the form of // We're done.
// "@digest". This
// is valid ans so
// we want to skip
// the final
// validation for
// any other state.
return
}
} else {
yield(PartInvalid, "")
return return
} }
state, j, partLen = PartBuild, i, 0 state, j, partLen = PartBuild, i, 0

View File

@ -23,7 +23,7 @@ func fieldsFromName(p Name) fields {
model: p.parts[PartModel], model: p.parts[PartModel],
tag: p.parts[PartTag], tag: p.parts[PartTag],
build: p.parts[PartBuild], build: p.parts[PartBuild],
digest: p.parts[PartDigest], digest: p.digest.String(),
} }
} }
@ -101,8 +101,8 @@ var testNames = map[string]fields{
func TestNameParts(t *testing.T) { func TestNameParts(t *testing.T) {
var p Name var p Name
if len(p.Parts()) != int(NumParts) { if w, g := int(PartBuild+1), len(p.Parts()); w != g {
t.Errorf("Parts() = %d; want %d", len(p.Parts()), NumParts) t.Errorf("Parts() = %d; want %d", g, w)
} }
} }
@ -137,7 +137,7 @@ func TestParseName(t *testing.T) {
// test round-trip // test round-trip
if !ParseName(name.String()).EqualFold(name) { if !ParseName(name.String()).EqualFold(name) {
t.Errorf("String() = %s; want %s", name.String(), baseName) t.Errorf("ParseName(%q).String() = %s; want %s", s, name.String(), baseName)
} }
if name.Valid() && name.DisplayModel() == "" { if name.Valid() && name.DisplayModel() == "" {
@ -146,9 +146,9 @@ func TestParseName(t *testing.T) {
t.Errorf("Valid() = false; Model() = %q; want empty name", got.model) t.Errorf("Valid() = false; Model() = %q; want empty name", got.model)
} }
if name.Resolved() && name.Digest() == "" { if name.Resolved() && !name.Digest().Valid() {
t.Errorf("Resolved() = true; Digest() = %q; want non-empty digest", got.digest) t.Errorf("Resolved() = true; Digest() = %q; want non-empty digest", got.digest)
} else if !name.Resolved() && name.Digest() != "" { } else if !name.Resolved() && name.Digest().Valid() {
t.Errorf("Resolved() = false; Digest() = %q; want empty digest", got.digest) t.Errorf("Resolved() = false; Digest() = %q; want empty digest", got.digest)
} }
}) })
@ -233,7 +233,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "example.com/library/mistral:latest", wantComplete: "example.com/library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "example.com/library/mistral:latest+Q4_0@?", wantGoString: "example.com/library/mistral:latest+Q4_0@?-?",
}, },
{ {
name: "Short Name", name: "Short Name",
@ -242,7 +242,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "mistral:latest", wantLong: "mistral:latest",
wantComplete: "mistral:latest", wantComplete: "mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/?/mistral:latest+?@?", wantGoString: "?/?/mistral:latest+?@?-?",
}, },
{ {
name: "Long Name", name: "Long Name",
@ -251,7 +251,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "library/mistral:latest", wantComplete: "library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/library/mistral:latest+?@?", wantGoString: "?/library/mistral:latest+?@?-?",
}, },
{ {
name: "Case Preserved", name: "Case Preserved",
@ -260,7 +260,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "Library/Mistral:Latest", wantLong: "Library/Mistral:Latest",
wantComplete: "Library/Mistral:Latest", wantComplete: "Library/Mistral:Latest",
wantModel: "Mistral", wantModel: "Mistral",
wantGoString: "?/Library/Mistral:Latest+?@?", wantGoString: "?/Library/Mistral:Latest+?@?-?",
}, },
{ {
name: "With digest", name: "With digest",