Lessons in order

When in doubt, stop...doubting Posted by Art Mills

Apr 28, 2023

And why the ChatGPT rate limit can be a good thing

I see problems with the code blocks here. Gonna have to fix that :).

I'm not much of a coder, but I enjoy playing around and learning with it. In writing a website for my youngest daughter, I am purring along, and am writing tests using the Golang standard library.

I write the following test:



// TestDiveUpdate tests the dive update functionality
func TestDiveUpdate(t *testing.T) {
	// Setup
	LoadEnv()
	db.Connect()

	// Sets the TEST_RUN env var to true for views requiring logged in user but tests that don't require a logged in user
	os.Setenv("TEST_RUN", "true")
	defer os.Setenv("TEST_RUN", "") // Reset the TEST_RUN env var

	// Create a router with the test route
	gin.SetMode(gin.TestMode)
	router := gin.Default()
	router.LoadHTMLGlob("../../templates/**/**")
	router.POST("/admin/dives/:id", controllers.DiveUpdate) // Add the DiveUpdate route
	router.GET("/admin/dives", controllers.DivesIndex)      // Add the DiveIndex route

	// Insert test data and defer cleanup
	dg1, dg2, dt1, dt2, bt1, bt2, bh1, bh2 := helpers.CreateTestData()

	// create a test dive
	dive, _ := models.DiveCreate("Test Dive", 154, 2.5, uint64(dt1.ID), uint64(dg1.ID), uint64(bt1.ID), uint64(bh1.ID))

	// use form to create updated dive
	data := url.Values{}
	data.Set("name", "Test Dive Updated")
	data.Set("number", "155")
	data.Set("difficulty", "2.6")
	data.Set("divegroup_id", fmt.Sprintf("%d", dg2.ID))
	data.Set("divetype_id", fmt.Sprintf("%d", dt2.ID))
	data.Set("boardtype_id", fmt.Sprintf("%d", bt2.ID))
	data.Set("boardheight_id", fmt.Sprintf("%d", bh2.ID))

	// Create a request and submit it to the router
	req, err := http.NewRequest(http.MethodPost, "/admin/dives/"+helpers.UintToString(dive.ID), strings.NewReader(data.Encode()))
	if err != nil {
		t.Fatalf("Failed to create a new request: %v", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	// Create a response recorder
	w := httptest.NewRecorder()

	// Submit the request
	router.ServeHTTP(w, req)

	// Check the status code
	if status := w.Code; status != http.StatusFound {
		t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
	}

	// Check the updated dive
	updatedDive, _ := models.DiveShow(uint64(dive.ID))
	if updatedDive.Name != "Test Dive Updated" {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.Name, "Test Dive Updated")
	}
	if updatedDive.Number != 155 {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.Number, 155)
	}
	if updatedDive.Difficulty != 2.6 {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.Difficulty, 2.6)
	}
	if updatedDive.DiveTypeID != uint64(dt2.ID) {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.DiveTypeID, dt2.ID)
	}

	if updatedDive.DiveGroupID != uint64(dg2.ID) {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.DiveGroupID, dg2.ID)
	}
	if updatedDive.BoardTypeID != uint64(bt2.ID) {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.BoardTypeID, bt2.ID)
	}
	if updatedDive.BoardHeightID != uint64(bh2.ID) {
		t.Errorf("handler returned unexpected body: got %v want %v", updatedDive.BoardHeightID, bh2.ID)
	}

	// Cleanup
	db.Database.Unscoped().Delete(&updatedDive)

	// Clean up test data
	helpers.CleanTestData(dg1, dg2, dt1, dt2, bt1, bt2, bh1, bh2)
}

Yes, I am cheating the test by using a TEST_RUN variable to allow these tests to simply run without creating a session each time. Sue me.

Moving beyond that, the test is fine.

It's an update test of one of my models that has four associated models.

But the damn thing doesn't work:



2023/04/28 15:21:46 D:/code/splattastic/models/dive_group.go:24 SLOW SQL >= 200ms
[259.904ms] [rows:1] INSERT INTO "dive_groups" ("created_at","updated_at","deleted_at","name") VALUES ('2023-04-28 15:21:46.592','2023-04-28 15:21:46.592',NULL,'Test Dive Group 1') RETURNING "id"2023/04/28 15:21:46 Dive group created: {{189 2023-04-28 15:21:46.5926146 -0500 CDT 2023-04-28 15:21:46.5926146 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Dive Group 1}
2023/04/28 15:21:46 Dive group created: {{190 2023-04-28 15:21:46.7513964 -0500 CDT 2023-04-28 15:21:46.7513964 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Dive Group 2}
2023/04/28 15:21:46 Dive type created: {{203 2023-04-28 15:21:46.8683983 -0500 CDT 2023-04-28 15:21:46.8683983 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Dive Type 1 Q}
2023/04/28 15:21:47 Dive type created: {{204 2023-04-28 15:21:47.0246299 -0500 CDT 2023-04-28 15:21:47.0246299 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Dive Type 2 R}
2023/04/28 15:21:47 Board type created: {{187 2023-04-28 15:21:47.143395 -0500 CDT 2023-04-28 15:21:47.143395 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Board Type 1}
2023/04/28 15:21:47 Board type created: {{188 2023-04-28 15:21:47.305703 -0500 CDT 2023-04-28 15:21:47.305703 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} Test Board Type 2}
2023/04/28 15:21:47 Board height created: {{184 2023-04-28 15:21:47.4239049 -0500 CDT 2023-04-28 15:21:47.4239049 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} 8.5}
2023/04/28 15:21:47 Board height created: {{185 2023-04-28 15:21:47.5980298 -0500 CDT 2023-04-28 15:21:47.5980298 -0500 CDT {0001-01-01 00:00:00 +0000 UTC false}} 6.5}

2023/04/28 15:21:48 D:/code/splattastic/models/dive.go:87 SLOW SQL >= 200ms
[378.266ms] [rows:1] SELECT * FROM "dives" WHERE "dives"."id" = 78 AND "dives"."deleted_at" IS NULL ORDER BY "dives"."id" LIMIT 1
2023/04/28 15:21:48 divetypeID: 204
2023/04/28 15:21:48 divegroupID: 190
2023/04/28 15:21:48 boardtypeID: 188
2023/04/28 15:21:48 boardheightID: 185

2023/04/28 15:21:48 D:/code/splattastic/models/dive_type.go:48 record not found
[40.500ms] [rows:0] SELECT * FROM "dive_types" WHERE "dive_types"."id" = 190 AND "dive_types"."deleted_at" IS NULL ORDER BY "dive_types"."id" LIMIT 1
2023/04/28 15:21:48 Error getting divetype by id: dive type not found
2023/04/28 15:21:48 Error updating dive: Error getting divetype by id
[GIN] 2023/04/28 - 15:21:48 | 500 |    419.1081ms |                 | POST     "/admin/dives/78"
Response body: {"error":"Error getting divetype by id"}
    dive_update_test.go:72: handler returned wrong status code: got 500 want 302
    dive_update_test.go:78: handler returned unexpected body: got Test Dive want Test Dive Updated
Updated dive: &{Model:{ID:78 CreatedAt:2023-04-28 15:21:48.021533 -0500 CDT UpdatedAt:2023-04-28 15:21:48.021533 -0500 CDT DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Test Dive Number:154 Difficulty:2.5 DiveTypeID:203 DiveType:{Model:{ID:203 CreatedAt:2023-04-28 15:21:46.868398 -0500 CDT UpdatedAt:2023-04-28 15:21:46.868398 -0500 CDT DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Test Dive Type 1 Letter:Q} DiveGroupID:189 DiveGroup:{Model:{ID:189 CreatedAt:2023-04-28 15:21:46.592614 -0500 CDT UpdatedAt:2023-04-28 15:21:46.592614 -0500 CDT DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Test Dive Group 1} BoardTypeID:187 BoardType:{Model:{ID:187 CreatedAt:2023-04-28 15:21:47.143395 -0500 CDT UpdatedAt:2023-04-28 15:21:47.143395 -0500 CDT DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:Test Board Type 1} BoardHeightID:184 BoardHeight:{Model:{ID:184 CreatedAt:2023-04-28 15:21:47.423904 -0500 
CDT UpdatedAt:2023-04-28 15:21:47.423904 -0500 CDT DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Height:8.5}}
--- FAIL: TestDiveUpdate (3.88s)
FAIL
FAIL    github.com/hail2skins/splattastic/controllers/tests     4.718s
FAIL

I'm quizzing GPT-4 about it and it is really pretty smart. But here it's meandering about being unhelpful right up until I hit my 25 questions over 3 hours when it went away.

Once it went away I realized something miraculous.

I still have eyes

And they work.

Look closely in that error and you see the test data is created. The error says it can't find "divetype" by the ID of 190. But 190 is the divegroup ID. Why is it looking for the ID of divegroup when divetype is created and sitting there waiting to be used?

Here's my controller:



// DiveUpdate updates a dive
func DiveUpdate(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		log.Printf("Error converting id to uint64: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}

	name := c.PostForm("name")
	if name == "" {
		log.Printf("Error updating dive: name is empty")
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid name"})
		return
	}
	numberStr := c.PostForm("number")
	number, err := strconv.Atoi(numberStr)
	if err != nil {
		log.Printf("Error converting number to int: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid number"})
		return
	}
	difficultyStr := c.PostForm("difficulty")
	difficulty, err := strconv.ParseFloat(difficultyStr, 32)
	if err != nil {
		log.Printf("Error converting difficulty to float32: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid difficulty"})
		return
	}

	diveTypeIDStr := c.PostForm("divetype_id")
	diveTypeID, err := strconv.ParseUint(diveTypeIDStr, 10, 64)
	if err != nil {
		log.Printf("Error converting divetype_id to uint64: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid divetype_id"})
		return
	}

	diveGroupIDStr := c.PostForm("divegroup_id")
	diveGroupID, err := strconv.ParseUint(diveGroupIDStr, 10, 64)
	if err != nil {
		log.Printf("Error converting dive_group_id to uint64: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid divegroup_id"})
		return
	}
	boardTypeIDStr := c.PostForm("boardtype_id")
	boardTypeID, err := strconv.ParseUint(boardTypeIDStr, 10, 64)
	if err != nil {
		log.Printf("Error converting board_type_id to uint64: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid boardtype_id"})
		return
	}
	boardHeightIDStr := c.PostForm("boardheight_id")
	boardHeightID, err := strconv.ParseUint(boardHeightIDStr, 10, 64)
	if err != nil {
		log.Printf("Error converting board_height_id to uint64: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid boardheight_id"})
		return
	}

	dive, err := models.DiveShow(id)
	if err != nil {
		log.Printf("Error fetching dive: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	err = dive.Update(name, number, float32(difficulty), diveGroupID, diveTypeID, boardTypeID, boardHeightID)
	if err != nil {
		log.Printf("Error updating dive: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.Redirect(http.StatusFound, "/admin/dives")

}

Look really closely here:
err = dive.Update(name, number, float32(difficulty), diveGroupID, diveTypeID, boardTypeID, boardHeightID)



Eureka.

Order matters.

Change it to this...
```Go
	err = dive.Update(name, number, float32(difficulty), diveTypeID, diveGroupID, boardTypeID, boardHeightID)

And we're running again.

Why?



// DiveUpdate updates a dive
func (dive *Dive) Update(name string, number int, difficulty float32, divetypeID uint64, divegroupID uint64, boardtypeID uint64, boardheightID uint64) error {
	// Check if the associated records exist
	_, err := DiveTypeShow(divetypeID)
	if err != nil {
		log.Printf("Error getting divetype by id: %v", err)
		return errors.New("Error getting divetype by id")
	}
	_, err = DiveGroupShow(divegroupID)
	if err != nil {
		log.Printf("Error getting divegroup by id: %v", err)
		return errors.New("Error getting divegroup by id")
	}

	_, err = BoardTypeShow(boardtypeID)
	if err != nil {
		log.Printf("Error getting boardtype by id: %v", err)
		return errors.New("Error getting boardtype by id")
	}
	_, err = BoardHeightShow(boardheightID)
	if err != nil {
		log.Printf("Error getting boardheight by id: %v", err)
		return errors.New("Error getting boardheight by id")
	}

	// Update dive fields
	dive.Name = name
	dive.Number = number
	dive.Difficulty = difficulty
	dive.DiveTypeID = divetypeID
	dive.DiveGroupID = divegroupID
	dive.BoardTypeID = boardtypeID
	dive.BoardHeightID = boardheightID

	// Save updated dive to the database
	err = db.Database.Model(&dive).Updates(Dive{
		Name:          name,
		Number:        number,
		Difficulty:    difficulty,
		DiveTypeID:    divetypeID,
		DiveGroupID:   divegroupID,
		BoardTypeID:   boardtypeID,
		BoardHeightID: boardheightID,
	}).Error
	if err != nil {
		log.Printf("Error updating dive: %v", err)
		return errors.New("Error updating dive")
	}

	return nil
}

The model demands attention :).

This is my first pretty complex model interaction with this language. Intuitively it does make sense that order matters, but I really wouldn't have thought about this at all but for GPT-4 telling me to stop answering questions. I've been using it to quickly get me passed bottlenecks.

But old fashioned human power still has a place.

For now.