From 611155955a2d3f88123c0f32a664761fcf4d5f13 Mon Sep 17 00:00:00 2001 From: arek Date: Thu, 15 May 2025 11:16:18 -0700 Subject: [PATCH 1/3] update CLI to API v2 --- cli/runner/client/client.go | 54 ++++++++++++++++----------------- cli/runner/cmd/run.go | 59 ++++++++++++------------------------- 2 files changed, 45 insertions(+), 68 deletions(-) diff --git a/cli/runner/client/client.go b/cli/runner/client/client.go index 5a7caa5..4a8d973 100644 --- a/cli/runner/client/client.go +++ b/cli/runner/client/client.go @@ -3,26 +3,28 @@ package client import ( "encoding/json" "fmt" - "github.com/runner-x/runner-x/server/api/v1" "io" "net/http" "strings" "time" - coderunner "github.com/runner-x/runner-x/engine/coderunner/v1" + v2 "github.com/runner-x/runner-x/server/api/v2" ) -// TODO: fill in this client package as needed and create, use Client as needed in CLI commands - const ( - DEFAULT_URL = "http://localhost:10100" + DEFAULT_URL = "http://localhost:10100" + + // Notably, the server still serves under these endpoints + // Despite the migration to v2 on the backend. LANG_ENDPOINT = "/api/v1/languages" RUN_ENDPOINT = "/api/v1/run" + + TIMEOUT_DEFAULT = time.Second * 5 ) type Requester interface { - Run(r *v1.RunRequest) (*v1.RunResponse, error) - Languages() (*v1.LanguagesResponse, error) + Run(r *v2.RunRequest) (*v2.RunResponse, error) + Languages() (*v2.LanguagesResponse, error) } type Client struct { @@ -37,18 +39,16 @@ type Config struct { } func NewClient() *Client { - // TODO: implement client with defaults like localhost url (nice to have) var c Client c.BaseUrl = DEFAULT_URL c.HttpClient = http.Client{ - Timeout: time.Second * coderunner.TIMEOUT_DEFAULT, + Timeout: TIMEOUT_DEFAULT, } return &c } func NewClientFromConfig(c Config) *Client { - // TODO: create client from config var client Client client.BaseUrl = c.BaseUrl client.HttpClient = http.Client{ @@ -58,17 +58,18 @@ func NewClientFromConfig(c Config) *Client { return &client } -func (c *Client) Run(r *v1.RunRequest) (*v1.RunResponse, error) { - // TODO: implement/refactor +func (c *Client) Run(r *v2.RunRequest) (*v2.RunResponse, error) { source := r.Source - reqBody := v1.RunRequest{ + reqBody := v2.RunRequest{ Source: source, Lang: r.Lang, } jsonBody, err := json.Marshal(reqBody) - PanicCheck(err) + if err != nil { + return nil, err + } body := strings.NewReader(string(jsonBody)) @@ -91,15 +92,16 @@ func (c *Client) Run(r *v1.RunRequest) (*v1.RunResponse, error) { return nil, err } - var ret v1.RunResponse - decodeErr := json.Unmarshal(respReader, &ret) - PanicCheck(decodeErr) + var ret v2.RunResponse + err = json.Unmarshal(respReader, &ret) + if err != nil { + return nil, err + } return &ret, nil } -func (c *Client) Languages() (*v1.LanguagesResponse, error) { - // TODO: implement/refactor +func (c *Client) Languages() (*v2.LanguagesResponse, error) { req, err := http.NewRequest("GET", c.BaseUrl+LANG_ENDPOINT, nil) if err != nil { return nil, err @@ -118,15 +120,11 @@ func (c *Client) Languages() (*v1.LanguagesResponse, error) { return nil, err } - var jsonLangs v1.LanguagesResponse - decodeErr := json.Unmarshal(body, &jsonLangs) - PanicCheck(decodeErr) - - return &jsonLangs, nil -} - -func PanicCheck(err error) { + var jsonLangs v2.LanguagesResponse + err = json.Unmarshal(body, &jsonLangs) if err != nil { - panic(err) + return nil, err } + + return &jsonLangs, nil } diff --git a/cli/runner/cmd/run.go b/cli/runner/cmd/run.go index a79bde9..5be1e9e 100644 --- a/cli/runner/cmd/run.go +++ b/cli/runner/cmd/run.go @@ -5,11 +5,12 @@ package cmd import ( "fmt" - "github.com/runner-x/runner-x/server/api/v1" "os" + "path/filepath" "github.com/runner-x/runner-x/cli/runner/client" - coderunner "github.com/runner-x/runner-x/engine/coderunner/v1" + coderunner "github.com/runner-x/runner-x/engine/coderunner/v2" + v2 "github.com/runner-x/runner-x/server/api/v2" "github.com/spf13/cobra" ) @@ -27,26 +28,25 @@ var runCmd = &cobra.Command{ fmt.Println("Multiple file compilation not yet supported. Please only specify a single file for compilation.") return } - str, err := cmd.Flags().GetString("lang") + + url, err := rootCmd.PersistentFlags().GetString("url") if err != nil { panic(err) } - url, err := rootCmd.PersistentFlags().GetString("url") + timeout, err := cmd.Flags().GetInt("timeout") if err != nil { panic(err) } filename := args[0] - ext := extractExtension(filename) - var langCheck coderunner.Language - if str == "implicit" { - if lang, found := coderunner.ExtensionFileMap[ext]; found { - langCheck = lang - } else { - fmt.Printf("unrecognized file type: %s", ext) - return - } + ext := filepath.Ext(filename) + var langCheck string + if lang, found := coderunner.FileExtensionToLangMap[ext]; found { + langCheck = lang.Name + } else { + fmt.Printf("unrecognized file type: %s", ext) + return } source, err := os.ReadFile(filename) @@ -55,15 +55,13 @@ var runCmd = &cobra.Command{ return } - // add a flag to modify timeout? - var cmdClient client.Requester - clint := client.Config{ + config := client.Config{ BaseUrl: url, - Timeout: coderunner.TIMEOUT_DEFAULT, + Timeout: timeout, } - cmdClient = client.NewClientFromConfig(clint) + cmdClient := client.NewClientFromConfig(config) - r := &v1.RunRequest{ + r := &v2.RunRequest{ Source: string(source[:]), Lang: langCheck, } @@ -71,7 +69,7 @@ var runCmd = &cobra.Command{ resp, err := cmdClient.Run(r) if err != nil { fmt.Println(err) - } else if resp.Error != nil { + } else if resp.Error != "" { fmt.Println(resp.Error) } else { fmt.Printf("Stdout: %s\nStderr: %s\n", resp.Stdout, resp.Stderr) @@ -82,24 +80,5 @@ var runCmd = &cobra.Command{ func init() { rootCmd.AddCommand(runCmd) - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // runCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // runCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - runCmd.Flags().StringP("lang", "l", "implicit", "specifies the language for the source file. If not specified, CLI will try to guess language type before making API call.") -} - -func extractExtension(filename string) string { - f := []rune(filename) - for i := len(f) - 1; i >= 0; i -= 1 { - if f[i] == '.' { - return string(f[i+1:]) - } - } - return filename + runCmd.Flags().IntP("timeout", "t", 5, "the timeout delay for each request. default 5 seconds") } From 7a6b01d73a1c4d4659740caf8289db37edb328aa Mon Sep 17 00:00:00 2001 From: arek Date: Sat, 31 May 2025 12:30:02 -0700 Subject: [PATCH 2/3] Migrated run and langs commands to v2 api, added testing - testing suites for langs - client mocks using the new Requester interface - HTTP mocks using the `httptest` package --- cli/runner/client/client.go | 72 +++++++++++++++++--- cli/runner/client/client_test.go | 109 ++++++++++++++++++++++++++++++ cli/runner/client/mocks/client.go | 65 ++++++++++++++++++ cli/runner/cmd/langs.go | 12 ++-- cli/runner/cmd/root.go | 6 +- cli/runner/cmd/run.go | 46 ++++++------- flake.nix | 46 +++++++------ 7 files changed, 294 insertions(+), 62 deletions(-) create mode 100644 cli/runner/client/client_test.go create mode 100644 cli/runner/client/mocks/client.go diff --git a/cli/runner/client/client.go b/cli/runner/client/client.go index 4a8d973..d61982e 100644 --- a/cli/runner/client/client.go +++ b/cli/runner/client/client.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "time" @@ -12,7 +13,8 @@ import ( ) const ( - DEFAULT_URL = "http://localhost:10100" + // DEFAULT_URL = "http://localhost:10100" + DEFAULT_URL = "https://runner.fly.dev" // Notably, the server still serves under these endpoints // Despite the migration to v2 on the backend. @@ -32,33 +34,48 @@ type Client struct { HttpClient http.Client // http client to use for GET, POST requests } +// abstracts the inner requester to allow us to generate mocks +type CliClient struct { + client Requester +} + type Config struct { BaseUrl string // add any other configurable values we may want here Timeout int } -func NewClient() *Client { - var c Client - c.BaseUrl = DEFAULT_URL - c.HttpClient = http.Client{ +func NewClient() *CliClient { + var client Client + client.BaseUrl = DEFAULT_URL + client.HttpClient = http.Client{ Timeout: TIMEOUT_DEFAULT, } - return &c + return &CliClient{ + client, + } } -func NewClientFromConfig(c Config) *Client { +func NewClientFromConfig(c Config) *CliClient { var client Client client.BaseUrl = c.BaseUrl client.HttpClient = http.Client{ Timeout: time.Second * time.Duration(c.Timeout), } - return &client + return &CliClient{ + client, + } } -func (c *Client) Run(r *v2.RunRequest) (*v2.RunResponse, error) { +func NewClientWithRequester(r Requester) *CliClient { + return &CliClient{ + client: r, + } +} + +func (c Client) Run(r *v2.RunRequest) (*v2.RunResponse, error) { source := r.Source reqBody := v2.RunRequest{ @@ -101,7 +118,7 @@ func (c *Client) Run(r *v2.RunRequest) (*v2.RunResponse, error) { return &ret, nil } -func (c *Client) Languages() (*v2.LanguagesResponse, error) { +func (c Client) Languages() (*v2.LanguagesResponse, error) { req, err := http.NewRequest("GET", c.BaseUrl+LANG_ENDPOINT, nil) if err != nil { return nil, err @@ -128,3 +145,38 @@ func (c *Client) Languages() (*v2.LanguagesResponse, error) { return &jsonLangs, nil } + +func (cli *CliClient) LanguageRequest() (*v2.LanguagesResponse, error) { + return cli.client.Languages() +} + +func (cli *CliClient) RunRequest(language string, filename string) (*v2.RunResponse, error) { + // check if the language is supported + langs, err := cli.client.Languages() + if err != nil { + return nil, err + } + + validLanguage := false + for _, lang := range langs.Languages { + if lang == language { + validLanguage = true + break + } + } + if !validLanguage { + return nil, fmt.Errorf("invalid language: %s", language) + } + + source, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("file not found: %s", filename) + } + + req := &v2.RunRequest{ + Source: string(source), + Lang: language, + } + + return cli.client.Run(req) +} diff --git a/cli/runner/client/client_test.go b/cli/runner/client/client_test.go new file mode 100644 index 0000000..f5d0726 --- /dev/null +++ b/cli/runner/client/client_test.go @@ -0,0 +1,109 @@ +package client_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + gomock "github.com/golang/mock/gomock" + "github.com/runner-x/runner-x/cli/runner/client" + mock_client "github.com/runner-x/runner-x/cli/runner/client/mocks" + v2 "github.com/runner-x/runner-x/server/api/v2" +) + +func TestLanguageRequest(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expected := []string{ + "python3", + "nodejs", + "c++", + "go", + "bash", + "rust", + } + mockLanguageAgent := mock_client.NewMockRequester(ctrl) + + langResponse := v2.LanguagesResponse{ + Languages: expected, + } + + mockLanguageAgent.EXPECT().Languages().Return(&langResponse, nil) + agent := client.NewClientWithRequester(mockLanguageAgent) + + resp, err := agent.LanguageRequest() + + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + for i, lang := range resp.Languages { + if expected[i] != lang { + t.Errorf("language request response mismatch: wanted %v, got %v", expected, resp.Languages) + return + } + } +} + +func TestLanguageHttpRequest(t *testing.T) { + testResponseServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(200) + res.Write([]byte("{\"languages\":[\"python3\",\"nodejs\",\"c++\",\"go\",\"bash\",\"rust\"]}")) + })) + defer testResponseServer.Close() + + expected := []string{ + "python3", + "nodejs", + "c++", + "go", + "bash", + "rust", + } + + client := client.NewClientFromConfig(client.Config{ + BaseUrl: testResponseServer.URL, + }) + + resp, err := client.LanguageRequest() + + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + for i, lang := range resp.Languages { + if expected[i] != lang { + t.Errorf("language request response mismatch: wanted %v, got %v", expected, resp.Languages) + return + } + } +} + +func TestHTTPRequestError(t *testing.T) { + client := client.NewClientFromConfig(client.Config{ + BaseUrl: "malformed-url", + }) + resp, err := client.LanguageRequest() + + if err == nil { + t.Errorf("expected error, got valid response: %v", resp) + } +} + +func TestLanguageBadStatusCode(t *testing.T) { + statusCode := 500 + testResponseServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(statusCode) + })) + defer testResponseServer.Close() + + client := client.NewClientFromConfig(client.Config{ + BaseUrl: testResponseServer.URL, + }) + + resp, err := client.LanguageRequest() + if err == nil { + t.Errorf("expected error, got non-nil response %v", resp) + } +} diff --git a/cli/runner/client/mocks/client.go b/cli/runner/client/mocks/client.go new file mode 100644 index 0000000..e0ca50e --- /dev/null +++ b/cli/runner/client/mocks/client.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client/client.go + +// Package mock_client is a generated GoMock package. +package mock_client + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v2 "github.com/runner-x/runner-x/server/api/v2" +) + +// MockRequester is a mock of Requester interface. +type MockRequester struct { + ctrl *gomock.Controller + recorder *MockRequesterMockRecorder +} + +// MockRequesterMockRecorder is the mock recorder for MockRequester. +type MockRequesterMockRecorder struct { + mock *MockRequester +} + +// NewMockRequester creates a new mock instance. +func NewMockRequester(ctrl *gomock.Controller) *MockRequester { + mock := &MockRequester{ctrl: ctrl} + mock.recorder = &MockRequesterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRequester) EXPECT() *MockRequesterMockRecorder { + return m.recorder +} + +// Languages mocks base method. +func (m *MockRequester) Languages() (*v2.LanguagesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Languages") + ret0, _ := ret[0].(*v2.LanguagesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Languages indicates an expected call of Languages. +func (mr *MockRequesterMockRecorder) Languages() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Languages", reflect.TypeOf((*MockRequester)(nil).Languages)) +} + +// Run mocks base method. +func (m *MockRequester) Run(r *v2.RunRequest) (*v2.RunResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run", r) + ret0, _ := ret[0].(*v2.RunResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Run indicates an expected call of Run. +func (mr *MockRequesterMockRecorder) Run(r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockRequester)(nil).Run), r) +} diff --git a/cli/runner/cmd/langs.go b/cli/runner/cmd/langs.go index a34eb4f..af5f64d 100644 --- a/cli/runner/cmd/langs.go +++ b/cli/runner/cmd/langs.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/runner-x/runner-x/cli/runner/client" - coderunner "github.com/runner-x/runner-x/engine/coderunner/v1" "github.com/spf13/cobra" ) @@ -21,14 +20,19 @@ var langsCmd = &cobra.Command{ panic(err) } - var cmdClient client.Requester + timeout, err := rootCmd.PersistentFlags().GetInt("timeout") + if err != nil { + panic(err) + } + + var cmdClient *client.CliClient clint := client.Config{ BaseUrl: url, - Timeout: coderunner.TIMEOUT_DEFAULT, + Timeout: timeout, } cmdClient = client.NewClientFromConfig(clint) - resp, err := cmdClient.Languages() + resp, err := cmdClient.LanguageRequest() if err != nil { fmt.Println(err) return diff --git a/cli/runner/cmd/root.go b/cli/runner/cmd/root.go index cf8a0ae..f9d3fac 100644 --- a/cli/runner/cmd/root.go +++ b/cli/runner/cmd/root.go @@ -6,6 +6,7 @@ package cmd import ( "os" + "github.com/runner-x/runner-x/cli/runner/client" "github.com/spf13/cobra" ) @@ -41,6 +42,7 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - rootCmd.PersistentFlags().StringP("url", "u", "http://localhost:10100", "specifies the url host for the runner API") + // rootCmd.Flags().BoolP("toggle", "", false, "Help message for toggle") + rootCmd.PersistentFlags().StringP("url", "u", client.DEFAULT_URL, "specifies the url host for the runner API") + rootCmd.PersistentFlags().IntP("timeout", "t", 5, "the timeout delay for each request. default 5 seconds") } diff --git a/cli/runner/cmd/run.go b/cli/runner/cmd/run.go index 5be1e9e..c05ee24 100644 --- a/cli/runner/cmd/run.go +++ b/cli/runner/cmd/run.go @@ -5,12 +5,10 @@ package cmd import ( "fmt" - "os" "path/filepath" "github.com/runner-x/runner-x/cli/runner/client" - coderunner "github.com/runner-x/runner-x/engine/coderunner/v2" - v2 "github.com/runner-x/runner-x/server/api/v2" + v2 "github.com/runner-x/runner-x/engine/coderunner/v2" "github.com/spf13/cobra" ) @@ -19,7 +17,6 @@ var runCmd = &cobra.Command{ Use: "run", Short: "runs the supplied code loaded in from a file.", Run: func(cmd *cobra.Command, args []string) { - //TODO: implement flag logic, implement source file args parsing argLen := len(args) if argLen < 1 { fmt.Println("No file specified for compilation; Please specify a file!") @@ -28,31 +25,32 @@ var runCmd = &cobra.Command{ fmt.Println("Multiple file compilation not yet supported. Please only specify a single file for compilation.") return } + sourceFile := args[0] url, err := rootCmd.PersistentFlags().GetString("url") if err != nil { panic(err) } - - timeout, err := cmd.Flags().GetInt("timeout") + timeout, err := rootCmd.PersistentFlags().GetInt("timeout") if err != nil { panic(err) } - - filename := args[0] - ext := filepath.Ext(filename) - var langCheck string - if lang, found := coderunner.FileExtensionToLangMap[ext]; found { - langCheck = lang.Name - } else { - fmt.Printf("unrecognized file type: %s", ext) - return + language, err := cmd.Flags().GetString("language") + if err != nil { + panic(err) } - source, err := os.ReadFile(filename) - if err != nil { - fmt.Printf("File not found: %s", filename) - return + if language == "" { + ext := filepath.Ext(sourceFile) + if ext == "" { + } + + if name, exists := v2.FileExtensionToLangMap[ext]; ext == "" || !exists { + fmt.Println("Unable to determine source language implicitly; please set with the language flag or add a file extension.") + return + } else { + language = name.Name + } } config := client.Config{ @@ -61,12 +59,7 @@ var runCmd = &cobra.Command{ } cmdClient := client.NewClientFromConfig(config) - r := &v2.RunRequest{ - Source: string(source[:]), - Lang: langCheck, - } - - resp, err := cmdClient.Run(r) + resp, err := cmdClient.RunRequest(language, sourceFile) if err != nil { fmt.Println(err) } else if resp.Error != "" { @@ -80,5 +73,6 @@ var runCmd = &cobra.Command{ func init() { rootCmd.AddCommand(runCmd) - runCmd.Flags().IntP("timeout", "t", 5, "the timeout delay for each request. default 5 seconds") + // runCmd.Flags().IntP("timeout", "t", 5, "the timeout delay for each request. default 5 seconds") + runCmd.Flags().StringP("language", "l", "", "specifies a compilation language. to see supported languages, see the `langs` command.") } diff --git a/flake.nix b/flake.nix index bead64d..aedb192 100644 --- a/flake.nix +++ b/flake.nix @@ -3,24 +3,30 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem - (system: - let - pkgs = import nixpkgs { - inherit system; - }; - in - with pkgs; - { - devShells.default = mkShell { - buildInputs = [ - go - earthly - nixpkgs-fmt - nodejs_18 - ]; - }; - } - ); + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + with pkgs; + { + devShells.default = mkShell { + buildInputs = [ + go + mockgen + earthly + nixpkgs-fmt + nodejs_18 + ]; + }; + } + ); } From ab26ba979cdf039ec3a3c9ca4aaacd41e82e4ddf Mon Sep 17 00:00:00 2001 From: arek Date: Sun, 1 Jun 2025 14:04:55 -0700 Subject: [PATCH 3/3] adding run request tests --- cli/runner/client/client_test.go | 91 +++++++++++++++++++++++++-- cli/runner/client/test-files/test.cpp | 4 ++ 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 cli/runner/client/test-files/test.cpp diff --git a/cli/runner/client/client_test.go b/cli/runner/client/client_test.go index f5d0726..f5167c3 100644 --- a/cli/runner/client/client_test.go +++ b/cli/runner/client/client_test.go @@ -3,6 +3,7 @@ package client_test import ( "net/http" "net/http/httptest" + "os" "testing" gomock "github.com/golang/mock/gomock" @@ -84,14 +85,19 @@ func TestHTTPRequestError(t *testing.T) { client := client.NewClientFromConfig(client.Config{ BaseUrl: "malformed-url", }) - resp, err := client.LanguageRequest() + langResp, err := client.LanguageRequest() + + if err == nil { + t.Errorf("expected error, got valid language response: %v", langResp) + } + runResp, err := client.RunRequest("c++", "test-files/test.cpp") if err == nil { - t.Errorf("expected error, got valid response: %v", resp) + t.Errorf("expected error, got valid run response: %v", runResp) } } -func TestLanguageBadStatusCode(t *testing.T) { +func TestBadStatusCode(t *testing.T) { statusCode := 500 testResponseServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(statusCode) @@ -102,8 +108,83 @@ func TestLanguageBadStatusCode(t *testing.T) { BaseUrl: testResponseServer.URL, }) - resp, err := client.LanguageRequest() + langResp, err := client.LanguageRequest() + if err == nil { + t.Errorf("expected error, got non-nil language response %v", langResp) + } + + runResp, err := client.RunRequest("c++", "test-files/test.cpp") + if err == nil { + t.Errorf("expected error, got non-nil run response %v", runResp) + } +} + +func TestRunRequest(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockAgent := mock_client.NewMockRequester(ctrl) + contents, err := os.ReadFile("test-files/test.cpp") + if err != nil { + t.Fatalf("unable to read test file: %v", err) + return + } + + mockRequest := v2.RunRequest{ + Source: string(contents), + Lang: "c++", + } + mockLangs := v2.LanguagesResponse{ + Languages: []string{"c++"}, + } + mockResponse := v2.RunResponse{ + Stdout: "Hello, World!", + Stderr: "", + Error: "", + } + + mockAgent.EXPECT().Run(&mockRequest).Return(&mockResponse, nil) + mockAgent.EXPECT().Languages().Return(&mockLangs, nil) + agent := client.NewClientWithRequester(mockAgent) + + resp, err := agent.RunRequest("c++", "test-files/test.cpp") + + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + if *resp != mockResponse { + t.Errorf("response mismatch, wanted %v, got %v", mockResponse, *resp) + } +} + +func TestRunInvalidLanguage(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(200) + res.Write([]byte("{\"languages\": [\"invalid-language\"]}")) + })) + defer testServer.Close() + + agent := client.NewClientFromConfig(client.Config{ + BaseUrl: testServer.URL, + }) + + resp, err := agent.RunRequest("c++", "test-files/test.cpp") + if err == nil { + t.Errorf("wanted err, got non-nil response %v", resp) + } +} + +func TestRunInvalidSource(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(200) + res.Write([]byte("{\"languages\": [\"c++\"]}")) + })) + defer testServer.Close() + agent := client.NewClient() + resp, err := agent.RunRequest("c++", "nonexistent-file") + if err == nil { - t.Errorf("expected error, got non-nil response %v", resp) + t.Errorf("wanted err, got non-nil response %v", resp) } } diff --git a/cli/runner/client/test-files/test.cpp b/cli/runner/client/test-files/test.cpp new file mode 100644 index 0000000..dc8ac04 --- /dev/null +++ b/cli/runner/client/test-files/test.cpp @@ -0,0 +1,4 @@ +#include +int main() { + std::cout << "Hello, World!" << std::endl; +} \ No newline at end of file