mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 01:25:06 +01:00 
			
		
		
		
	new test: start/stop job via REST API
This commit is contained in:
		@@ -81,13 +81,12 @@ This project uses [gqlgen](https://github.com/99designs/gqlgen) for the GraphQL
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### TODO
 | 
					### TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [ ] fix frontend
 | 
					 | 
				
			||||||
- [ ] write (unit) tests
 | 
					- [ ] write (unit) tests
 | 
				
			||||||
- [ ] make tokens and sessions (currently based on cookies) expire after some configurable time
 | 
					- [ ] make tokens and sessions (currently based on cookies) expire after some configurable time
 | 
				
			||||||
- [ ] when authenticating using a JWT, check if that user still exists
 | 
					- [ ] when authenticating using a JWT, check if that user still exists
 | 
				
			||||||
- [ ] allow mysql as database and passing the database uri as environment variable
 | 
					- [ ] allow passing the database uri as environment variable
 | 
				
			||||||
- [ ] fix InfluxDB MetricDataRepository (new or old line-protocol format? Support node-level metrics only?)
 | 
					- [ ] fix InfluxDB MetricDataRepository (new or old line-protocol format? Support node-level metrics only?)
 | 
				
			||||||
- [ ] support all metric scopes
 | 
					 | 
				
			||||||
- [ ] documentation, comments in the code base
 | 
					- [ ] documentation, comments in the code base
 | 
				
			||||||
- [ ] write more TODOs
 | 
					- [ ] write more TODOs
 | 
				
			||||||
- [ ] caching
 | 
					- [ ] use more prepared statements and [sqrl](https://github.com/elgris/sqrl) instead of *squirrel*
 | 
				
			||||||
 | 
					- [ ] replace `github.com/ClusterCockpit/cc-jobarchive` with `github.com/ClusterCockpit/cc-backend` in all import paths
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -183,8 +183,8 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if config.GetClusterConfig(req.Cluster) == nil {
 | 
						if config.GetClusterConfig(req.Cluster) == nil || config.GetPartition(req.Cluster, req.Partition) == nil {
 | 
				
			||||||
		http.Error(rw, fmt.Sprintf("cluster '%s' does not exist", req.Cluster), http.StatusBadRequest)
 | 
							http.Error(rw, fmt.Sprintf("cluster %#v or partition %#v does not exist", req.Cluster, req.Partition), http.StatusBadRequest)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -209,6 +209,10 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if req.State == "" {
 | 
				
			||||||
 | 
							req.State = schema.JobStateRunning
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req.RawResources, err = json.Marshal(req.Resources)
 | 
						req.RawResources, err = json.Marshal(req.Resources)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatal(err)
 | 
							log.Fatal(err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										283
									
								
								api_test.go
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										283
									
								
								api_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-jobarchive/api"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-jobarchive/config"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-jobarchive/graph"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-jobarchive/metricdata"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-jobarchive/schema"
 | 
				
			||||||
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
 | 
						"github.com/jmoiron/sqlx"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setup(t *testing.T) *api.RestApi {
 | 
				
			||||||
 | 
						if db != nil {
 | 
				
			||||||
 | 
							panic("prefer using sub-tests (`t.Run`) or implement `cleanup` before calling setup twice.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						devNull, err := os.Open(os.DevNull)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Makes output cleaner
 | 
				
			||||||
 | 
						log.SetOutput(devNull)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const testclusterJson = `{
 | 
				
			||||||
 | 
							"name": "testcluster",
 | 
				
			||||||
 | 
							"partitions": [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									"name": "default",
 | 
				
			||||||
 | 
									"processorType": "Intel Core i7-4770",
 | 
				
			||||||
 | 
									"socketsPerNode": 1,
 | 
				
			||||||
 | 
									"coresPerSocket": 4,
 | 
				
			||||||
 | 
									"threadsPerCore": 2,
 | 
				
			||||||
 | 
									"flopRateScalar": 44,
 | 
				
			||||||
 | 
									"flopRateSimd": 704,
 | 
				
			||||||
 | 
									"memoryBandwidth": 80,
 | 
				
			||||||
 | 
									"topology": {
 | 
				
			||||||
 | 
										"node": [0, 1, 2, 3, 4, 5, 6, 7],
 | 
				
			||||||
 | 
										"socket": [[0, 1, 2, 3, 4, 5, 6, 7]],
 | 
				
			||||||
 | 
										"memoryDomain": [[0, 1, 2, 3, 4, 5, 6, 7]],
 | 
				
			||||||
 | 
										"die": [[0, 1, 2, 3, 4, 5, 6, 7]],
 | 
				
			||||||
 | 
										"core": [[0], [1], [2], [3], [4], [5], [6], [7]],
 | 
				
			||||||
 | 
										"accelerators": []
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
							"metricDataRepository": {"kind": "test"},
 | 
				
			||||||
 | 
							"metricConfig": [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									"name": "load_one",
 | 
				
			||||||
 | 
									"unit": "load",
 | 
				
			||||||
 | 
									"scope": "node",
 | 
				
			||||||
 | 
									"timestep": 60,
 | 
				
			||||||
 | 
									"peak": 8,
 | 
				
			||||||
 | 
									"normal": 0,
 | 
				
			||||||
 | 
									"caution": 0,
 | 
				
			||||||
 | 
									"alert": 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
							"filterRanges": {
 | 
				
			||||||
 | 
								"numNodes": { "from": 1, "to": 1 },
 | 
				
			||||||
 | 
								"duration": { "from": 0, "to": 172800 },
 | 
				
			||||||
 | 
								"startTime": { "from": "2010-01-01T00:00:00Z", "to": null }
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tmpdir := t.TempDir()
 | 
				
			||||||
 | 
						jobarchive := filepath.Join(tmpdir, "job-archive")
 | 
				
			||||||
 | 
						if err := os.Mkdir(jobarchive, 0777); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(filepath.Join(jobarchive, "testcluster"), 0777); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.WriteFile(filepath.Join(jobarchive, "testcluster", "cluster.json"), []byte(testclusterJson), 0666); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dbfilepath := filepath.Join(tmpdir, "test.db")
 | 
				
			||||||
 | 
						f, err := os.Create(dbfilepath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						f.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db, err = sqlx.Open("sqlite3", fmt.Sprintf("%s?_foreign_keys=on", dbfilepath))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db.SetMaxOpenConns(1)
 | 
				
			||||||
 | 
						if _, err := db.Exec(JOBS_DB_SCHEMA); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := config.Init(db, false, programConfig.UiDefaults, jobarchive); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := metricdata.Init(jobarchive, false); err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resolver := &graph.Resolver{DB: db}
 | 
				
			||||||
 | 
						resolver.Init()
 | 
				
			||||||
 | 
						return &api.RestApi{
 | 
				
			||||||
 | 
							DB:       db,
 | 
				
			||||||
 | 
							Resolver: resolver,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cleanup() {
 | 
				
			||||||
 | 
						log.SetOutput(os.Stderr)
 | 
				
			||||||
 | 
						// TODO: Clear all caches, reset all modules, etc...
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * This function starts a job, stops it, and then reads its data from the job-archive.
 | 
				
			||||||
 | 
					 * Do not run sub-tests in parallel! Tests should not be run in parallel at all, because
 | 
				
			||||||
 | 
					 * at least `setup` modifies global state. Log-Output is redirected to /dev/null on purpose.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					func TestRestApi(t *testing.T) {
 | 
				
			||||||
 | 
						restapi := setup(t)
 | 
				
			||||||
 | 
						t.Cleanup(cleanup)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testData := schema.JobData{
 | 
				
			||||||
 | 
							"load_one": map[schema.MetricScope]*schema.JobMetric{
 | 
				
			||||||
 | 
								schema.MetricScopeNode: {
 | 
				
			||||||
 | 
									Unit:     "load",
 | 
				
			||||||
 | 
									Scope:    schema.MetricScopeNode,
 | 
				
			||||||
 | 
									Timestep: 60,
 | 
				
			||||||
 | 
									Series: []schema.Series{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											Hostname:   "testhost",
 | 
				
			||||||
 | 
											Statistics: &schema.MetricStatistics{Min: 0.1, Avg: 0.2, Max: 0.3},
 | 
				
			||||||
 | 
											Data:       []schema.Float{0.1, 0.1, 0.1, 0.2, 0.2, 0.2, 0.3, 0.3, 0.3},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metricdata.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context) (schema.JobData, error) {
 | 
				
			||||||
 | 
							return testData, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						r := mux.NewRouter()
 | 
				
			||||||
 | 
						restapi.MountRoutes(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const startJobBody string = `{
 | 
				
			||||||
 | 
							"jobId":            123,
 | 
				
			||||||
 | 
					    	"user":             "testuser",
 | 
				
			||||||
 | 
					    	"project":          "testproj",
 | 
				
			||||||
 | 
					    	"cluster":          "testcluster",
 | 
				
			||||||
 | 
					    	"partition":        "default",
 | 
				
			||||||
 | 
					    	"arrayJobId":       0,
 | 
				
			||||||
 | 
					    	"numNodes":         1,
 | 
				
			||||||
 | 
					    	"numHwthreads":     8,
 | 
				
			||||||
 | 
					    	"numAcc":           0,
 | 
				
			||||||
 | 
					    	"exclusive":        1,
 | 
				
			||||||
 | 
					    	"monitoringStatus": 1,
 | 
				
			||||||
 | 
					    	"smt":              1,
 | 
				
			||||||
 | 
					    	"tags":             [],
 | 
				
			||||||
 | 
					    	"resources": [
 | 
				
			||||||
 | 
					        	{
 | 
				
			||||||
 | 
					            	"hostname": "testhost",
 | 
				
			||||||
 | 
					            	"hwthreads": [0, 1, 2, 3, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					        	}
 | 
				
			||||||
 | 
					    	],
 | 
				
			||||||
 | 
					    	"metaData":  null,
 | 
				
			||||||
 | 
					    	"startTime": 123456789
 | 
				
			||||||
 | 
						}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var dbid int64
 | 
				
			||||||
 | 
						if ok := t.Run("StartJob", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := httptest.NewRequest(http.MethodPost, "/api/jobs/start_job/", bytes.NewBuffer([]byte(startJobBody)))
 | 
				
			||||||
 | 
							recorder := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							r.ServeHTTP(recorder, req)
 | 
				
			||||||
 | 
							response := recorder.Result()
 | 
				
			||||||
 | 
							if response.StatusCode != http.StatusCreated {
 | 
				
			||||||
 | 
								t.Fatal(response.Status, recorder.Body.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var res api.StartJobApiRespone
 | 
				
			||||||
 | 
							if err := json.Unmarshal(recorder.Body.Bytes(), &res); err != nil {
 | 
				
			||||||
 | 
								t.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							job, err := restapi.Resolver.Query().Job(context.Background(), strconv.Itoa(int(res.DBID)))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if job.JobID != 123 ||
 | 
				
			||||||
 | 
								job.User != "testuser" ||
 | 
				
			||||||
 | 
								job.Project != "testproj" ||
 | 
				
			||||||
 | 
								job.Cluster != "testcluster" ||
 | 
				
			||||||
 | 
								job.Partition != "default" ||
 | 
				
			||||||
 | 
								job.ArrayJobId != 0 ||
 | 
				
			||||||
 | 
								job.NumNodes != 1 ||
 | 
				
			||||||
 | 
								job.NumHWThreads != 8 ||
 | 
				
			||||||
 | 
								job.NumAcc != 0 ||
 | 
				
			||||||
 | 
								job.Exclusive != 1 ||
 | 
				
			||||||
 | 
								job.MonitoringStatus != 1 ||
 | 
				
			||||||
 | 
								job.SMT != 1 ||
 | 
				
			||||||
 | 
								len(job.Tags) != 0 ||
 | 
				
			||||||
 | 
								!reflect.DeepEqual(job.Resources, []*schema.Resource{{Hostname: "testhost", HWThreads: []int{0, 1, 2, 3, 4, 5, 6, 7}}}) ||
 | 
				
			||||||
 | 
								job.StartTime.Unix() != 123456789 {
 | 
				
			||||||
 | 
								t.Fatalf("unexpected job properties: %#v", job)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							dbid = res.DBID
 | 
				
			||||||
 | 
						}); !ok {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const stopJobBody string = `{
 | 
				
			||||||
 | 
							"jobId":     123,
 | 
				
			||||||
 | 
							"startTime": 123456789,
 | 
				
			||||||
 | 
							"cluster":   "testcluster",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"jobState": "completed",
 | 
				
			||||||
 | 
							"stopTime": 123457789
 | 
				
			||||||
 | 
						}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var stoppedJob *schema.Job
 | 
				
			||||||
 | 
						if ok := t.Run("StopJob", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := httptest.NewRequest(http.MethodPost, "/api/jobs/stop_job/", bytes.NewBuffer([]byte(stopJobBody)))
 | 
				
			||||||
 | 
							recorder := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							r.ServeHTTP(recorder, req)
 | 
				
			||||||
 | 
							response := recorder.Result()
 | 
				
			||||||
 | 
							if response.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
								t.Fatal(response.Status, recorder.Body.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							restapi.OngoingArchivings.Wait()
 | 
				
			||||||
 | 
							job, err := restapi.Resolver.Query().Job(context.Background(), strconv.Itoa(int(dbid)))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if job.State != schema.JobStateCompleted {
 | 
				
			||||||
 | 
								t.Fatal("expected job to be completed")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if job.Duration != (123457789 - 123456789) {
 | 
				
			||||||
 | 
								t.Fatalf("unexpected job properties: %#v", job)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							stoppedJob = job
 | 
				
			||||||
 | 
						}); !ok {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("CheckArchive", func(t *testing.T) {
 | 
				
			||||||
 | 
							data, err := metricdata.LoadData(stoppedJob, []string{"load_one"}, []schema.MetricScope{schema.MetricScopeNode}, context.Background())
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !reflect.DeepEqual(data, testData) {
 | 
				
			||||||
 | 
								t.Fatal("unexpected data fetched from archive")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -14,6 +14,7 @@ import (
 | 
				
			|||||||
type Float float64
 | 
					type Float float64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var NaN Float = Float(math.NaN())
 | 
					var NaN Float = Float(math.NaN())
 | 
				
			||||||
 | 
					var nullAsBytes []byte = []byte("null")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (f Float) IsNaN() bool {
 | 
					func (f Float) IsNaN() bool {
 | 
				
			||||||
	return math.IsNaN(float64(f))
 | 
						return math.IsNaN(float64(f))
 | 
				
			||||||
@@ -22,10 +23,10 @@ func (f Float) IsNaN() bool {
 | 
				
			|||||||
// NaN will be serialized to `null`.
 | 
					// NaN will be serialized to `null`.
 | 
				
			||||||
func (f Float) MarshalJSON() ([]byte, error) {
 | 
					func (f Float) MarshalJSON() ([]byte, error) {
 | 
				
			||||||
	if f.IsNaN() {
 | 
						if f.IsNaN() {
 | 
				
			||||||
		return []byte("null"), nil
 | 
							return nullAsBytes, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return []byte(strconv.FormatFloat(float64(f), 'f', 2, 64)), nil
 | 
						return strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 2, 64), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// `null` will be unserialized to NaN.
 | 
					// `null` will be unserialized to NaN.
 | 
				
			||||||
@@ -59,8 +60,8 @@ func (f *Float) UnmarshalGQL(v interface{}) error {
 | 
				
			|||||||
// NaN will be serialized to `null`.
 | 
					// NaN will be serialized to `null`.
 | 
				
			||||||
func (f Float) MarshalGQL(w io.Writer) {
 | 
					func (f Float) MarshalGQL(w io.Writer) {
 | 
				
			||||||
	if f.IsNaN() {
 | 
						if f.IsNaN() {
 | 
				
			||||||
		w.Write([]byte(`null`))
 | 
							w.Write(nullAsBytes)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		w.Write([]byte(strconv.FormatFloat(float64(f), 'f', 2, 64)))
 | 
							w.Write(strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 2, 64))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user