ref_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package blob
  2. import (
  3. "strings"
  4. "testing"
  5. )
  6. var testRefs = map[string]Ref{
  7. "mistral:latest": {name: "mistral", tag: "latest"},
  8. "mistral": {name: "mistral"},
  9. "mistral:30B": {name: "mistral", tag: "30B"},
  10. "mistral:7b": {name: "mistral", tag: "7b"},
  11. "mistral:7b+Q4_0": {name: "mistral", tag: "7b", build: "Q4_0"},
  12. "mistral+KQED": {name: "mistral", build: "KQED"},
  13. "mistral.x-3:7b+Q4_0": {name: "mistral.x-3", tag: "7b", build: "Q4_0"},
  14. "mistral:7b+q4_0": {name: "mistral", tag: "7b", build: "Q4_0"},
  15. "llama2": {name: "llama2"},
  16. // invalid (includes fuzzing trophies)
  17. "+": {},
  18. "mistral:7b+Q4_0:latest": {},
  19. "mi tral": {},
  20. "x/y/z/foo": {},
  21. "/0": {},
  22. "0 /0": {},
  23. "0 /": {},
  24. "0/": {},
  25. ":": {},
  26. ":/0": {},
  27. "+0/00000": {},
  28. "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": {},
  29. "0//0": {},
  30. "m+^^^": {},
  31. "file:///etc/passwd": {},
  32. "file:///etc/passwd:latest": {},
  33. "file:///etc/passwd:latest+u": {},
  34. strings.Repeat("a", MaxRefLength): {name: strings.Repeat("a", MaxRefLength)},
  35. strings.Repeat("a", MaxRefLength+1): {},
  36. }
  37. func TestRefParts(t *testing.T) {
  38. const wantNumParts = 5
  39. var ref Ref
  40. if len(ref.Parts()) != wantNumParts {
  41. t.Errorf("Parts() = %d; want %d", len(ref.Parts()), wantNumParts)
  42. }
  43. }
  44. func TestParseRef(t *testing.T) {
  45. for s, want := range testRefs {
  46. for _, prefix := range []string{"", "https://", "http://"} {
  47. // We should get the same results with or without the
  48. // http(s) prefixes
  49. s := prefix + s
  50. t.Run(s, func(t *testing.T) {
  51. got := ParseRef(s)
  52. if got != want {
  53. t.Errorf("ParseRef(%q) = %q; want %q", s, got, want)
  54. }
  55. // test round-trip
  56. if ParseRef(got.String()) != got {
  57. t.Errorf("String() = %s; want %s", got.String(), s)
  58. }
  59. if got.Valid() && got.Name() == "" {
  60. t.Errorf("Valid() = true; Name() = %q; want non-empty name", got.Name())
  61. } else if !got.Valid() && got.Name() != "" {
  62. t.Errorf("Valid() = false; Name() = %q; want empty name", got.Name())
  63. }
  64. })
  65. }
  66. }
  67. }
  68. func TestRefComplete(t *testing.T) {
  69. cases := []struct {
  70. in string
  71. complete bool
  72. completeWithoutBuild bool
  73. }{
  74. {"", false, false},
  75. {"example.com/mistral:7b+x", false, false},
  76. {"example.com/mistral:7b+Q4_0", false, false},
  77. {"mistral:7b+x", false, false},
  78. {"example.com/x/mistral:latest+Q4_0", true, true},
  79. {"example.com/x/mistral:latest", false, true},
  80. }
  81. for _, tt := range cases {
  82. t.Run(tt.in, func(t *testing.T) {
  83. ref := ParseRef(tt.in)
  84. t.Logf("ParseRef(%q) = %#v", tt.in, ref)
  85. if g := ref.Complete(); g != tt.complete {
  86. t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete)
  87. }
  88. if g := ref.CompleteWithoutBuild(); g != tt.completeWithoutBuild {
  89. t.Errorf("CompleteWithoutBuild(%q) = %v; want %v", tt.in, g, tt.completeWithoutBuild)
  90. }
  91. })
  92. }
  93. }
  94. func TestRefStringVariants(t *testing.T) {
  95. cases := []struct {
  96. in string
  97. nameAndTag string
  98. nameTagAndBuild string
  99. }{
  100. {"x/y/z:8n+I", "z:8n", "z:8n+I"},
  101. {"x/y/z:8n", "z:8n", "z:8n"},
  102. }
  103. for _, tt := range cases {
  104. t.Run(tt.in, func(t *testing.T) {
  105. ref := ParseRef(tt.in)
  106. t.Logf("ParseRef(%q) = %#v", tt.in, ref)
  107. if g := ref.NameAndTag(); g != tt.nameAndTag {
  108. t.Errorf("NameAndTag(%q) = %q; want %q", tt.in, g, tt.nameAndTag)
  109. }
  110. if g := ref.NameTagAndBuild(); g != tt.nameTagAndBuild {
  111. t.Errorf("NameTagAndBuild(%q) = %q; want %q", tt.in, g, tt.nameTagAndBuild)
  112. }
  113. })
  114. }
  115. }
  116. func TestRefFull(t *testing.T) {
  117. const empty = "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/!(MISSING NAME):!(MISSING TAG)+!(MISSING BUILD)"
  118. cases := []struct {
  119. in string
  120. wantFull string
  121. }{
  122. {"", empty},
  123. {"example.com/mistral:7b+x", "!(MISSING DOMAIN)/example.com/mistral:7b+X"},
  124. {"example.com/mistral:7b+Q4_0", "!(MISSING DOMAIN)/example.com/mistral:7b+Q4_0"},
  125. {"example.com/x/mistral:latest", "example.com/x/mistral:latest+!(MISSING BUILD)"},
  126. {"example.com/x/mistral:latest+Q4_0", "example.com/x/mistral:latest+Q4_0"},
  127. {"mistral:7b+x", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:7b+X"},
  128. {"mistral:7b+q4_0", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:7b+Q4_0"},
  129. {"mistral:7b+Q4_0", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:7b+Q4_0"},
  130. {"mistral:latest", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:latest+!(MISSING BUILD)"},
  131. {"mistral", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:!(MISSING TAG)+!(MISSING BUILD)"},
  132. {"mistral:30b", "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/mistral:30b+!(MISSING BUILD)"},
  133. }
  134. for _, tt := range cases {
  135. t.Run(tt.in, func(t *testing.T) {
  136. ref := ParseRef(tt.in)
  137. t.Logf("ParseRef(%q) = %#v", tt.in, ref)
  138. if g := ref.Full(); g != tt.wantFull {
  139. t.Errorf("Full(%q) = %q; want %q", tt.in, g, tt.wantFull)
  140. }
  141. })
  142. }
  143. }
  144. func TestParseRefAllocs(t *testing.T) {
  145. // test allocations
  146. var r Ref
  147. allocs := testing.AllocsPerRun(1000, func() {
  148. r = ParseRef("example.com/mistral:7b+Q4_0")
  149. })
  150. _ = r
  151. if allocs > 0 {
  152. t.Errorf("ParseRef allocs = %v; want 0", allocs)
  153. }
  154. }
  155. func BenchmarkParseRef(b *testing.B) {
  156. b.ReportAllocs()
  157. var r Ref
  158. for i := 0; i < b.N; i++ {
  159. r = ParseRef("example.com/mistral:7b+Q4_0")
  160. }
  161. _ = r
  162. }
  163. func FuzzParseRef(f *testing.F) {
  164. f.Add("example.com/mistral:7b+Q4_0")
  165. f.Add("example.com/mistral:7b+q4_0")
  166. f.Add("example.com/mistral:7b+x")
  167. f.Add("x/y/z:8n+I")
  168. f.Fuzz(func(t *testing.T, s string) {
  169. r0 := ParseRef(s)
  170. if !r0.Valid() {
  171. if r0 != (Ref{}) {
  172. t.Errorf("expected invalid ref to be zero value; got %#v", r0)
  173. }
  174. t.Skipf("invalid ref: %q", s)
  175. }
  176. for _, p := range r0.Parts() {
  177. if len(p) > MaxRefLength {
  178. t.Errorf("part too long: %q", p)
  179. }
  180. }
  181. if !strings.EqualFold(r0.String(), s) {
  182. t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.String(), s)
  183. }
  184. r1 := ParseRef(r0.String())
  185. if r0 != r1 {
  186. t.Errorf("round-trip mismatch: %+v != %+v", r0, r1)
  187. }
  188. })
  189. }