name_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. package model
  2. import (
  3. "path/filepath"
  4. "reflect"
  5. "runtime"
  6. "testing"
  7. )
  8. const (
  9. part80 = "88888888888888888888888888888888888888888888888888888888888888888888888888888888"
  10. part350 = "33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
  11. )
  12. func TestParseNameParts(t *testing.T) {
  13. cases := []struct {
  14. in string
  15. want Name
  16. wantFilepath string
  17. wantValidDigest bool
  18. }{
  19. {
  20. in: "scheme://host:port/namespace/model:tag",
  21. want: Name{
  22. Host: "host:port",
  23. Namespace: "namespace",
  24. Model: "model",
  25. Tag: "tag",
  26. },
  27. wantFilepath: filepath.Join("host%port", "namespace", "model", "tag"),
  28. },
  29. {
  30. in: "host/namespace/model:tag",
  31. want: Name{
  32. Host: "host",
  33. Namespace: "namespace",
  34. Model: "model",
  35. Tag: "tag",
  36. },
  37. wantFilepath: filepath.Join("host", "namespace", "model", "tag"),
  38. },
  39. {
  40. in: "host:port/namespace/model:tag",
  41. want: Name{
  42. Host: "host:port",
  43. Namespace: "namespace",
  44. Model: "model",
  45. Tag: "tag",
  46. },
  47. wantFilepath: filepath.Join("host%port", "namespace", "model", "tag"),
  48. },
  49. {
  50. in: "host/namespace/model",
  51. want: Name{
  52. Host: "host",
  53. Namespace: "namespace",
  54. Model: "model",
  55. },
  56. wantFilepath: filepath.Join("host", "namespace", "model", "latest"),
  57. },
  58. {
  59. in: "host:port/namespace/model",
  60. want: Name{
  61. Host: "host:port",
  62. Namespace: "namespace",
  63. Model: "model",
  64. },
  65. wantFilepath: filepath.Join("host%port", "namespace", "model", "latest"),
  66. },
  67. {
  68. in: "namespace/model",
  69. want: Name{
  70. Namespace: "namespace",
  71. Model: "model",
  72. },
  73. wantFilepath: filepath.Join("registry.ollama.ai", "namespace", "model", "latest"),
  74. },
  75. {
  76. in: "model",
  77. want: Name{
  78. Model: "model",
  79. },
  80. wantFilepath: filepath.Join("registry.ollama.ai", "library", "model", "latest"),
  81. },
  82. {
  83. in: "h/nn/mm:t",
  84. want: Name{
  85. Host: "h",
  86. Namespace: "nn",
  87. Model: "mm",
  88. Tag: "t",
  89. },
  90. wantFilepath: filepath.Join("h", "nn", "mm", "t"),
  91. },
  92. {
  93. in: part80 + "/" + part80 + "/" + part80 + ":" + part80,
  94. want: Name{
  95. Host: part80,
  96. Namespace: part80,
  97. Model: part80,
  98. Tag: part80,
  99. },
  100. wantFilepath: filepath.Join(part80, part80, part80, part80),
  101. },
  102. {
  103. in: part350 + "/" + part80 + "/" + part80 + ":" + part80,
  104. want: Name{
  105. Host: part350,
  106. Namespace: part80,
  107. Model: part80,
  108. Tag: part80,
  109. },
  110. wantFilepath: filepath.Join(part350, part80, part80, part80),
  111. },
  112. {
  113. in: "@digest",
  114. want: Name{
  115. RawDigest: "digest",
  116. },
  117. wantValidDigest: false,
  118. },
  119. {
  120. in: "model@sha256:123",
  121. want: Name{
  122. Model: "model",
  123. RawDigest: "sha256:123",
  124. },
  125. wantValidDigest: true,
  126. },
  127. {
  128. in: "y.com:443/n/model",
  129. want: Name{
  130. Host: "y.com:443",
  131. Namespace: "n",
  132. Model: "model",
  133. },
  134. wantFilepath: filepath.Join("y.com%443", "n", "model", "latest"),
  135. },
  136. }
  137. for _, tt := range cases {
  138. t.Run(tt.in, func(t *testing.T) {
  139. got := ParseNameBare(tt.in)
  140. if !reflect.DeepEqual(got, tt.want) {
  141. t.Errorf("parseName(%q) = %v; want %v", tt.in, got, tt.want)
  142. }
  143. got = ParseName(tt.in)
  144. if tt.wantFilepath != "" && got.Filepath() != tt.wantFilepath {
  145. t.Errorf("parseName(%q).Filepath() = %q; want %q", tt.in, got.Filepath(), tt.wantFilepath)
  146. }
  147. })
  148. }
  149. }
  150. var testCases = map[string]bool{ // name -> valid
  151. "": false,
  152. "_why/_the/_lucky:_stiff": true,
  153. // minimal
  154. "h/n/m:t@d": true,
  155. "host/namespace/model:tag": true,
  156. "host/namespace/model": false,
  157. "namespace/model": false,
  158. "model": false,
  159. "@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  160. "model@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  161. "model@sha256:1000000000000000000000000000000000000000000000000000000000000000": false,
  162. // long (but valid)
  163. part80 + "/" + part80 + "/" + part80 + ":" + part80: true,
  164. part350 + "/" + part80 + "/" + part80 + ":" + part80: true,
  165. "h/nn/mm:t@sha256-1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  166. "h/nn/mm:t@sha256:1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  167. // unqualified
  168. "m": false,
  169. "n/m:": false,
  170. "h/n/m": false,
  171. "@t": false,
  172. "m@d": false,
  173. // invalids
  174. "^": false,
  175. "mm:": false,
  176. "/nn/mm": false,
  177. "//": false,
  178. "//mm": false,
  179. "hh//": false,
  180. "//mm:@": false,
  181. "00@": false,
  182. "@": false,
  183. // not starting with alphanum
  184. "-hh/nn/mm:tt@dd": false,
  185. "hh/-nn/mm:tt@dd": false,
  186. "hh/nn/-mm:tt@dd": false,
  187. "hh/nn/mm:-tt@dd": false,
  188. "hh/nn/mm:tt@-dd": false,
  189. // hosts
  190. "host:https/namespace/model:tag": true,
  191. // colon in non-host part before tag
  192. "host/name:space/model:tag": false,
  193. }
  194. func TestNameparseNameDefault(t *testing.T) {
  195. const name = "xx"
  196. n := ParseName(name)
  197. got := n.String()
  198. want := "registry.ollama.ai/library/xx:latest"
  199. if got != want {
  200. t.Errorf("parseName(%q).String() = %q; want %q", name, got, want)
  201. }
  202. }
  203. func TestNameIsValid(t *testing.T) {
  204. var numStringTests int
  205. for s, want := range testCases {
  206. n := ParseNameBare(s)
  207. got := n.IsValid()
  208. if got != want {
  209. t.Errorf("parseName(%q).IsValid() = %v; want %v", s, got, want)
  210. }
  211. // Test roundtrip with String
  212. if got {
  213. got := ParseNameBare(s).String()
  214. if got != s {
  215. t.Errorf("parseName(%q).String() = %q; want %q", s, got, s)
  216. }
  217. numStringTests++
  218. }
  219. }
  220. if numStringTests == 0 {
  221. t.Errorf("no tests for Name.String")
  222. }
  223. }
  224. func TestNameIsValidPart(t *testing.T) {
  225. cases := []struct {
  226. kind partKind
  227. s string
  228. want bool
  229. }{
  230. {kind: kindHost, s: "", want: false},
  231. {kind: kindHost, s: "a", want: true},
  232. {kind: kindHost, s: "a.", want: true},
  233. {kind: kindHost, s: "a.b", want: true},
  234. {kind: kindHost, s: "a:123", want: true},
  235. {kind: kindHost, s: "a:123/aa/bb", want: false},
  236. {kind: kindNamespace, s: "bb", want: true},
  237. {kind: kindNamespace, s: "a.", want: false},
  238. {kind: kindModel, s: "-h", want: false},
  239. {kind: kindDigest, s: "sha256-1000000000000000000000000000000000000000000000000000000000000000", want: true},
  240. }
  241. for _, tt := range cases {
  242. t.Run(tt.s, func(t *testing.T) {
  243. got := isValidPart(tt.kind, tt.s)
  244. if got != tt.want {
  245. t.Errorf("isValidPart(%s, %q) = %v; want %v", tt.kind, tt.s, got, tt.want)
  246. }
  247. })
  248. }
  249. }
  250. func TestFilepathAllocs(t *testing.T) {
  251. n := ParseNameBare("HOST/NAMESPACE/MODEL:TAG")
  252. allocs := testing.AllocsPerRun(1000, func() {
  253. n.Filepath()
  254. })
  255. allowedAllocs := 2.0
  256. if runtime.GOOS == "windows" {
  257. allowedAllocs = 4
  258. }
  259. if allocs > allowedAllocs {
  260. t.Errorf("allocs = %v; allowed %v", allocs, allowedAllocs)
  261. }
  262. }
  263. const (
  264. validSha256 = "sha256-1000000000000000000000000000000000000000000000000000000000000000"
  265. validSha256Old = "sha256:1000000000000000000000000000000000000000000000000000000000000000"
  266. )
  267. func TestParseDigest(t *testing.T) {
  268. cases := []struct {
  269. in string
  270. want string
  271. }{
  272. {"", ""}, // empty
  273. {"sha123-12", ""}, // invalid type
  274. {"sha256-", ""}, // invalid sum
  275. {"sha256-123", ""}, // invalid odd length sum
  276. {validSha256, validSha256},
  277. {validSha256Old, validSha256},
  278. }
  279. for _, tt := range cases {
  280. t.Run(tt.in, func(t *testing.T) {
  281. got, err := ParseDigest(tt.in)
  282. if err != nil {
  283. if tt.want != "" {
  284. t.Errorf("parseDigest(%q) = %v; want %v", tt.in, err, tt.want)
  285. }
  286. return
  287. }
  288. if got.String() != tt.want {
  289. t.Errorf("parseDigest(%q).String() = %q; want %q", tt.in, got, tt.want)
  290. }
  291. })
  292. }
  293. }
  294. func FuzzName(f *testing.F) {
  295. for s := range testCases {
  296. f.Add(s)
  297. }
  298. f.Fuzz(func(t *testing.T, s string) {
  299. n := ParseNameBare(s)
  300. if n.IsValid() {
  301. parts := [...]string{n.Host, n.Namespace, n.Model, n.Tag, n.RawDigest}
  302. for _, part := range parts {
  303. if part == ".." {
  304. t.Errorf("unexpected .. as valid part")
  305. }
  306. if len(part) > 350 {
  307. t.Errorf("part too long: %q", part)
  308. }
  309. }
  310. if n.String() != s {
  311. t.Errorf("String() = %q; want %q", n.String(), s)
  312. }
  313. }
  314. })
  315. }