feat(job-array-one-based-index): xcecli submit shows the array (task 8.3)
Submitting an array prints the server's message with the array ID in written form (no [k]) and the element count, e.g. 'Job <0-0-...> is submitted to queue <normal> (3 elements).'; ordinary submits print exactly as before. The client model mirrors array_size. Submit help documents name[1-N] and name[1], numbering from 1 and the API-server limit (default 1000, at most 32767). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
58ca7abb36
commit
d3b12159a4
4 changed files with 141 additions and 11 deletions
|
|
@ -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 <array-id> is submitted ... (3 elements)` with the formatted array identity and the help text lists the supported forms
|
||||
- _Requirements: 3.1, 3.2, 9.1_
|
||||
|
|
|
|||
|
|
@ -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("<<waiting for dispatch ...>>")
|
||||
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 <array-id> is submitted to queue <normal> (N elements).
|
||||
Refer to one element as <array-id>[k]. Interactive jobs cannot be arrays.`,
|
||||
|
||||
SilenceUsage: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
|
|
|||
|
|
@ -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 <normal> (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 <normal> (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 <normal>.", 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 <normal>.\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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue