CodeBench: Go-tuki — Chi + SQLite + httptest
- Golden example: todo-go/ (6/6 testit läpi) - todo-go.md golden reference - prompts/code-go.md koodigenerointi-prompti - Dockerfile.go-test (golang:1.23-alpine) - benchmark.mjs: LANG_CONFIG, parseTestOutput, prompt/golden-valinta Go:lle - Käyttö: node benchmark.mjs --lang go --models qwen2.5-coder:32b
This commit is contained in:
4
kipina-codebench/Dockerfile.go-test
Normal file
4
kipina-codebench/Dockerfile.go-test
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM golang:1.23-alpine
|
||||||
|
RUN apk add --no-cache gcc musl-dev
|
||||||
|
WORKDIR /work
|
||||||
|
ENTRYPOINT ["sh", "-c", "cp -r /src/* . && go test -v -count=1 ./... 2>&1"]
|
||||||
@@ -34,7 +34,7 @@ const OUTPUT_DIR = arg('output', `/tmp/kipina-benchmark/${TIMESTAMP}`);
|
|||||||
const RESULTS_DIR = join(__dirname, 'results');
|
const RESULTS_DIR = join(__dirname, 'results');
|
||||||
const THINK_MODE = args.includes('--think');
|
const THINK_MODE = args.includes('--think');
|
||||||
const COMPACT_MODE = args.includes('--compact');
|
const COMPACT_MODE = args.includes('--compact');
|
||||||
const LANG = arg('lang', 'python'); // python | rust
|
const LANG = arg('lang', 'python'); // python | rust | go
|
||||||
const ROUNDS = parseInt(arg('rounds', '1')); // 1-10 toistoa
|
const ROUNDS = parseInt(arg('rounds', '1')); // 1-10 toistoa
|
||||||
const MAX_FIX_ROUNDS = 2;
|
const MAX_FIX_ROUNDS = 2;
|
||||||
|
|
||||||
@@ -53,11 +53,12 @@ const PROFILES = JSON.parse(readFileSync(join(__dirname, 'profiles.json'), 'utf-
|
|||||||
function getGoldenForModel(model) {
|
function getGoldenForModel(model) {
|
||||||
const modelConf = PROFILES.models[model];
|
const modelConf = PROFILES.models[model];
|
||||||
const goldenFile = modelConf?.golden || 'todo.md';
|
const goldenFile = modelConf?.golden || 'todo.md';
|
||||||
// Rust-kielelle vaihda .md → -rs.md (todo.md → todo-rs.md)
|
// Kielispesifi golden: todo.md → todo-rs.md / todo-go.md
|
||||||
if (LANG === 'rust') {
|
const langSuffix = { rust: '-rs', go: '-go' }[LANG];
|
||||||
const rsFile = goldenFile.replace(/\.md$/, '-rs.md');
|
if (langSuffix) {
|
||||||
const rsPath = join(GOLDEN_DIR, rsFile);
|
const langFile = goldenFile.replace(/\.md$/, `${langSuffix}.md`);
|
||||||
if (existsSync(rsPath)) return rsFile;
|
const langPath = join(GOLDEN_DIR, langFile);
|
||||||
|
if (existsSync(langPath)) return langFile;
|
||||||
}
|
}
|
||||||
return goldenFile;
|
return goldenFile;
|
||||||
}
|
}
|
||||||
@@ -66,8 +67,8 @@ function getCodePromptForModel(model) {
|
|||||||
const modelConf = PROFILES.models[model];
|
const modelConf = PROFILES.models[model];
|
||||||
const profile = modelConf?.profile || PROFILES.default_profile;
|
const profile = modelConf?.profile || PROFILES.default_profile;
|
||||||
const promptName = modelConf?.prompt || PROFILES.profiles[profile]?.prompt || 'code';
|
const promptName = modelConf?.prompt || PROFILES.profiles[profile]?.prompt || 'code';
|
||||||
const suffix = LANG === 'rust' ? '-rs' : '';
|
const suffix = { rust: '-rs', go: '-go' }[LANG] || '';
|
||||||
// Yritä kielispesifistä ensin (code-small-rs), sitten perus (code-small)
|
// Yritä kielispesifistä ensin (code-small-rs, code-go), sitten perus (code-small)
|
||||||
const candidates = [`${promptName}${suffix}`, promptName, `code${suffix}`, 'code'];
|
const candidates = [`${promptName}${suffix}`, promptName, `code${suffix}`, 'code'];
|
||||||
for (const name of candidates) {
|
for (const name of candidates) {
|
||||||
const path = join(__dirname, 'prompts', `${name}.md`);
|
const path = join(__dirname, 'prompts', `${name}.md`);
|
||||||
@@ -91,18 +92,24 @@ const LANG_CONFIG = {
|
|||||||
required: ['Cargo.toml', 'src/models.rs', 'src/handlers.rs', 'src/lib.rs', 'src/main.rs', 'tests/api_test.rs'],
|
required: ['Cargo.toml', 'src/models.rs', 'src/handlers.rs', 'src/lib.rs', 'src/main.rs', 'tests/api_test.rs'],
|
||||||
dockerImage: 'kipina-cargo-test',
|
dockerImage: 'kipina-cargo-test',
|
||||||
},
|
},
|
||||||
|
go: {
|
||||||
|
goldenDir: 'todo-go',
|
||||||
|
files: ['go.mod', 'models.go', 'handlers.go', 'main.go', 'handlers_test.go'],
|
||||||
|
required: ['go.mod', 'models.go', 'handlers.go', 'main.go', 'handlers_test.go'],
|
||||||
|
dockerImage: 'kipina-go-test',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const LCONF = LANG_CONFIG[LANG] || LANG_CONFIG.python;
|
const LCONF = LANG_CONFIG[LANG] || LANG_CONFIG.python;
|
||||||
|
|
||||||
function loadGoldenExample(model) {
|
function loadGoldenExample(model) {
|
||||||
// --compact: käytä tiivistettyä templaattia
|
// --compact: käytä tiivistettyä templaattia
|
||||||
if (COMPACT_MODE) {
|
if (COMPACT_MODE) {
|
||||||
const compactFile = LANG === 'rust' ? 'golden-compact-rs.md' : 'golden-compact-py.md';
|
const compactFile = { rust: 'golden-compact-rs.md', go: 'golden-compact-go.md' }[LANG] || 'golden-compact-py.md';
|
||||||
const compactPath = join(__dirname, 'prompts', compactFile);
|
const compactPath = join(__dirname, 'prompts', compactFile);
|
||||||
if (existsSync(compactPath)) return '\n' + readFileSync(compactPath, 'utf-8').trim() + '\n';
|
if (existsSync(compactPath)) return '\n' + readFileSync(compactPath, 'utf-8').trim() + '\n';
|
||||||
}
|
}
|
||||||
// Mallikohtainen golden example profiilista
|
// Mallikohtainen golden example profiilista
|
||||||
const goldenFile = model ? getGoldenForModel(model) : (LANG === 'rust' ? 'todo-rs.md' : 'todo.md');
|
const goldenFile = model ? getGoldenForModel(model) : ({ rust: 'todo-rs.md', go: 'todo-go.md' }[LANG] || 'todo.md');
|
||||||
const mdPath = join(GOLDEN_DIR, goldenFile);
|
const mdPath = join(GOLDEN_DIR, goldenFile);
|
||||||
if (existsSync(mdPath)) return '\n' + readFileSync(mdPath, 'utf-8').trim() + '\n';
|
if (existsSync(mdPath)) return '\n' + readFileSync(mdPath, 'utf-8').trim() + '\n';
|
||||||
// Fallback: erilliset tiedostot
|
// Fallback: erilliset tiedostot
|
||||||
@@ -201,7 +208,13 @@ function parseTestOutput(output) {
|
|||||||
const failed = parseInt(cargoMatch[2]);
|
const failed = parseInt(cargoMatch[2]);
|
||||||
return { testsPassed: passed, testsFailed: failed, testsTotal: passed + failed };
|
return { testsPassed: passed, testsFailed: failed, testsTotal: passed + failed };
|
||||||
}
|
}
|
||||||
// Cargo compilation error: count "error[E" occurrences
|
// Go test: "--- PASS:" / "--- FAIL:"
|
||||||
|
const goPassed = (output.match(/--- PASS:/g) || []).length;
|
||||||
|
const goFailed = (output.match(/--- FAIL:/g) || []).length;
|
||||||
|
if (goPassed + goFailed > 0) {
|
||||||
|
return { testsPassed: goPassed, testsFailed: goFailed, testsTotal: goPassed + goFailed };
|
||||||
|
}
|
||||||
|
// Cargo/Go compilation error: count "error[E" or Go compile errors
|
||||||
const compileErrors = (output.match(/error\[E\d+\]/g) || []).length;
|
const compileErrors = (output.match(/error\[E\d+\]/g) || []).length;
|
||||||
if (compileErrors > 0) {
|
if (compileErrors > 0) {
|
||||||
return { testsPassed: 0, testsFailed: compileErrors, testsTotal: compileErrors };
|
return { testsPassed: 0, testsFailed: compileErrors, testsTotal: compileErrors };
|
||||||
@@ -333,7 +346,7 @@ async function runPipeline(model, scenario) {
|
|||||||
// 3. LLM-koodigenerointi
|
// 3. LLM-koodigenerointi
|
||||||
const fileCount = LCONF.required.length;
|
const fileCount = LCONF.required.length;
|
||||||
const goldenExample = loadGoldenExample(model);
|
const goldenExample = loadGoldenExample(model);
|
||||||
const codeTokens = LANG === 'rust' ? 12288 : 8192;
|
const codeTokens = LANG === 'rust' ? 12288 : LANG === 'go' ? 10240 : 8192;
|
||||||
let files;
|
let files;
|
||||||
|
|
||||||
// Orkestrointi: pilko entiteetti kerrallaan pienille malleille
|
// Orkestrointi: pilko entiteetti kerrallaan pienille malleille
|
||||||
@@ -454,7 +467,7 @@ async function runPipeline(model, scenario) {
|
|||||||
result.fixRounds = fixRound;
|
result.fixRounds = fixRound;
|
||||||
|
|
||||||
// 5. Testit Docker-kontissa + itsekorjaava looppi (Taso 4)
|
// 5. Testit Docker-kontissa + itsekorjaava looppi (Taso 4)
|
||||||
const testLabel = LANG === 'rust' ? 'Cargo test' : 'Pytest';
|
const testLabel = { rust: 'Cargo test', go: 'Go test', python: 'Pytest' }[LANG] || 'Test';
|
||||||
const dockerTimeout = LANG === 'rust' ? 300000 : 120000;
|
const dockerTimeout = LANG === 'rust' ? 300000 : 120000;
|
||||||
const MAX_TEST_FIX = 3;
|
const MAX_TEST_FIX = 3;
|
||||||
let bestFiles = { ...files }; // Paras versio tiedostoista
|
let bestFiles = { ...files }; // Paras versio tiedostoista
|
||||||
@@ -538,13 +551,13 @@ async function runPipeline(model, scenario) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Itsekorjaus: syötä virhe + koodi mallille
|
// Itsekorjaus: syötä virhe + koodi mallille
|
||||||
const errorLines = testOut.split('\n').filter(l => /^E |FAILED|ERROR|error\[E/.test(l)).slice(0, 20).join('\n');
|
const errorLines = testOut.split('\n').filter(l => /^E |FAILED|ERROR|error\[E|--- FAIL|panic:|\.go:\d+/.test(l)).slice(0, 20).join('\n');
|
||||||
if (!errorLines) break; // Ei parsittavia virheitä
|
if (!errorLines) break; // Ei parsittavia virheitä
|
||||||
|
|
||||||
console.log(` [5/5] Itsekorjaus: ${result.testsFailed || 'virhe'}...`);
|
console.log(` [5/5] Itsekorjaus: ${result.testsFailed || 'virhe'}...`);
|
||||||
const allCode = Object.entries(files).map(([fn, c]) => `=== ${fn} ===\n${c}`).join('\n\n');
|
const allCode = Object.entries(files).map(([fn, c]) => `=== ${fn} ===\n${c}`).join('\n\n');
|
||||||
const fixPrompt = `The following test errors occurred. Fix the code so ALL tests pass. Return ALL files with === markers.\n\nERRORS:\n${errorLines}\n\nCURRENT CODE:\n${allCode}`;
|
const fixPrompt = `The following test errors occurred. Fix the code so ALL tests pass. Return ALL files with === markers.\n\nERRORS:\n${errorLines}\n\nCURRENT CODE:\n${allCode}`;
|
||||||
const fixResp = await ollamaChat(model, fixPrompt, CODE_SYSTEM, LANG === 'rust' ? 12288 : 8192);
|
const fixResp = await ollamaChat(model, fixPrompt, CODE_SYSTEM, LANG === 'rust' ? 12288 : LANG === 'go' ? 10240 : 8192);
|
||||||
timings.push(fixResp);
|
timings.push(fixResp);
|
||||||
|
|
||||||
const fixedFiles = parseGeneratedFiles(fixResp.text);
|
const fixedFiles = parseGeneratedFiles(fixResp.text);
|
||||||
|
|||||||
466
kipina-codebench/golden-examples/todo-go.md
Normal file
466
kipina-codebench/golden-examples/todo-go.md
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
# Todo — reference implementation (Go + Chi + SQLite)
|
||||||
|
|
||||||
|
This is a complete example. Generate equivalent structure for the given project.
|
||||||
|
Use ONLY the fields from the JSON spec — do not add extras.
|
||||||
|
|
||||||
|
## go.mod
|
||||||
|
|
||||||
|
Chi v5 router, modernc.org/sqlite (pure Go, no CGO).
|
||||||
|
|
||||||
|
```
|
||||||
|
module todo-go
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
|
modernc.org/sqlite v1.37.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
modernc.org/libc v1.65.7 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## models.go
|
||||||
|
|
||||||
|
Data structs: Todo (full row), CreateTodo (POST), UpdateTodo (PUT, all fields optional pointers).
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Todo represents a task with priority and status tracking.
|
||||||
|
type Todo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority int64 `json:"priority"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTodo is the request body for creating a new todo.
|
||||||
|
type CreateTodo struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority *int64 `json:"priority,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTodo is the request body for updating an existing todo.
|
||||||
|
type UpdateTodo struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority *int64 `json:"priority,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## handlers.go
|
||||||
|
|
||||||
|
CRUD handlers as closures taking *sql.DB. INSERT/UPDATE use RETURNING, sql.ErrNoRows for 404.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var input CreateTodo
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
priority := int64(1)
|
||||||
|
if input.Priority != nil {
|
||||||
|
priority = *input.Priority
|
||||||
|
}
|
||||||
|
status := "pending"
|
||||||
|
if input.Status != nil {
|
||||||
|
status = *input.Status
|
||||||
|
}
|
||||||
|
var todo Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
`INSERT INTO todos (title, description, due_date, priority, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
RETURNING id, title, description, due_date, priority, status`,
|
||||||
|
input.Title, input.Description, input.DueDate, priority, status,
|
||||||
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTodos(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var todos []Todo
|
||||||
|
for rows.Next() {
|
||||||
|
var t Todo
|
||||||
|
if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
todos = append(todos, t)
|
||||||
|
}
|
||||||
|
if todos == nil {
|
||||||
|
todos = []Todo{}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(todos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
var todo Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
||||||
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
var existing Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
||||||
|
).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var input UpdateTodo
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if input.Title != nil {
|
||||||
|
existing.Title = *input.Title
|
||||||
|
}
|
||||||
|
if input.Description != nil {
|
||||||
|
existing.Description = input.Description
|
||||||
|
}
|
||||||
|
if input.DueDate != nil {
|
||||||
|
existing.DueDate = input.DueDate
|
||||||
|
}
|
||||||
|
if input.Priority != nil {
|
||||||
|
existing.Priority = *input.Priority
|
||||||
|
}
|
||||||
|
if input.Status != nil {
|
||||||
|
existing.Status = *input.Status
|
||||||
|
}
|
||||||
|
var updated Todo
|
||||||
|
err = db.QueryRow(
|
||||||
|
`UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ?
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING id, title, description, due_date, priority, status`,
|
||||||
|
existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id,
|
||||||
|
).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
result, err := db.Exec("DELETE FROM todos WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, _ := result.RowsAffected()
|
||||||
|
if rows == 0 {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## main.go
|
||||||
|
|
||||||
|
Entry point: SQLite connection, table init, Chi router on port 3000.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitDB creates tables if they don't exist.
|
||||||
|
func InitDB(db *sql.DB) {
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
due_date TEXT,
|
||||||
|
priority INTEGER NOT NULL DEFAULT 1,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a chi router with all routes.
|
||||||
|
func NewRouter(db *sql.DB) http.Handler {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Post("/todos", createTodo(db))
|
||||||
|
r.Get("/todos", listTodos(db))
|
||||||
|
r.Get("/todos/{id}", getTodo(db))
|
||||||
|
r.Put("/todos/{id}", updateTodo(db))
|
||||||
|
r.Delete("/todos/{id}", deleteTodo(db))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := sql.Open("sqlite", "file:app.db?mode=rwc")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
InitDB(db)
|
||||||
|
|
||||||
|
fmt.Println("Server running: http://127.0.0.1:3000")
|
||||||
|
log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## handlers_test.go
|
||||||
|
|
||||||
|
Integration tests: setupTestServer with httptest.NewServer + :memory: SQLite, unique data per test.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
InitDB(db)
|
||||||
|
return httptest.NewServer(NewRouter(db)), db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, err := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Buy groceries","priority":2}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["title"] != "Buy groceries" {
|
||||||
|
t.Fatalf("expected title 'Buy groceries', got %v", body["title"])
|
||||||
|
}
|
||||||
|
if body["id"] == nil {
|
||||||
|
t.Fatal("expected id to be present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTodos(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Listable task"}`))
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/todos")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body []map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if len(body) < 1 {
|
||||||
|
t.Fatal("expected at least 1 todo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTodoByID(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Fetchable task"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
resp, err := http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["id"] != id {
|
||||||
|
t.Fatalf("expected id %.0f, got %v", id, body["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTodoNotFound(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/todos/99999")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Old title"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
req, _ := http.NewRequest(http.MethodPut, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id),
|
||||||
|
strings.NewReader(`{"title":"New title"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["title"] != "New title" {
|
||||||
|
t.Fatalf("expected 'New title', got %v", body["title"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Deletable task"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
t.Fatalf("expected 204, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected 404 after delete, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
kipina-codebench/golden-examples/todo-go/go.mod
Normal file
23
kipina-codebench/golden-examples/todo-go/go.mod
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module todo-go
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
|
modernc.org/sqlite v1.37.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
modernc.org/libc v1.65.7 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
49
kipina-codebench/golden-examples/todo-go/go.sum
Normal file
49
kipina-codebench/golden-examples/todo-go/go.sum
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
|
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||||
|
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||||
|
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||||
|
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
155
kipina-codebench/golden-examples/todo-go/handlers.go
Normal file
155
kipina-codebench/golden-examples/todo-go/handlers.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var input CreateTodo
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
priority := int64(1)
|
||||||
|
if input.Priority != nil {
|
||||||
|
priority = *input.Priority
|
||||||
|
}
|
||||||
|
status := "pending"
|
||||||
|
if input.Status != nil {
|
||||||
|
status = *input.Status
|
||||||
|
}
|
||||||
|
var todo Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
`INSERT INTO todos (title, description, due_date, priority, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
RETURNING id, title, description, due_date, priority, status`,
|
||||||
|
input.Title, input.Description, input.DueDate, priority, status,
|
||||||
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTodos(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var todos []Todo
|
||||||
|
for rows.Next() {
|
||||||
|
var t Todo
|
||||||
|
if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
todos = append(todos, t)
|
||||||
|
}
|
||||||
|
if todos == nil {
|
||||||
|
todos = []Todo{}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(todos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
var todo Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
||||||
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
var existing Todo
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
||||||
|
).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var input UpdateTodo
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if input.Title != nil {
|
||||||
|
existing.Title = *input.Title
|
||||||
|
}
|
||||||
|
if input.Description != nil {
|
||||||
|
existing.Description = input.Description
|
||||||
|
}
|
||||||
|
if input.DueDate != nil {
|
||||||
|
existing.DueDate = input.DueDate
|
||||||
|
}
|
||||||
|
if input.Priority != nil {
|
||||||
|
existing.Priority = *input.Priority
|
||||||
|
}
|
||||||
|
if input.Status != nil {
|
||||||
|
existing.Status = *input.Status
|
||||||
|
}
|
||||||
|
var updated Todo
|
||||||
|
err = db.QueryRow(
|
||||||
|
`UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ?
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING id, title, description, due_date, priority, status`,
|
||||||
|
existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id,
|
||||||
|
).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteTodo(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
result, err := db.Exec("DELETE FROM todos WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, _ := result.RowsAffected()
|
||||||
|
if rows == 0 {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
171
kipina-codebench/golden-examples/todo-go/handlers_test.go
Normal file
171
kipina-codebench/golden-examples/todo-go/handlers_test.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
InitDB(db)
|
||||||
|
return httptest.NewServer(NewRouter(db)), db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, err := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Buy groceries","priority":2}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["title"] != "Buy groceries" {
|
||||||
|
t.Fatalf("expected title 'Buy groceries', got %v", body["title"])
|
||||||
|
}
|
||||||
|
if body["id"] == nil {
|
||||||
|
t.Fatal("expected id to be present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTodos(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Listable task"}`))
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/todos")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body []map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if len(body) < 1 {
|
||||||
|
t.Fatal("expected at least 1 todo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTodoByID(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Fetchable task"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
resp, err := http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["id"] != id {
|
||||||
|
t.Fatalf("expected id %.0f, got %v", id, body["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTodoNotFound(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/todos/99999")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Old title"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
req, _ := http.NewRequest(http.MethodPut, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id),
|
||||||
|
strings.NewReader(`{"title":"New title"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if body["title"] != "New title" {
|
||||||
|
t.Fatalf("expected 'New title', got %v", body["title"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteTodo(t *testing.T) {
|
||||||
|
ts, db := setupTestServer(t)
|
||||||
|
defer ts.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
||||||
|
strings.NewReader(`{"title":"Deletable task"}`))
|
||||||
|
var created map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&created)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
id := created["id"].(float64)
|
||||||
|
req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
t.Fatalf("expected 204, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected 404 after delete, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
kipina-codebench/golden-examples/todo-go/main.go
Normal file
49
kipina-codebench/golden-examples/todo-go/main.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitDB creates tables if they don't exist.
|
||||||
|
func InitDB(db *sql.DB) {
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
due_date TEXT,
|
||||||
|
priority INTEGER NOT NULL DEFAULT 1,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a chi router with all routes.
|
||||||
|
func NewRouter(db *sql.DB) http.Handler {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Post("/todos", createTodo(db))
|
||||||
|
r.Get("/todos", listTodos(db))
|
||||||
|
r.Get("/todos/{id}", getTodo(db))
|
||||||
|
r.Put("/todos/{id}", updateTodo(db))
|
||||||
|
r.Delete("/todos/{id}", deleteTodo(db))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := sql.Open("sqlite", "file:app.db?mode=rwc")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
InitDB(db)
|
||||||
|
|
||||||
|
fmt.Println("Server running: http://127.0.0.1:3000")
|
||||||
|
log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db)))
|
||||||
|
}
|
||||||
29
kipina-codebench/golden-examples/todo-go/models.go
Normal file
29
kipina-codebench/golden-examples/todo-go/models.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Todo represents a task with priority and status tracking.
|
||||||
|
type Todo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority int64 `json:"priority"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTodo is the request body for creating a new todo.
|
||||||
|
type CreateTodo struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority *int64 `json:"priority,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTodo is the request body for updating an existing todo.
|
||||||
|
type UpdateTodo struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
Priority *int64 `json:"priority,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
69
kipina-codebench/prompts/code-go.md
Normal file
69
kipina-codebench/prompts/code-go.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
You are a Go backend developer. Generate a Chi web project with SQLite.
|
||||||
|
|
||||||
|
Given the project requirements, JSON specification, and a REFERENCE IMPLEMENTATION, generate these files:
|
||||||
|
|
||||||
|
1. go.mod — module declaration, go-chi/chi/v5, modernc.org/sqlite
|
||||||
|
2. models.go — Structs with json tags
|
||||||
|
3. handlers.go — Handler closures for each CRUD endpoint
|
||||||
|
4. main.go — Entry point with InitDB(), NewRouter(), main()
|
||||||
|
5. handlers_test.go — Integration tests using httptest against in-memory SQLite
|
||||||
|
|
||||||
|
Do NOT generate any other files. Do NOT generate go.sum.
|
||||||
|
|
||||||
|
OUTPUT FORMAT — use these exact markers to separate files:
|
||||||
|
|
||||||
|
=== go.mod ===
|
||||||
|
<module content>
|
||||||
|
|
||||||
|
=== models.go ===
|
||||||
|
<go code>
|
||||||
|
|
||||||
|
=== handlers.go ===
|
||||||
|
<go code>
|
||||||
|
|
||||||
|
=== main.go ===
|
||||||
|
<go code>
|
||||||
|
|
||||||
|
=== handlers_test.go ===
|
||||||
|
<go code>
|
||||||
|
|
||||||
|
DOCUMENTATION — structs get // one-line comments. Keep it brief.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Follow the REFERENCE IMPLEMENTATION patterns exactly
|
||||||
|
- Chi router with chi.URLParam(r, "param") for path parameters
|
||||||
|
- database/sql + modernc.org/sqlite (pure Go driver, no CGO required)
|
||||||
|
- Import the driver as blank import: _ "modernc.org/sqlite"
|
||||||
|
- Handlers are closures: func handler(db *sql.DB) http.HandlerFunc
|
||||||
|
- INSERT/UPDATE queries use RETURNING clause to get the row back via QueryRow + Scan
|
||||||
|
- POST returns 201 (http.StatusCreated), DELETE returns 204 (http.StatusNoContent), GET missing returns 404
|
||||||
|
- Use sql.ErrNoRows for not-found checks: if err == sql.ErrNoRows { ... }
|
||||||
|
- No compile-time query macros — use db.QueryRow(), db.Query(), db.Exec() directly
|
||||||
|
- Empty slice not nil for list endpoints: if items == nil { items = []Item{} }
|
||||||
|
- Optional fields use pointer types (*string, *int64) with json tag omitempty
|
||||||
|
- Set Content-Type header: w.Header().Set("Content-Type", "application/json")
|
||||||
|
- Parse path ID with strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
- InitDB uses log.Fatal on error, NewRouter returns http.Handler
|
||||||
|
- main() opens "file:app.db?mode=rwc" and listens on 127.0.0.1:3000
|
||||||
|
- No markdown fences inside file content — just raw code
|
||||||
|
- You MUST generate ALL 5 files. Do not stop early.
|
||||||
|
|
||||||
|
TESTS — follow this exact setupTestServer pattern:
|
||||||
|
|
||||||
|
func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
InitDB(db)
|
||||||
|
return httptest.NewServer(NewRouter(db)), db
|
||||||
|
}
|
||||||
|
|
||||||
|
- Each test function calls setupTestServer(t) to get (ts, db)
|
||||||
|
- defer ts.Close() and defer db.Close() in every test
|
||||||
|
- Use standard library: http.Post, http.Get, http.NewRequest for PUT/DELETE
|
||||||
|
- Use strings.NewReader for JSON request bodies
|
||||||
|
- Decode responses with json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
- Unique descriptive data, NOT generic "test" strings
|
||||||
|
- Format IDs with fmt.Sprintf("%.0f", id) when building URLs from float64
|
||||||
Reference in New Issue
Block a user