name_test.go 9.1 KB

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