name_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. package model
  2. import (
  3. "bytes"
  4. "cmp"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "slices"
  9. "strings"
  10. "testing"
  11. )
  12. type fields struct {
  13. host, namespace, model, tag, build string
  14. digest string
  15. }
  16. func fieldsFromName(p Name) fields {
  17. return fields{
  18. host: p.parts[PartHost],
  19. namespace: p.parts[PartNamespace],
  20. model: p.parts[PartModel],
  21. tag: p.parts[PartTag],
  22. build: p.parts[PartBuild],
  23. digest: p.digest.String(),
  24. }
  25. }
  26. func mustParse(s string) Name {
  27. p := ParseName(s)
  28. if !p.Valid() {
  29. panic(fmt.Sprintf("invalid name: %q", s))
  30. }
  31. return p
  32. }
  33. var testNames = map[string]fields{
  34. "mistral:latest": {model: "mistral", tag: "latest"},
  35. "mistral": {model: "mistral"},
  36. "mistral:30B": {model: "mistral", tag: "30B"},
  37. "mistral:7b": {model: "mistral", tag: "7b"},
  38. "mistral:7b+Q4_0": {model: "mistral", tag: "7b", build: "Q4_0"},
  39. "mistral+KQED": {model: "mistral", build: "KQED"},
  40. "mistral.x-3:7b+Q4_0": {model: "mistral.x-3", tag: "7b", build: "Q4_0"},
  41. "mistral:7b+q4_0": {model: "mistral", tag: "7b", build: "q4_0"},
  42. "llama2": {model: "llama2"},
  43. "user/model": {namespace: "user", model: "model"},
  44. "example.com/ns/mistral:7b+Q4_0": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "Q4_0"},
  45. "example.com/ns/mistral:7b+X": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "X"},
  46. // invalid digest
  47. "mistral:latest@invalid256-": {},
  48. "mistral:latest@-123": {},
  49. "mistral:latest@!-123": {},
  50. "mistral:latest@1-!": {},
  51. "mistral:latest@": {},
  52. // resolved
  53. "x@sha123-1": {model: "x", digest: "sha123-1"},
  54. "@sha456-2": {digest: "sha456-2"},
  55. "@@sha123-1": {},
  56. // preserves case for build
  57. "x+b": {model: "x", build: "b"},
  58. // invalid (includes fuzzing trophies)
  59. " / / : + ": {},
  60. " / : + ": {},
  61. " : + ": {},
  62. " + ": {},
  63. " : ": {},
  64. " / ": {},
  65. " /": {},
  66. "/ ": {},
  67. "/": {},
  68. ":": {},
  69. "+": {},
  70. // (".") in namepsace is not allowed
  71. "invalid.com/7b+x": {},
  72. "invalid:7b+Q4_0:latest": {},
  73. "in valid": {},
  74. "invalid/y/z/foo": {},
  75. "/0": {},
  76. "0 /0": {},
  77. "0 /": {},
  78. "0/": {},
  79. ":/0": {},
  80. "+0/00000": {},
  81. "0+.\xf2\x80\xf6\x9d00000\xe5\x99\xe6\xd900\xd90\xa60\x91\xdc0\xff\xbf\x99\xe800\xb9\xdc\xd6\xc300\x970\xfb\xfd0\xe0\x8a\xe1\xad\xd40\x9700\xa80\x980\xdd0000\xb00\x91000\xfe0\x89\x9b\x90\x93\x9f0\xe60\xf7\x84\xb0\x87\xa5\xff0\xa000\x9a\x85\xf6\x85\xfe\xa9\xf9\xe9\xde00\xf4\xe0\x8f\x81\xad\xde00\xd700\xaa\xe000000\xb1\xee0\x91": {},
  82. "0//0": {},
  83. "m+^^^": {},
  84. "file:///etc/passwd": {},
  85. "file:///etc/passwd:latest": {},
  86. "file:///etc/passwd:latest+u": {},
  87. strings.Repeat("a", MaxNamePartLen): {model: strings.Repeat("a", MaxNamePartLen)},
  88. strings.Repeat("a", MaxNamePartLen+1): {},
  89. }
  90. func TestNameParts(t *testing.T) {
  91. var p Name
  92. if w, g := int(PartBuild+1), len(p.Parts()); w != g {
  93. t.Errorf("Parts() = %d; want %d", g, w)
  94. }
  95. }
  96. func TestNamePartString(t *testing.T) {
  97. if g := PartKind(-2).String(); g != "Unknown" {
  98. t.Errorf("Unknown part = %q; want %q", g, "Unknown")
  99. }
  100. for kind, name := range kindNames {
  101. if g := kind.String(); g != name {
  102. t.Errorf("%s = %q; want %q", kind, g, name)
  103. }
  104. }
  105. }
  106. func TestParseName(t *testing.T) {
  107. for baseName, want := range testNames {
  108. for _, prefix := range []string{"", "https://", "http://"} {
  109. // We should get the same results with or without the
  110. // http(s) prefixes
  111. s := prefix + baseName
  112. t.Run(s, func(t *testing.T) {
  113. for kind, part := range Parts(s) {
  114. t.Logf("Part: %s: %q", kind, part)
  115. }
  116. name := ParseName(s)
  117. got := fieldsFromName(name)
  118. if got != want {
  119. t.Errorf("ParseName(%q) = %q; want %q", s, got, want)
  120. }
  121. // test round-trip
  122. if !ParseName(name.String()).EqualFold(name) {
  123. t.Errorf("ParseName(%q).String() = %s; want %s", s, name.String(), baseName)
  124. }
  125. if name.Valid() && name.DisplayModel() == "" {
  126. t.Errorf("Valid() = true; Model() = %q; want non-empty name", got.model)
  127. } else if !name.Valid() && name.DisplayModel() != "" {
  128. t.Errorf("Valid() = false; Model() = %q; want empty name", got.model)
  129. }
  130. if name.Resolved() && !name.Digest().Valid() {
  131. t.Errorf("Resolved() = true; Digest() = %q; want non-empty digest", got.digest)
  132. } else if !name.Resolved() && name.Digest().Valid() {
  133. t.Errorf("Resolved() = false; Digest() = %q; want empty digest", got.digest)
  134. }
  135. })
  136. }
  137. }
  138. }
  139. func TestCompleteWithAndWithoutBuild(t *testing.T) {
  140. cases := []struct {
  141. in string
  142. complete bool
  143. completeNoBuild bool
  144. }{
  145. {"", false, false},
  146. {"incomplete/mistral:7b+x", false, false},
  147. {"incomplete/mistral:7b+Q4_0", false, false},
  148. {"incomplete:7b+x", false, false},
  149. {"complete.com/x/mistral:latest+Q4_0", true, true},
  150. {"complete.com/x/mistral:latest", false, true},
  151. }
  152. for _, tt := range cases {
  153. t.Run(tt.in, func(t *testing.T) {
  154. p := ParseName(tt.in)
  155. t.Logf("ParseName(%q) = %#v", tt.in, p)
  156. if g := p.Complete(); g != tt.complete {
  157. t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete)
  158. }
  159. if g := p.CompleteNoBuild(); g != tt.completeNoBuild {
  160. t.Errorf("CompleteNoBuild(%q) = %v; want %v", tt.in, g, tt.completeNoBuild)
  161. }
  162. })
  163. }
  164. // Complete uses Parts which returns a slice, but it should be
  165. // inlined when used in Complete, preventing any allocations or
  166. // escaping to the heap.
  167. allocs := testing.AllocsPerRun(1000, func() {
  168. keep(ParseName("complete.com/x/mistral:latest+Q4_0").Complete())
  169. })
  170. if allocs > 0 {
  171. t.Errorf("Complete allocs = %v; want 0", allocs)
  172. }
  173. }
  174. func TestNameLogValue(t *testing.T) {
  175. cases := []string{
  176. "example.com/library/mistral:latest+Q4_0",
  177. "mistral:latest",
  178. "mistral:7b+Q4_0",
  179. }
  180. for _, s := range cases {
  181. t.Run(s, func(t *testing.T) {
  182. var b bytes.Buffer
  183. log := slog.New(slog.NewTextHandler(&b, nil))
  184. name := ParseName(s)
  185. log.Info("", "name", name)
  186. want := fmt.Sprintf("name=%s", name.GoString())
  187. got := b.String()
  188. if !strings.Contains(got, want) {
  189. t.Errorf("expected log output to contain %q; got %q", want, got)
  190. }
  191. })
  192. }
  193. }
  194. func TestNameDisplay(t *testing.T) {
  195. cases := []struct {
  196. name string
  197. in string
  198. wantShort string
  199. wantLong string
  200. wantComplete string
  201. wantString string
  202. wantModel string
  203. wantGoString string // default is tt.in
  204. }{
  205. {
  206. name: "Complete Name",
  207. in: "example.com/library/mistral:latest+Q4_0",
  208. wantShort: "mistral:latest",
  209. wantLong: "library/mistral:latest",
  210. wantComplete: "example.com/library/mistral:latest",
  211. wantModel: "mistral",
  212. wantGoString: "example.com/library/mistral:latest+Q4_0@?-?",
  213. },
  214. {
  215. name: "Short Name",
  216. in: "mistral:latest",
  217. wantShort: "mistral:latest",
  218. wantLong: "mistral:latest",
  219. wantComplete: "mistral:latest",
  220. wantModel: "mistral",
  221. wantGoString: "?/?/mistral:latest+?@?-?",
  222. },
  223. {
  224. name: "Long Name",
  225. in: "library/mistral:latest",
  226. wantShort: "mistral:latest",
  227. wantLong: "library/mistral:latest",
  228. wantComplete: "library/mistral:latest",
  229. wantModel: "mistral",
  230. wantGoString: "?/library/mistral:latest+?@?-?",
  231. },
  232. {
  233. name: "Case Preserved",
  234. in: "Library/Mistral:Latest",
  235. wantShort: "Mistral:Latest",
  236. wantLong: "Library/Mistral:Latest",
  237. wantComplete: "Library/Mistral:Latest",
  238. wantModel: "Mistral",
  239. wantGoString: "?/Library/Mistral:Latest+?@?-?",
  240. },
  241. {
  242. name: "With digest",
  243. in: "Library/Mistral:Latest@sha256-123456",
  244. wantShort: "Mistral:Latest",
  245. wantLong: "Library/Mistral:Latest",
  246. wantComplete: "Library/Mistral:Latest",
  247. wantModel: "Mistral",
  248. wantGoString: "?/Library/Mistral:Latest+?@sha256-123456",
  249. },
  250. }
  251. for _, tt := range cases {
  252. t.Run(tt.name, func(t *testing.T) {
  253. p := ParseName(tt.in)
  254. if g := p.DisplayShort(); g != tt.wantShort {
  255. t.Errorf("DisplayShort = %q; want %q", g, tt.wantShort)
  256. }
  257. if g := p.DisplayLong(); g != tt.wantLong {
  258. t.Errorf("DisplayLong = %q; want %q", g, tt.wantLong)
  259. }
  260. if g := p.DisplayFullest(); g != tt.wantComplete {
  261. t.Errorf("DisplayFullest = %q; want %q", g, tt.wantComplete)
  262. }
  263. if g := p.String(); g != tt.in {
  264. t.Errorf("String(%q) = %q; want %q", tt.in, g, tt.in)
  265. }
  266. if g := p.DisplayModel(); g != tt.wantModel {
  267. t.Errorf("Model = %q; want %q", g, tt.wantModel)
  268. }
  269. tt.wantGoString = cmp.Or(tt.wantGoString, tt.in)
  270. if g := fmt.Sprintf("%#v", p); g != tt.wantGoString {
  271. t.Errorf("GoString() = %q; want %q", g, tt.wantGoString)
  272. }
  273. })
  274. }
  275. }
  276. func TestParseNameAllocs(t *testing.T) {
  277. allocs := testing.AllocsPerRun(1000, func() {
  278. keep(ParseName("example.com/mistral:7b+Q4_0"))
  279. })
  280. if allocs > 0 {
  281. t.Errorf("ParseName allocs = %v; want 0", allocs)
  282. }
  283. }
  284. func BenchmarkParseName(b *testing.B) {
  285. b.ReportAllocs()
  286. for range b.N {
  287. keep(ParseName("example.com/mistral:7b+Q4_0"))
  288. }
  289. }
  290. func BenchmarkNameDisplay(b *testing.B) {
  291. b.ReportAllocs()
  292. r := ParseName("example.com/mistral:7b+Q4_0")
  293. b.Run("Short", func(b *testing.B) {
  294. for range b.N {
  295. keep(r.DisplayShort())
  296. }
  297. })
  298. }
  299. func FuzzParseName(f *testing.F) {
  300. f.Add("example.com/mistral:7b+Q4_0")
  301. f.Add("example.com/mistral:7b+q4_0")
  302. f.Add("example.com/mistral:7b+x")
  303. f.Add("x/y/z:8n+I")
  304. f.Fuzz(func(t *testing.T, s string) {
  305. r0 := ParseName(s)
  306. if !r0.Valid() {
  307. if !r0.EqualFold(Name{}) {
  308. t.Errorf("expected invalid path to be zero value; got %#v", r0)
  309. }
  310. t.Skipf("invalid path: %q", s)
  311. }
  312. for _, p := range r0.Parts() {
  313. if len(p) > MaxNamePartLen {
  314. t.Errorf("part too long: %q", p)
  315. }
  316. }
  317. if !strings.EqualFold(r0.String(), s) {
  318. t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.String(), s)
  319. }
  320. r1 := ParseName(r0.String())
  321. if !r0.EqualFold(r1) {
  322. t.Errorf("round-trip mismatch: %+v != %+v", r0, r1)
  323. }
  324. })
  325. }
  326. func TestFill(t *testing.T) {
  327. cases := []struct {
  328. dst string
  329. src string
  330. want string
  331. }{
  332. {"mistral", "o.com/library/PLACEHOLDER:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
  333. {"o.com/library/mistral", "PLACEHOLDER:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
  334. {"", "o.com/library/mistral:latest+Q4_0", "o.com/library/mistral:latest+Q4_0"},
  335. }
  336. for _, tt := range cases {
  337. t.Run(tt.dst, func(t *testing.T) {
  338. r := Fill(ParseName(tt.dst), ParseName(tt.src))
  339. if r.String() != tt.want {
  340. t.Errorf("Fill(%q, %q) = %q; want %q", tt.dst, tt.src, r, tt.want)
  341. }
  342. })
  343. }
  344. }
  345. func TestNameTextMarshal(t *testing.T) {
  346. cases := []struct {
  347. in string
  348. want string
  349. wantErr error
  350. }{
  351. {"example.com/mistral:latest+Q4_0", "", nil},
  352. {"mistral:latest+Q4_0", "mistral:latest+Q4_0", nil},
  353. {"mistral:latest", "mistral:latest", nil},
  354. {"mistral", "mistral", nil},
  355. {"mistral:7b", "mistral:7b", nil},
  356. {"example.com/library/mistral:latest+Q4_0", "example.com/library/mistral:latest+Q4_0", nil},
  357. }
  358. for _, tt := range cases {
  359. t.Run(tt.in, func(t *testing.T) {
  360. p := ParseName(tt.in)
  361. got, err := p.MarshalText()
  362. if !errors.Is(err, tt.wantErr) {
  363. t.Fatalf("MarshalText() error = %v; want %v", err, tt.wantErr)
  364. }
  365. if string(got) != tt.want {
  366. t.Errorf("MarshalText() = %q; want %q", got, tt.want)
  367. }
  368. var r Name
  369. if err := r.UnmarshalText(got); err != nil {
  370. t.Fatalf("UnmarshalText() error = %v; want nil", err)
  371. }
  372. if !r.EqualFold(p) {
  373. t.Errorf("UnmarshalText() = %q; want %q", r, p)
  374. }
  375. })
  376. }
  377. t.Run("UnmarshalText into valid Name", func(t *testing.T) {
  378. // UnmarshalText should not be called on a valid Name.
  379. p := mustParse("x")
  380. if err := p.UnmarshalText([]byte("mistral:latest+Q4_0")); err == nil {
  381. t.Error("UnmarshalText() = nil; want error")
  382. }
  383. })
  384. t.Run("TextMarshal allocs", func(t *testing.T) {
  385. var data []byte
  386. name := ParseName("example.com/ns/mistral:latest+Q4_0")
  387. if !name.Complete() {
  388. // sanity check
  389. panic("sanity check failed")
  390. }
  391. allocs := testing.AllocsPerRun(1000, func() {
  392. var err error
  393. data, err = name.MarshalText()
  394. if err != nil {
  395. t.Fatal(err)
  396. }
  397. if len(data) == 0 {
  398. t.Fatal("MarshalText() = 0; want non-zero")
  399. }
  400. })
  401. if allocs > 0 {
  402. // TODO: Update when/if this lands:
  403. // https://github.com/golang/go/issues/62384
  404. //
  405. // Currently, the best we can do is 1 alloc.
  406. t.Errorf("MarshalText allocs = %v; want <= 1", allocs)
  407. }
  408. })
  409. }
  410. func TestSQL(t *testing.T) {
  411. t.Run("Scan for already valid Name", func(t *testing.T) {
  412. p := mustParse("x")
  413. if err := p.Scan("mistral:latest+Q4_0"); err == nil {
  414. t.Error("Scan() = nil; want error")
  415. }
  416. })
  417. t.Run("Scan for invalid Name", func(t *testing.T) {
  418. p := Name{}
  419. if err := p.Scan("mistral:latest+Q4_0"); err != nil {
  420. t.Errorf("Scan() = %v; want nil", err)
  421. }
  422. if p.String() != "mistral:latest+Q4_0" {
  423. t.Errorf("String() = %q; want %q", p, "mistral:latest+Q4_0")
  424. }
  425. })
  426. t.Run("Value", func(t *testing.T) {
  427. p := mustParse("x")
  428. if g, err := p.Value(); err != nil {
  429. t.Errorf("Value() error = %v; want nil", err)
  430. } else if g != "x" {
  431. t.Errorf("Value() = %q; want %q", g, "x")
  432. }
  433. })
  434. }
  435. func TestNameStringAllocs(t *testing.T) {
  436. name := ParseName("example.com/ns/mistral:latest+Q4_0")
  437. allocs := testing.AllocsPerRun(1000, func() {
  438. keep(name.String())
  439. })
  440. if allocs > 1 {
  441. t.Errorf("String allocs = %v; want 0", allocs)
  442. }
  443. }
  444. func ExampleFill() {
  445. defaults := ParseName("registry.ollama.com/library/PLACEHOLDER:latest+Q4_0")
  446. r := Fill(ParseName("mistral"), defaults)
  447. fmt.Println(r)
  448. // Output:
  449. // registry.ollama.com/library/mistral:latest+Q4_0
  450. }
  451. func ExampleName_MapHash() {
  452. m := map[uint64]bool{}
  453. // key 1
  454. m[ParseName("mistral:latest+q4").MapHash()] = true
  455. m[ParseName("miSTRal:latest+Q4").MapHash()] = true
  456. m[ParseName("mistral:LATest+Q4").MapHash()] = true
  457. // key 2
  458. m[ParseName("mistral:LATest").MapHash()] = true
  459. fmt.Println(len(m))
  460. // Output:
  461. // 2
  462. }
  463. func ExampleName_CompareFold_sort() {
  464. names := []Name{
  465. ParseName("mistral:latest"),
  466. ParseName("mistRal:7b+q4"),
  467. ParseName("MIstral:7b"),
  468. }
  469. slices.SortFunc(names, Name.CompareFold)
  470. for _, n := range names {
  471. fmt.Println(n)
  472. }
  473. // Output:
  474. // MIstral:7b
  475. // mistRal:7b+q4
  476. // mistral:latest
  477. }
  478. func ExampleName_completeAndResolved() {
  479. for _, s := range []string{
  480. "x/y/z:latest+q4_0@sha123-1",
  481. "x/y/z:latest+q4_0",
  482. "@sha123-1",
  483. } {
  484. p := ParseName(s)
  485. fmt.Printf("complete:%v resolved:%v digest:%s\n", p.Complete(), p.Resolved(), p.Digest())
  486. }
  487. // Output:
  488. // complete:true resolved:true digest:sha123-1
  489. // complete:true resolved:false digest:
  490. // complete:false resolved:true digest:sha123-1
  491. }
  492. func ExampleName_DisplayFullest() {
  493. for _, s := range []string{
  494. "example.com/jmorganca/mistral:latest+Q4_0",
  495. "mistral:latest+Q4_0",
  496. "mistral:latest",
  497. } {
  498. fmt.Println(ParseName(s).DisplayFullest())
  499. }
  500. // Output:
  501. // example.com/jmorganca/mistral:latest
  502. // mistral:latest
  503. // mistral:latest
  504. }
  505. func keep[T any](v T) T { return v }