name_test.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. for _, tt := range cases {
  129. t.Run(tt.in, func(t *testing.T) {
  130. got := ParseNameBare(tt.in)
  131. if !reflect.DeepEqual(got, tt.want) {
  132. t.Errorf("parseName(%q) = %v; want %v", tt.in, got, tt.want)
  133. }
  134. got = ParseName(tt.in)
  135. if tt.wantFilepath != "" && got.Filepath() != tt.wantFilepath {
  136. t.Errorf("parseName(%q).Filepath() = %q; want %q", tt.in, got.Filepath(), tt.wantFilepath)
  137. }
  138. })
  139. }
  140. }
  141. var testCases = map[string]bool{ // name -> valid
  142. "": false,
  143. "_why/_the/_lucky:_stiff": true,
  144. // minimal
  145. "h/n/m:t@d": true,
  146. "host/namespace/model:tag": true,
  147. "host/namespace/model": false,
  148. "namespace/model": false,
  149. "model": false,
  150. "@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  151. "model@sha256-1000000000000000000000000000000000000000000000000000000000000000": false,
  152. "model@sha256:1000000000000000000000000000000000000000000000000000000000000000": false,
  153. // long (but valid)
  154. part80 + "/" + part80 + "/" + part80 + ":" + part80: true,
  155. part350 + "/" + part80 + "/" + part80 + ":" + part80: true,
  156. "h/nn/mm:t@sha256-1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  157. "h/nn/mm:t@sha256:1000000000000000000000000000000000000000000000000000000000000000": true, // bare minimum part sizes
  158. // unqualified
  159. "m": false,
  160. "n/m:": false,
  161. "h/n/m": false,
  162. "@t": false,
  163. "m@d": false,
  164. // invalids
  165. "^": false,
  166. "mm:": false,
  167. "/nn/mm": false,
  168. "//": false,
  169. "//mm": false,
  170. "hh//": false,
  171. "//mm:@": false,
  172. "00@": false,
  173. "@": false,
  174. // not starting with alphanum
  175. "-hh/nn/mm:tt@dd": false,
  176. "hh/-nn/mm:tt@dd": false,
  177. "hh/nn/-mm:tt@dd": false,
  178. "hh/nn/mm:-tt@dd": false,
  179. "hh/nn/mm:tt@-dd": false,
  180. // hosts
  181. "host:https/namespace/model:tag": true,
  182. // colon in non-host part before tag
  183. "host/name:space/model:tag": false,
  184. }
  185. func TestNameparseNameDefault(t *testing.T) {
  186. const name = "xx"
  187. n := ParseName(name)
  188. got := n.String()
  189. want := "registry.ollama.ai/library/xx:latest"
  190. if got != want {
  191. t.Errorf("parseName(%q).String() = %q; want %q", name, got, want)
  192. }
  193. }
  194. func TestNameIsValid(t *testing.T) {
  195. var numStringTests int
  196. for s, want := range testCases {
  197. n := ParseNameBare(s)
  198. got := n.IsValid()
  199. if got != want {
  200. t.Errorf("parseName(%q).IsValid() = %v; want %v", s, got, want)
  201. }
  202. // Test roundtrip with String
  203. if got {
  204. got := ParseNameBare(s).String()
  205. if got != s {
  206. t.Errorf("parseName(%q).String() = %q; want %q", s, got, s)
  207. }
  208. numStringTests++
  209. }
  210. }
  211. if numStringTests == 0 {
  212. t.Errorf("no tests for Name.String")
  213. }
  214. }
  215. func TestNameIsValidPart(t *testing.T) {
  216. cases := []struct {
  217. kind partKind
  218. s string
  219. want bool
  220. }{
  221. {kind: kindHost, s: "", want: false},
  222. {kind: kindHost, s: "a", want: true},
  223. {kind: kindHost, s: "a.", want: true},
  224. {kind: kindHost, s: "a.b", want: true},
  225. {kind: kindHost, s: "a:123", want: true},
  226. {kind: kindHost, s: "a:123/aa/bb", want: false},
  227. {kind: kindNamespace, s: "bb", want: true},
  228. {kind: kindNamespace, s: "a.", want: false},
  229. {kind: kindModel, s: "-h", want: false},
  230. {kind: kindDigest, s: "sha256-1000000000000000000000000000000000000000000000000000000000000000", want: true},
  231. }
  232. for _, tt := range cases {
  233. t.Run(tt.s, func(t *testing.T) {
  234. got := isValidPart(tt.kind, tt.s)
  235. if got != tt.want {
  236. t.Errorf("isValidPart(%s, %q) = %v; want %v", tt.kind, tt.s, got, tt.want)
  237. }
  238. })
  239. }
  240. }
  241. func TestFilepathAllocs(t *testing.T) {
  242. n := ParseNameBare("HOST/NAMESPACE/MODEL:TAG")
  243. allocs := testing.AllocsPerRun(1000, func() {
  244. n.Filepath()
  245. })
  246. allowedAllocs := 2.0
  247. if runtime.GOOS == "windows" {
  248. allowedAllocs = 4
  249. }
  250. if allocs > allowedAllocs {
  251. t.Errorf("allocs = %v; allowed %v", allocs, allowedAllocs)
  252. }
  253. }
  254. const (
  255. validSha256 = "sha256-1000000000000000000000000000000000000000000000000000000000000000"
  256. validSha256Old = "sha256:1000000000000000000000000000000000000000000000000000000000000000"
  257. )
  258. func TestParseDigest(t *testing.T) {
  259. cases := []struct {
  260. in string
  261. want string
  262. }{
  263. {"", ""}, // empty
  264. {"sha123-12", ""}, // invalid type
  265. {"sha256-", ""}, // invalid sum
  266. {"sha256-123", ""}, // invalid odd length sum
  267. {validSha256, validSha256},
  268. {validSha256Old, validSha256},
  269. }
  270. for _, tt := range cases {
  271. t.Run(tt.in, func(t *testing.T) {
  272. got, err := ParseDigest(tt.in)
  273. if err != nil {
  274. if tt.want != "" {
  275. t.Errorf("parseDigest(%q) = %v; want %v", tt.in, err, tt.want)
  276. }
  277. return
  278. }
  279. if got.String() != tt.want {
  280. t.Errorf("parseDigest(%q).String() = %q; want %q", tt.in, got, tt.want)
  281. }
  282. })
  283. }
  284. }
  285. func TestParseNameFromFilepath(t *testing.T) {
  286. cases := map[string]Name{
  287. filepath.Join("host", "namespace", "model", "tag"): {Host: "host", Namespace: "namespace", Model: "model", Tag: "tag"},
  288. filepath.Join("host:port", "namespace", "model", "tag"): {Host: "host:port", Namespace: "namespace", Model: "model", Tag: "tag"},
  289. filepath.Join("namespace", "model", "tag"): {},
  290. filepath.Join("model", "tag"): {},
  291. filepath.Join("model"): {},
  292. filepath.Join("..", "..", "model", "tag"): {},
  293. filepath.Join("", "namespace", ".", "tag"): {},
  294. filepath.Join(".", ".", ".", "."): {},
  295. filepath.Join("/", "path", "to", "random", "file"): {},
  296. }
  297. for in, want := range cases {
  298. t.Run(in, func(t *testing.T) {
  299. got := ParseNameFromFilepath(in)
  300. if !reflect.DeepEqual(got, want) {
  301. t.Errorf("parseNameFromFilepath(%q) = %v; want %v", in, got, want)
  302. }
  303. })
  304. }
  305. }
  306. func TestDisplayShortest(t *testing.T) {
  307. cases := map[string]string{
  308. "registry.ollama.ai/library/model:latest": "model:latest",
  309. "registry.ollama.ai/library/model:tag": "model:tag",
  310. "registry.ollama.ai/namespace/model:tag": "namespace/model:tag",
  311. "host/namespace/model:tag": "host/namespace/model:tag",
  312. "host/library/model:tag": "host/library/model:tag",
  313. }
  314. for in, want := range cases {
  315. t.Run(in, func(t *testing.T) {
  316. got := ParseNameBare(in).DisplayShortest()
  317. if got != want {
  318. t.Errorf("parseName(%q).DisplayShortest() = %q; want %q", in, got, want)
  319. }
  320. })
  321. }
  322. }
  323. func FuzzName(f *testing.F) {
  324. for s := range testCases {
  325. f.Add(s)
  326. }
  327. f.Fuzz(func(t *testing.T, s string) {
  328. n := ParseNameBare(s)
  329. if n.IsValid() {
  330. parts := [...]string{n.Host, n.Namespace, n.Model, n.Tag, n.RawDigest}
  331. for _, part := range parts {
  332. if part == ".." {
  333. t.Errorf("unexpected .. as valid part")
  334. }
  335. if len(part) > 350 {
  336. t.Errorf("part too long: %q", part)
  337. }
  338. }
  339. if n.String() != s {
  340. t.Errorf("String() = %q; want %q", n.String(), s)
  341. }
  342. }
  343. })
  344. }