name_test.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. package model
  2. import (
  3. "reflect"
  4. "strings"
  5. "testing"
  6. )
  7. const (
  8. part80 = "88888888888888888888888888888888888888888888888888888888888888888888888888888888"
  9. part350 = "33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
  10. )
  11. func TestParseNameParts(t *testing.T) {
  12. cases := []struct {
  13. in string
  14. want Name
  15. wantValidDigest bool
  16. }{
  17. {
  18. in: "host/namespace/model:tag",
  19. want: Name{
  20. Host: "host",
  21. Namespace: "namespace",
  22. Model: "model",
  23. Tag: "tag",
  24. },
  25. },
  26. {
  27. in: "host/namespace/model",
  28. want: Name{
  29. Host: "host",
  30. Namespace: "namespace",
  31. Model: "model",
  32. },
  33. },
  34. {
  35. in: "namespace/model",
  36. want: Name{
  37. Namespace: "namespace",
  38. Model: "model",
  39. },
  40. },
  41. {
  42. in: "model",
  43. want: Name{
  44. Model: "model",
  45. },
  46. },
  47. {
  48. in: "h/nn/mm:t",
  49. want: Name{
  50. Host: "h",
  51. Namespace: "nn",
  52. Model: "mm",
  53. Tag: "t",
  54. },
  55. },
  56. {
  57. in: part80 + "/" + part80 + "/" + part80 + ":" + part80,
  58. want: Name{
  59. Host: part80,
  60. Namespace: part80,
  61. Model: part80,
  62. Tag: part80,
  63. },
  64. },
  65. {
  66. in: part350 + "/" + part80 + "/" + part80 + ":" + part80,
  67. want: Name{
  68. Host: part350,
  69. Namespace: part80,
  70. Model: part80,
  71. Tag: part80,
  72. },
  73. },
  74. {
  75. in: "@digest",
  76. want: Name{
  77. RawDigest: "digest",
  78. },
  79. wantValidDigest: false,
  80. },
  81. {
  82. in: "model@sha256:" + validSHA256Hex,
  83. want: Name{
  84. Model: "model",
  85. RawDigest: "sha256:" + validSHA256Hex,
  86. },
  87. wantValidDigest: true,
  88. },
  89. }
  90. for _, tt := range cases {
  91. t.Run(tt.in, func(t *testing.T) {
  92. got := parseName(tt.in)
  93. if !reflect.DeepEqual(got, tt.want) {
  94. t.Errorf("parseName(%q) = %v; want %v", tt.in, got, tt.want)
  95. }
  96. if got.Digest().IsValid() != tt.wantValidDigest {
  97. t.Errorf("parseName(%q).Digest().IsValid() = %v; want %v", tt.in, got.Digest().IsValid(), tt.wantValidDigest)
  98. }
  99. })
  100. }
  101. }
  102. var testCases = map[string]bool{ // name -> valid
  103. "host/namespace/model:tag": true,
  104. "host/namespace/model": false,
  105. "namespace/model": false,
  106. "model": false,
  107. "@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  108. "model@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  109. "model@sha256:1000000000000000000000000000000000000000000000000000000000000000": false,
  110. // long (but valid)
  111. part80 + "/" + part80 + "/" + part80 + ":" + part80: true,
  112. part350 + "/" + part80 + "/" + part80 + ":" + part80: true,
  113. "h/nn/mm:t@sha256-1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  114. "h/nn/mm:t@sha256:1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  115. "m": false, // model too short
  116. "n/mm:": false, // namespace too short
  117. "h/n/mm:t": false, // namespace too short
  118. "@t": false, // digest too short
  119. "mm@d": false, // digest too short
  120. // invalids
  121. "^": false,
  122. "mm:": false,
  123. "/nn/mm": false,
  124. "//": false,
  125. "//mm": false,
  126. "hh//": false,
  127. "//mm:@": false,
  128. "00@": false,
  129. "@": false,
  130. // not starting with alphanum
  131. "-hh/nn/mm:tt@dd": false,
  132. "hh/-nn/mm:tt@dd": false,
  133. "hh/nn/-mm:tt@dd": false,
  134. "hh/nn/mm:-tt@dd": false,
  135. "hh/nn/mm:tt@-dd": false,
  136. "": false,
  137. // hosts
  138. "host:https/namespace/model:tag": true,
  139. // colon in non-host part before tag
  140. "host/name:space/model:tag": false,
  141. }
  142. func TestNameparseNameDefault(t *testing.T) {
  143. const name = "xx"
  144. n := ParseName(name)
  145. got := n.String()
  146. want := "registry.ollama.ai/library/xx:latest"
  147. if got != want {
  148. t.Errorf("parseName(%q).String() = %q; want %q", name, got, want)
  149. }
  150. }
  151. func TestNameIsValid(t *testing.T) {
  152. var numStringTests int
  153. for s, want := range testCases {
  154. n := parseName(s)
  155. t.Logf("n: %#v", n)
  156. got := n.IsValid()
  157. if got != want {
  158. t.Errorf("parseName(%q).IsValid() = %v; want %v", s, got, want)
  159. }
  160. // Test roundtrip with String
  161. if got {
  162. got := parseName(s).String()
  163. if got != s {
  164. t.Errorf("parseName(%q).String() = %q; want %q", s, got, s)
  165. }
  166. numStringTests++
  167. }
  168. }
  169. if numStringTests == 0 {
  170. t.Errorf("no tests for Name.String")
  171. }
  172. }
  173. func TestNameIsValidPart(t *testing.T) {
  174. cases := []struct {
  175. kind partKind
  176. s string
  177. want bool
  178. }{
  179. {kind: kindHost, s: "", want: false},
  180. {kind: kindHost, s: "a", want: true},
  181. {kind: kindHost, s: "a.", want: true},
  182. {kind: kindHost, s: "a.b", want: true},
  183. {kind: kindHost, s: "a:123", want: true},
  184. {kind: kindHost, s: "a:123/aa/bb", want: false},
  185. {kind: kindNamespace, s: "bb", want: true},
  186. {kind: kindNamespace, s: "a.", want: false},
  187. {kind: kindModel, s: "-h", want: false},
  188. {kind: kindDigest, s: "sha256-1000000000000000000000000000000000000000000000000000000000000000", want: true},
  189. }
  190. for _, tt := range cases {
  191. t.Run(tt.s, func(t *testing.T) {
  192. got := isValidPart(tt.kind, tt.s)
  193. if got != tt.want {
  194. t.Errorf("isValidPart(%s, %q) = %v; want %v", tt.kind, tt.s, got, tt.want)
  195. }
  196. })
  197. }
  198. }
  199. func FuzzName(f *testing.F) {
  200. for s := range testCases {
  201. f.Add(s)
  202. }
  203. f.Fuzz(func(t *testing.T, s string) {
  204. n := parseName(s)
  205. if n.IsValid() {
  206. parts := [...]string{n.Host, n.Namespace, n.Model, n.Tag, n.RawDigest}
  207. for _, part := range parts {
  208. if part == ".." {
  209. t.Errorf("unexpected .. as valid part")
  210. }
  211. if len(part) > 350 {
  212. t.Errorf("part too long: %q", part)
  213. }
  214. }
  215. if n.String() != s {
  216. t.Errorf("String() = %q; want %q", n.String(), s)
  217. }
  218. }
  219. })
  220. }
  221. const validSHA256Hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
  222. func TestParseDigest(t *testing.T) {
  223. cases := map[string]bool{
  224. "sha256-1000000000000000000000000000000000000000000000000000000000000000": true,
  225. "sha256:1000000000000000000000000000000000000000000000000000000000000000": true,
  226. "sha256:0000000000000000000000000000000000000000000000000000000000000000": false,
  227. "sha256:" + validSHA256Hex: true,
  228. "sha256-" + validSHA256Hex: true,
  229. "": false,
  230. "sha134:" + validSHA256Hex: false,
  231. "sha256:" + validSHA256Hex + "x": false,
  232. "sha256:x" + validSHA256Hex: false,
  233. "sha256-" + validSHA256Hex + "x": false,
  234. "sha256-x": false,
  235. }
  236. for s, want := range cases {
  237. t.Run(s, func(t *testing.T) {
  238. d := ParseDigest(s)
  239. if d.IsValid() != want {
  240. t.Errorf("ParseDigest(%q).IsValid() = %v; want %v", s, d.IsValid(), want)
  241. }
  242. norm := strings.ReplaceAll(s, ":", "-")
  243. if d.IsValid() && d.String() != norm {
  244. t.Errorf("ParseDigest(%q).String() = %q; want %q", s, d.String(), norm)
  245. }
  246. })
  247. }
  248. }
  249. func TestDigestString(t *testing.T) {
  250. cases := []struct {
  251. in string
  252. want string
  253. }{
  254. {in: "sha256:" + validSHA256Hex, want: "sha256-" + validSHA256Hex},
  255. {in: "sha256-" + validSHA256Hex, want: "sha256-" + validSHA256Hex},
  256. {in: "", want: "unknown-0000000000000000000000000000000000000000000000000000000000000000"},
  257. {in: "blah-100000000000000000000000000000000000000000000000000000000000000", want: "unknown-0000000000000000000000000000000000000000000000000000000000000000"},
  258. }
  259. for _, tt := range cases {
  260. t.Run(tt.in, func(t *testing.T) {
  261. d := ParseDigest(tt.in)
  262. if d.String() != tt.want {
  263. t.Errorf("ParseDigest(%q).String() = %q; want %q", tt.in, d.String(), tt.want)
  264. }
  265. })
  266. }
  267. }