diff --git a/.kiro/specs/job-array-one-based-index/tasks.md b/.kiro/specs/job-array-one-based-index/tasks.md index e351529b..0364517f 100644 --- a/.kiro/specs/job-array-one-based-index/tasks.md +++ b/.kiro/specs/job-array-one-based-index/tasks.md @@ -198,7 +198,7 @@ - _Requirements: 5.1, 5.2, 5.16, 8.2, 8.3, 9.2_ - _Depends: 8.1_ -- [ ] 8.3 Show the array in submit output and help +- [x] 8.3 Show the array in submit output and help - Add the element count to the client's submit response model; print the server's submit message with the array identity formatted (the count comes from the message); describe `name[1-N]`, `name[1]`, numbering from 1 and the limit (default 1000, at most 32767) in submit help - Done when a submit-output test shows `Job is submitted ... (3 elements)` with the formatted array identity and the help text lists the supported forms - _Requirements: 3.1, 3.2, 9.1_ diff --git a/src/frontend/xcecli/cmd/jobs/submit.go b/src/frontend/xcecli/cmd/jobs/submit.go index db7ddf1b..1e795386 100755 --- a/src/frontend/xcecli/cmd/jobs/submit.go +++ b/src/frontend/xcecli/cmd/jobs/submit.go @@ -489,19 +489,11 @@ func (js *JobSubmitter) RunJobsSubmit() error { return err } jobReq.TransferFiles = transferFile - jobResp, err := apiClient.SubmitJob("Bearer "+serverInfo.Token, jobReq) - logger.Debug("jobResp", zap.Any("jobResp", jobResp), zap.Error(err)) - logger.Info("jobResp", zap.Any("jobResp", jobResp), zap.Error(err)) - + jobResp, err := submitAndReport(apiClient, serverInfo.Token, jobReq) if err != nil { return err } - msg := strings.Replace(jobResp.Data.Message, fmt.Sprintf("<%d>", jobResp.Data.JobID), fmt.Sprintf("<%s>", format.FormatJobID(jobResp.Data.JobID)), 1) - fmt.Println(msg) - logger.Debug("job submitted successfully", zap.String("message", jobResp.Data.Message)) - logger.Info("job submitted successfully", zap.String("message", jobResp.Data.Message)) - if js.interactive { fmt.Println("<>") url, err := js.waitForInteractiveURL(apiClient, serverInfo.Token, fmt.Sprintf("%d", jobResp.Data.JobID)) @@ -520,6 +512,26 @@ func (js *JobSubmitter) RunJobsSubmit() error { return nil } +// submitAndReport submits jobReq and prints the server's message with the +// returned identity in written form. For an array the identity is the array +// ID (no element number) and the message already carries "(n elements)" +// (3.1, 3.2), so nothing is appended here. +func submitAndReport(apiClient *request.APIClient, token string, jobReq *models.JobSubmitRequest) (*models.JobSubmitResponse, error) { + jobResp, err := apiClient.SubmitJob("Bearer "+token, jobReq) + logger.Debug("jobResp", zap.Any("jobResp", jobResp), zap.Error(err)) + logger.Info("jobResp", zap.Any("jobResp", jobResp), zap.Error(err)) + + if err != nil { + return nil, err + } + + msg := strings.Replace(jobResp.Data.Message, fmt.Sprintf("<%d>", jobResp.Data.JobID), fmt.Sprintf("<%s>", format.FormatJobID(jobResp.Data.JobID)), 1) + fmt.Println(msg) + logger.Debug("job submitted successfully", zap.String("message", jobResp.Data.Message)) + logger.Info("job submitted successfully", zap.String("message", jobResp.Data.Message)) + return jobResp, nil +} + func (js *JobSubmitter) checkJobStatus(apiClient *request.APIClient, token string, jobID string) (string, error) { var resp models.JobsResponse err := apiClient.GetRequestFromAPIServer( @@ -650,7 +662,17 @@ Example: xce jobs submit --configfile job.xce # submit job using parameters from a configuration file xce jobs submit -m "host1 host2" sleep 10 # submit job into host1 host2 xce jobs submit -I --tty sleep 10 # submit interactive job with tty - xce jobs submit -I --xjob sleep 10 # submit interactive job with xjob`, + xce jobs submit -I --xjob sleep 10 # submit interactive job with xjob + xce jobs submit -J 'name[1-N]' sleep 10 # submit a job array of N elements named "name" + xce jobs submit -J 'name[1]' sleep 10 # submit a job array of one element + +Job arrays: + -J 'name[1-N]' submits N elements, numbered 1 through N; -J 'name[1]' + submits a one-element array. Element numbers start at 1. The maximum + array size is set by the API server's configuration (default 1000, + never above 32767). Submit prints the array identity, e.g. + Job is submitted to queue (N elements). + Refer to one element as [k]. Interactive jobs cannot be arrays.`, SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { diff --git a/src/frontend/xcecli/cmd/jobs/submit_test.go b/src/frontend/xcecli/cmd/jobs/submit_test.go index 61c1db90..9aa63a55 100644 --- a/src/frontend/xcecli/cmd/jobs/submit_test.go +++ b/src/frontend/xcecli/cmd/jobs/submit_test.go @@ -1,8 +1,20 @@ package jobs import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "os" + "strings" "testing" + + "go.uber.org/zap" + + "xcecli/common/format" + "xcecli/common/request" + "xcecli/config" + "xcecli/models" ) // bug-1676: BuildJobRequest asserts our own hostname as FromHost rather than @@ -25,3 +37,96 @@ func TestBuildJobRequest_SetsFromHostToLocalHostname(t *testing.T) { t.Errorf("FromHost = %q, want local hostname %q", req.FromHost, wantHost) } } + +// submitServer answers every submit with the given response data. +func submitServer(t *testing.T, data models.JobSubmitData) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(models.JobSubmitResponse{Code: 0, Msg: "ok", Data: data}) + })) + t.Cleanup(srv.Close) + // The submit path logs through config.Log, which only main initialises. + if config.Log == nil { + config.Log = zap.NewNop() + t.Cleanup(func() { config.Log = nil }) + } + return srv +} + +// An accepted array submission prints the array identity in written form, +// without an element number, and the server's element count (3.1, 3.2); the +// count is mirrored in the client model for scripted use. +func TestSubmitOutput_ArrayIdentityAndCount(t *testing.T) { + srv := submitServer(t, models.JobSubmitData{ + JobID: int64(testArrayKey), + Message: fmt.Sprintf("Job <%d> is submitted to queue (3 elements).", testArrayKey), + ArraySize: 3, + }) + js := NewJobSubmitter(nil) + js.command = "sleep 1" + req, err := js.BuildJobRequest() + if err != nil { + t.Fatalf("BuildJobRequest: %v", err) + } + var resp *models.JobSubmitResponse + out, err := captureStdout(t, func() error { + var e error + resp, e = submitAndReport(request.NewAPIClient(srv.URL), "tok", req) + return e + }) + if err != nil { + t.Fatalf("submitAndReport: %v", err) + } + want := "Job <" + testArrayWritten + "> is submitted to queue (3 elements).\n" + if out != want { + t.Fatalf("output = %q, want %q", out, want) + } + if resp.Data.ArraySize != 3 { + t.Errorf("ArraySize = %d, want 3", resp.Data.ArraySize) + } +} + +// An ordinary submission prints exactly as before (3.3): no count added. +func TestSubmitOutput_OrdinaryUnchanged(t *testing.T) { + id := format.MakeJobID(0, 0, 1700000000, 0, 7) + srv := submitServer(t, models.JobSubmitData{ + JobID: int64(id), + Message: fmt.Sprintf("Job <%d> is submitted to queue .", id), + }) + js := NewJobSubmitter(nil) + js.command = "sleep 1" + req, err := js.BuildJobRequest() + if err != nil { + t.Fatalf("BuildJobRequest: %v", err) + } + var resp *models.JobSubmitResponse + out, err := captureStdout(t, func() error { + var e error + resp, e = submitAndReport(request.NewAPIClient(srv.URL), "tok", req) + return e + }) + if err != nil { + t.Fatalf("submitAndReport: %v", err) + } + want := "Job <0-0-1700000000-7> is submitted to queue .\n" + if out != want { + t.Fatalf("output = %q, want %q", out, want) + } + if resp.Data.ArraySize != 0 { + t.Errorf("ArraySize = %d, want 0", resp.Data.ArraySize) + } +} + +// Submit help describes the array forms, numbering from 1 and the limit (9.1). +func TestSubmitHelpShowsArrayForms(t *testing.T) { + long := NewSubmitJobsCmd(nil).Long + for _, s := range []string{ + "-J 'name[1-N]'", "-J 'name[1]'", "start at 1", + "set by the API server", "default 1000", "32767", + } { + if !strings.Contains(long, s) { + t.Errorf("submit help lacks %q", s) + } + } +} diff --git a/src/frontend/xcecli/models/jobs.go b/src/frontend/xcecli/models/jobs.go index eb8ebb86..5bf0e34e 100755 --- a/src/frontend/xcecli/models/jobs.go +++ b/src/frontend/xcecli/models/jobs.go @@ -365,6 +365,9 @@ type JobSubmitRequest struct { type JobSubmitData struct { JobID int64 `json:"jobid"` Message string `json:"message"` + // ArraySize is the number of elements of a submitted array (3.2); zero + // for an ordinary job. The printed count comes from Message. + ArraySize int `json:"array_size,omitempty"` } // JobSubmitResponse define the data structure in the job submit response