cmd_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. package cmd
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "os"
  10. "strings"
  11. "testing"
  12. "time"
  13. "github.com/google/go-cmp/cmp"
  14. "github.com/spf13/cobra"
  15. "github.com/ollama/ollama/api"
  16. )
  17. func TestShowInfo(t *testing.T) {
  18. t.Run("bare details", func(t *testing.T) {
  19. var b bytes.Buffer
  20. if err := showInfo(&api.ShowResponse{
  21. Details: api.ModelDetails{
  22. Family: "test",
  23. ParameterSize: "7B",
  24. QuantizationLevel: "FP16",
  25. },
  26. }, &b); err != nil {
  27. t.Fatal(err)
  28. }
  29. expect := ` Model
  30. architecture test
  31. parameters 7B
  32. quantization FP16
  33. `
  34. if diff := cmp.Diff(expect, b.String()); diff != "" {
  35. t.Errorf("unexpected output (-want +got):\n%s", diff)
  36. }
  37. })
  38. t.Run("bare model info", func(t *testing.T) {
  39. var b bytes.Buffer
  40. if err := showInfo(&api.ShowResponse{
  41. ModelInfo: map[string]any{
  42. "general.architecture": "test",
  43. "general.parameter_count": float64(7_000_000_000),
  44. "test.context_length": float64(0),
  45. "test.embedding_length": float64(0),
  46. },
  47. Details: api.ModelDetails{
  48. Family: "test",
  49. ParameterSize: "7B",
  50. QuantizationLevel: "FP16",
  51. },
  52. }, &b); err != nil {
  53. t.Fatal(err)
  54. }
  55. expect := ` Model
  56. architecture test
  57. parameters 7B
  58. context length 0
  59. embedding length 0
  60. quantization FP16
  61. `
  62. if diff := cmp.Diff(expect, b.String()); diff != "" {
  63. t.Errorf("unexpected output (-want +got):\n%s", diff)
  64. }
  65. })
  66. t.Run("parameters", func(t *testing.T) {
  67. var b bytes.Buffer
  68. if err := showInfo(&api.ShowResponse{
  69. Details: api.ModelDetails{
  70. Family: "test",
  71. ParameterSize: "7B",
  72. QuantizationLevel: "FP16",
  73. },
  74. Parameters: `
  75. stop never
  76. stop gonna
  77. stop give
  78. stop you
  79. stop up
  80. temperature 99`,
  81. }, &b); err != nil {
  82. t.Fatal(err)
  83. }
  84. expect := ` Model
  85. architecture test
  86. parameters 7B
  87. quantization FP16
  88. Parameters
  89. stop never
  90. stop gonna
  91. stop give
  92. stop you
  93. stop up
  94. temperature 99
  95. `
  96. if diff := cmp.Diff(expect, b.String()); diff != "" {
  97. t.Errorf("unexpected output (-want +got):\n%s", diff)
  98. }
  99. })
  100. t.Run("project info", func(t *testing.T) {
  101. var b bytes.Buffer
  102. if err := showInfo(&api.ShowResponse{
  103. Details: api.ModelDetails{
  104. Family: "test",
  105. ParameterSize: "7B",
  106. QuantizationLevel: "FP16",
  107. },
  108. ProjectorInfo: map[string]any{
  109. "general.architecture": "clip",
  110. "general.parameter_count": float64(133_700_000),
  111. "clip.vision.embedding_length": float64(0),
  112. "clip.vision.projection_dim": float64(0),
  113. },
  114. }, &b); err != nil {
  115. t.Fatal(err)
  116. }
  117. expect := ` Model
  118. architecture test
  119. parameters 7B
  120. quantization FP16
  121. Projector
  122. architecture clip
  123. parameters 133.70M
  124. embedding length 0
  125. dimensions 0
  126. `
  127. if diff := cmp.Diff(expect, b.String()); diff != "" {
  128. t.Errorf("unexpected output (-want +got):\n%s", diff)
  129. }
  130. })
  131. t.Run("system", func(t *testing.T) {
  132. var b bytes.Buffer
  133. if err := showInfo(&api.ShowResponse{
  134. Details: api.ModelDetails{
  135. Family: "test",
  136. ParameterSize: "7B",
  137. QuantizationLevel: "FP16",
  138. },
  139. System: `You are a pirate!
  140. Ahoy, matey!
  141. Weigh anchor!
  142. `,
  143. }, &b); err != nil {
  144. t.Fatal(err)
  145. }
  146. expect := ` Model
  147. architecture test
  148. parameters 7B
  149. quantization FP16
  150. System
  151. You are a pirate!
  152. Ahoy, matey!
  153. `
  154. if diff := cmp.Diff(expect, b.String()); diff != "" {
  155. t.Errorf("unexpected output (-want +got):\n%s", diff)
  156. }
  157. })
  158. t.Run("license", func(t *testing.T) {
  159. var b bytes.Buffer
  160. license := "MIT License\nCopyright (c) Ollama\n"
  161. if err := showInfo(&api.ShowResponse{
  162. Details: api.ModelDetails{
  163. Family: "test",
  164. ParameterSize: "7B",
  165. QuantizationLevel: "FP16",
  166. },
  167. License: license,
  168. }, &b); err != nil {
  169. t.Fatal(err)
  170. }
  171. expect := ` Model
  172. architecture test
  173. parameters 7B
  174. quantization FP16
  175. License
  176. MIT License
  177. Copyright (c) Ollama
  178. `
  179. if diff := cmp.Diff(expect, b.String()); diff != "" {
  180. t.Errorf("unexpected output (-want +got):\n%s", diff)
  181. }
  182. })
  183. }
  184. func TestDeleteHandler(t *testing.T) {
  185. stopped := false
  186. mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  187. if r.URL.Path == "/api/delete" && r.Method == http.MethodDelete {
  188. var req api.DeleteRequest
  189. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  190. http.Error(w, err.Error(), http.StatusBadRequest)
  191. return
  192. }
  193. if req.Name == "test-model" {
  194. w.WriteHeader(http.StatusOK)
  195. } else {
  196. w.WriteHeader(http.StatusNotFound)
  197. }
  198. return
  199. }
  200. if r.URL.Path == "/api/generate" && r.Method == http.MethodPost {
  201. var req api.GenerateRequest
  202. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  203. http.Error(w, err.Error(), http.StatusBadRequest)
  204. return
  205. }
  206. if req.Model == "test-model" {
  207. w.WriteHeader(http.StatusOK)
  208. if err := json.NewEncoder(w).Encode(api.GenerateResponse{
  209. Done: true,
  210. }); err != nil {
  211. http.Error(w, err.Error(), http.StatusInternalServerError)
  212. }
  213. stopped = true
  214. return
  215. } else {
  216. w.WriteHeader(http.StatusNotFound)
  217. if err := json.NewEncoder(w).Encode(api.GenerateResponse{
  218. Done: false,
  219. }); err != nil {
  220. http.Error(w, err.Error(), http.StatusInternalServerError)
  221. }
  222. }
  223. }
  224. }))
  225. t.Setenv("OLLAMA_HOST", mockServer.URL)
  226. t.Cleanup(mockServer.Close)
  227. cmd := &cobra.Command{}
  228. cmd.SetContext(context.TODO())
  229. if err := DeleteHandler(cmd, []string{"test-model"}); err != nil {
  230. t.Fatalf("DeleteHandler failed: %v", err)
  231. }
  232. if !stopped {
  233. t.Fatal("Model was not stopped before deletion")
  234. }
  235. err := DeleteHandler(cmd, []string{"test-model-not-found"})
  236. if err == nil || !strings.Contains(err.Error(), "unable to stop existing running model \"test-model-not-found\"") {
  237. t.Fatalf("DeleteHandler failed: expected error about stopping non-existent model, got %v", err)
  238. }
  239. }
  240. func TestGetModelfileName(t *testing.T) {
  241. tests := []struct {
  242. name string
  243. modelfileName string
  244. fileExists bool
  245. expectedName string
  246. expectedErr error
  247. }{
  248. {
  249. name: "no modelfile specified, no modelfile exists",
  250. modelfileName: "",
  251. fileExists: false,
  252. expectedName: "",
  253. expectedErr: os.ErrNotExist,
  254. },
  255. {
  256. name: "no modelfile specified, modelfile exists",
  257. modelfileName: "",
  258. fileExists: true,
  259. expectedName: "Modelfile",
  260. expectedErr: nil,
  261. },
  262. {
  263. name: "modelfile specified, no modelfile exists",
  264. modelfileName: "crazyfile",
  265. fileExists: false,
  266. expectedName: "",
  267. expectedErr: os.ErrNotExist,
  268. },
  269. {
  270. name: "modelfile specified, modelfile exists",
  271. modelfileName: "anotherfile",
  272. fileExists: true,
  273. expectedName: "anotherfile",
  274. expectedErr: nil,
  275. },
  276. }
  277. for _, tt := range tests {
  278. t.Run(tt.name, func(t *testing.T) {
  279. cmd := &cobra.Command{
  280. Use: "fakecmd",
  281. }
  282. cmd.Flags().String("file", "", "path to modelfile")
  283. var expectedFilename string
  284. if tt.fileExists {
  285. tempDir, err := os.MkdirTemp("", "modelfiledir")
  286. defer os.RemoveAll(tempDir)
  287. if err != nil {
  288. t.Fatalf("temp modelfile dir creation failed: %v", err)
  289. }
  290. var fn string
  291. if tt.modelfileName != "" {
  292. fn = tt.modelfileName
  293. } else {
  294. fn = "Modelfile"
  295. }
  296. tempFile, err := os.CreateTemp(tempDir, fn)
  297. if err != nil {
  298. t.Fatalf("temp modelfile creation failed: %v", err)
  299. }
  300. expectedFilename = tempFile.Name()
  301. err = cmd.Flags().Set("file", expectedFilename)
  302. if err != nil {
  303. t.Fatalf("couldn't set file flag: %v", err)
  304. }
  305. } else {
  306. expectedFilename = tt.expectedName
  307. if tt.modelfileName != "" {
  308. err := cmd.Flags().Set("file", tt.modelfileName)
  309. if err != nil {
  310. t.Fatalf("couldn't set file flag: %v", err)
  311. }
  312. }
  313. }
  314. actualFilename, actualErr := getModelfileName(cmd)
  315. if actualFilename != expectedFilename {
  316. t.Errorf("expected filename: '%s' actual filename: '%s'", expectedFilename, actualFilename)
  317. }
  318. if tt.expectedErr != os.ErrNotExist {
  319. if actualErr != tt.expectedErr {
  320. t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr)
  321. }
  322. } else {
  323. if !os.IsNotExist(actualErr) {
  324. t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr)
  325. }
  326. }
  327. })
  328. }
  329. }
  330. func TestPushHandler(t *testing.T) {
  331. tests := []struct {
  332. name string
  333. modelName string
  334. serverResponse map[string]func(w http.ResponseWriter, r *http.Request)
  335. expectedError string
  336. expectedOutput string
  337. }{
  338. {
  339. name: "successful push",
  340. modelName: "test-model",
  341. serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
  342. "/api/push": func(w http.ResponseWriter, r *http.Request) {
  343. if r.Method != http.MethodPost {
  344. t.Errorf("expected POST request, got %s", r.Method)
  345. }
  346. var req api.PushRequest
  347. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  348. http.Error(w, err.Error(), http.StatusBadRequest)
  349. return
  350. }
  351. if req.Name != "test-model" {
  352. t.Errorf("expected model name 'test-model', got %s", req.Name)
  353. }
  354. // Simulate progress updates
  355. responses := []api.ProgressResponse{
  356. {Status: "preparing manifest"},
  357. {Digest: "sha256:abc123456789", Total: 100, Completed: 50},
  358. {Digest: "sha256:abc123456789", Total: 100, Completed: 100},
  359. }
  360. for _, resp := range responses {
  361. if err := json.NewEncoder(w).Encode(resp); err != nil {
  362. http.Error(w, err.Error(), http.StatusInternalServerError)
  363. return
  364. }
  365. w.(http.Flusher).Flush()
  366. }
  367. },
  368. },
  369. expectedOutput: "\nYou can find your model at:\n\n\thttps://ollama.com/test-model\n",
  370. },
  371. {
  372. name: "unauthorized push",
  373. modelName: "unauthorized-model",
  374. serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
  375. "/api/push": func(w http.ResponseWriter, r *http.Request) {
  376. w.Header().Set("Content-Type", "application/json")
  377. w.WriteHeader(http.StatusUnauthorized)
  378. err := json.NewEncoder(w).Encode(map[string]string{
  379. "error": "access denied",
  380. })
  381. if err != nil {
  382. t.Fatal(err)
  383. }
  384. },
  385. },
  386. expectedError: "you are not authorized to push to this namespace, create the model under a namespace you own",
  387. },
  388. }
  389. for _, tt := range tests {
  390. t.Run(tt.name, func(t *testing.T) {
  391. mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  392. if handler, ok := tt.serverResponse[r.URL.Path]; ok {
  393. handler(w, r)
  394. return
  395. }
  396. http.Error(w, "not found", http.StatusNotFound)
  397. }))
  398. defer mockServer.Close()
  399. t.Setenv("OLLAMA_HOST", mockServer.URL)
  400. cmd := &cobra.Command{}
  401. cmd.Flags().Bool("insecure", false, "")
  402. cmd.SetContext(context.TODO())
  403. // Redirect stderr to capture progress output
  404. oldStderr := os.Stderr
  405. r, w, _ := os.Pipe()
  406. os.Stderr = w
  407. // Capture stdout for the "Model pushed" message
  408. oldStdout := os.Stdout
  409. outR, outW, _ := os.Pipe()
  410. os.Stdout = outW
  411. err := PushHandler(cmd, []string{tt.modelName})
  412. // Restore stderr
  413. w.Close()
  414. os.Stderr = oldStderr
  415. // drain the pipe
  416. if _, err := io.ReadAll(r); err != nil {
  417. t.Fatal(err)
  418. }
  419. // Restore stdout and get output
  420. outW.Close()
  421. os.Stdout = oldStdout
  422. stdout, _ := io.ReadAll(outR)
  423. if tt.expectedError == "" {
  424. if err != nil {
  425. t.Errorf("expected no error, got %v", err)
  426. }
  427. if tt.expectedOutput != "" {
  428. if got := string(stdout); got != tt.expectedOutput {
  429. t.Errorf("expected output %q, got %q", tt.expectedOutput, got)
  430. }
  431. }
  432. } else {
  433. if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
  434. t.Errorf("expected error containing %q, got %v", tt.expectedError, err)
  435. }
  436. }
  437. })
  438. }
  439. }
  440. func TestListHandler(t *testing.T) {
  441. tests := []struct {
  442. name string
  443. args []string
  444. serverResponse []api.ListModelResponse
  445. expectedError string
  446. expectedOutput string
  447. }{
  448. {
  449. name: "list all models",
  450. args: []string{},
  451. serverResponse: []api.ListModelResponse{
  452. {Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
  453. {Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
  454. },
  455. expectedOutput: "NAME ID SIZE MODIFIED \n" +
  456. "model1 sha256:abc12 1.0 KB 24 hours ago \n" +
  457. "model2 sha256:def45 2.0 KB 2 days ago \n",
  458. },
  459. {
  460. name: "filter models by prefix",
  461. args: []string{"model1"},
  462. serverResponse: []api.ListModelResponse{
  463. {Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
  464. {Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-24 * time.Hour)},
  465. },
  466. expectedOutput: "NAME ID SIZE MODIFIED \n" +
  467. "model1 sha256:abc12 1.0 KB 24 hours ago \n",
  468. },
  469. {
  470. name: "server error",
  471. args: []string{},
  472. expectedError: "server error",
  473. },
  474. }
  475. for _, tt := range tests {
  476. t.Run(tt.name, func(t *testing.T) {
  477. mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  478. if r.URL.Path != "/api/tags" || r.Method != http.MethodGet {
  479. t.Errorf("unexpected request to %s %s", r.Method, r.URL.Path)
  480. http.Error(w, "not found", http.StatusNotFound)
  481. return
  482. }
  483. if tt.expectedError != "" {
  484. http.Error(w, tt.expectedError, http.StatusInternalServerError)
  485. return
  486. }
  487. response := api.ListResponse{Models: tt.serverResponse}
  488. if err := json.NewEncoder(w).Encode(response); err != nil {
  489. t.Fatal(err)
  490. }
  491. }))
  492. defer mockServer.Close()
  493. t.Setenv("OLLAMA_HOST", mockServer.URL)
  494. cmd := &cobra.Command{}
  495. cmd.SetContext(context.TODO())
  496. // Capture stdout
  497. oldStdout := os.Stdout
  498. r, w, _ := os.Pipe()
  499. os.Stdout = w
  500. err := ListHandler(cmd, tt.args)
  501. // Restore stdout and get output
  502. w.Close()
  503. os.Stdout = oldStdout
  504. output, _ := io.ReadAll(r)
  505. if tt.expectedError == "" {
  506. if err != nil {
  507. t.Errorf("expected no error, got %v", err)
  508. }
  509. if got := string(output); got != tt.expectedOutput {
  510. t.Errorf("expected output:\n%s\ngot:\n%s", tt.expectedOutput, got)
  511. }
  512. } else {
  513. if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
  514. t.Errorf("expected error containing %q, got %v", tt.expectedError, err)
  515. }
  516. }
  517. })
  518. }
  519. }
  520. func TestCreateHandler(t *testing.T) {
  521. tests := []struct {
  522. name string
  523. modelName string
  524. modelFile string
  525. serverResponse map[string]func(w http.ResponseWriter, r *http.Request)
  526. expectedError string
  527. expectedOutput string
  528. }{
  529. {
  530. name: "successful create",
  531. modelName: "test-model",
  532. modelFile: "FROM foo",
  533. serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
  534. "/api/create": func(w http.ResponseWriter, r *http.Request) {
  535. if r.Method != http.MethodPost {
  536. t.Errorf("expected POST request, got %s", r.Method)
  537. }
  538. req := api.CreateRequest{}
  539. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  540. http.Error(w, err.Error(), http.StatusBadRequest)
  541. return
  542. }
  543. if req.Name != "test-model" {
  544. t.Errorf("expected model name 'test-model', got %s", req.Name)
  545. }
  546. if req.From != "foo" {
  547. t.Errorf("expected from 'foo', got %s", req.From)
  548. }
  549. responses := []api.ProgressResponse{
  550. {Status: "using existing layer sha256:56bb8bd477a519ffa694fc449c2413c6f0e1d3b1c88fa7e3c9d88d3ae49d4dcb"},
  551. {Status: "writing manifest"},
  552. {Status: "success"},
  553. }
  554. for _, resp := range responses {
  555. if err := json.NewEncoder(w).Encode(resp); err != nil {
  556. http.Error(w, err.Error(), http.StatusInternalServerError)
  557. return
  558. }
  559. w.(http.Flusher).Flush()
  560. }
  561. },
  562. },
  563. expectedOutput: "",
  564. },
  565. }
  566. for _, tt := range tests {
  567. t.Run(tt.name, func(t *testing.T) {
  568. mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  569. handler, ok := tt.serverResponse[r.URL.Path]
  570. if !ok {
  571. t.Errorf("unexpected request to %s", r.URL.Path)
  572. http.Error(w, "not found", http.StatusNotFound)
  573. return
  574. }
  575. handler(w, r)
  576. }))
  577. t.Setenv("OLLAMA_HOST", mockServer.URL)
  578. t.Cleanup(mockServer.Close)
  579. tempFile, err := os.CreateTemp("", "modelfile")
  580. if err != nil {
  581. t.Fatal(err)
  582. }
  583. defer os.Remove(tempFile.Name())
  584. if _, err := tempFile.WriteString(tt.modelFile); err != nil {
  585. t.Fatal(err)
  586. }
  587. if err := tempFile.Close(); err != nil {
  588. t.Fatal(err)
  589. }
  590. cmd := &cobra.Command{}
  591. cmd.Flags().String("file", "", "")
  592. if err := cmd.Flags().Set("file", tempFile.Name()); err != nil {
  593. t.Fatal(err)
  594. }
  595. cmd.Flags().Bool("insecure", false, "")
  596. cmd.SetContext(context.TODO())
  597. // Redirect stderr to capture progress output
  598. oldStderr := os.Stderr
  599. r, w, _ := os.Pipe()
  600. os.Stderr = w
  601. // Capture stdout for the "Model pushed" message
  602. oldStdout := os.Stdout
  603. outR, outW, _ := os.Pipe()
  604. os.Stdout = outW
  605. err = CreateHandler(cmd, []string{tt.modelName})
  606. // Restore stderr
  607. w.Close()
  608. os.Stderr = oldStderr
  609. // drain the pipe
  610. if _, err := io.ReadAll(r); err != nil {
  611. t.Fatal(err)
  612. }
  613. // Restore stdout and get output
  614. outW.Close()
  615. os.Stdout = oldStdout
  616. stdout, _ := io.ReadAll(outR)
  617. if tt.expectedError == "" {
  618. if err != nil {
  619. t.Errorf("expected no error, got %v", err)
  620. }
  621. if tt.expectedOutput != "" {
  622. if got := string(stdout); got != tt.expectedOutput {
  623. t.Errorf("expected output %q, got %q", tt.expectedOutput, got)
  624. }
  625. }
  626. }
  627. })
  628. }
  629. }