From b2b8268532681b7802b315abde3836c282a9cd17 Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Mon, 12 Aug 2019 14:47:26 +0700 Subject: [PATCH 1/6] Implementation of creating pill and printing to StdOut. --- main.go | 33 +++++++++++++++++++++++++++++ pill.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test.svg | 20 ++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 main.go create mode 100644 pill.go create mode 100644 test.svg diff --git a/main.go b/main.go new file mode 100644 index 0000000..7a1b454 --- /dev/null +++ b/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "os" + "text/template" +) + +const ( + baseColour = "#555" + modifier float64 = 0.2 + maxValue = 220 + blue = 55 +) + +func main() { + t := template.New("svgConv") + + percentage := 0.999 + fill := percentageToRGB(percentage) + + pill := CoveragePill{ + fmt.Sprintf("%.1f", toFixed(percentage, 3)*100), + baseColour, + fill, + } + t, err := t.Parse(svgTemplate) + if err != nil { + panic(err) + } + + _ = t.Execute(os.Stdout, pill) +} diff --git a/pill.go b/pill.go new file mode 100644 index 0000000..e1cf55c --- /dev/null +++ b/pill.go @@ -0,0 +1,63 @@ +package main + +import ( + "math" + "strconv" +) + +const svgTemplate string = ` + + + + + + + + + + + + + + coverage + coverage + {{.Percentage}}% + {{.Percentage}}% + +` + +type CoveragePill struct { + Percentage string + BaseColour string + FillColour string +} + +func percentageToRGB(percentage float64) string { + red := modifier + clamp(2-2*math.Pow(percentage, 2), 0, 1)*(1-modifier) + green := modifier + clamp(2*math.Pow(percentage, 2), 0, 1)*(1-modifier) + + redValue := strconv.Itoa(int(red * maxValue)) + greenValue := strconv.Itoa(int(green * maxValue)) + blueValue := strconv.Itoa(blue) + + return "rgb(" + redValue + ", " + greenValue + ", " + blueValue + ")" +} + +func clamp(operation float64, min float64, max float64) float64 { + if operation < min { + return min + } + if operation > max { + return max + } + return operation +} + +func round(num float64) int { + return int(num + math.Copysign(0.5, num)) +} + +func toFixed(num float64, precision int) float64 { + output := math.Pow(10, float64(precision)) + return float64(round(num*output)) / output +} diff --git a/test.svg b/test.svg new file mode 100644 index 0000000..b84c231 --- /dev/null +++ b/test.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 99.9% + 99.9% + + -- 2.38.4 From 63a1cad8d4b17ad9dbdc81a988e5984331a38cfc Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Mon, 12 Aug 2019 17:53:09 +0700 Subject: [PATCH 2/6] HTTP Basic implementation: Print file to STDOUT --- main.go | 60 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 7a1b454..147bae0 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "fmt" - "os" - "text/template" + "github.com/gorilla/mux" + "io/ioutil" + "net/http" ) const ( @@ -14,20 +16,54 @@ const ( ) func main() { - t := template.New("svgConv") + //_ := template.New("svgConv") - percentage := 0.999 - fill := percentageToRGB(percentage) + //percentage := 0.999 + //fill := percentageToRGB(percentage) - pill := CoveragePill{ - fmt.Sprintf("%.1f", toFixed(percentage, 3)*100), - baseColour, - fill, + //_ := CoveragePill{ + // fmt.Sprintf("%.1f", toFixed(percentage, 3)*100), + // baseColour, + // fill, + //} + + r := mux.NewRouter() + r.HandleFunc("/upload/go", upload) + _ = http.ListenAndServe(":8080", r) +} + +func upload(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, "Request parameters not parsed", 500) + return } - t, err := t.Parse(svgTemplate) + err = r.ParseMultipartForm(5 << 20) if err != nil { - panic(err) + http.Error(w, "Request parameters not parsed", 500) + return + } + + form := r.Form + + project := form.Get("project") + tag := form.Get("tag") + id := form.Get("id") + file, _, err := r.FormFile("file") + b, _ := ioutil.ReadAll(file) + n := bytes.Index(b, []byte{0}) + content := string(b[n]) + + fmt.Print(content) + + if project == "" || tag == "" || id == "" { + http.Error(w, "Request parameters [secret, project, tag, id] not found", 400) + return } - _ = t.Execute(os.Stdout, pill) + _, _ = fmt.Fprintf(w, "project: '%s'\ntag: '%s'\nid: '%s'\n", project, tag, id) } -- 2.38.4 From 99695a4c4da8ebb907904f3f60e5ffdc17811900 Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Mon, 12 Aug 2019 23:13:53 +0700 Subject: [PATCH 3/6] Implementation of /badge/commit/. --- Badge.go | 57 ++++++++++++++++++++++++++++++++++++++ connection.go | 22 +++++++++++++++ database.sqlite | Bin 0 -> 20480 bytes main.go | 71 +++++++++++++----------------------------------- pill.go | 14 ++++++++-- upload.go | 45 ++++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 55 deletions(-) create mode 100644 Badge.go create mode 100644 connection.go create mode 100644 database.sqlite create mode 100644 upload.go diff --git a/Badge.go b/Badge.go new file mode 100644 index 0000000..d834aee --- /dev/null +++ b/Badge.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "github.com/gorilla/mux" + "net/http" + "text/template" +) + +var baseTemplate *template.Template + +func init() { + baseTemplate = template.New("svgTemplate") + baseTemplate, _ = baseTemplate.Parse(svgTemplate) +} + +func BadgeFromCommit(w http.ResponseWriter, r *http.Request) { + fmt.Println(mux.Vars(r)) + commitID := mux.Vars(r)["commit"] + result, _ := db.Query("SELECT percentage FROM badge WHERE commit_hash=?", commitID) + defer result.Close() + + w.Header().Set("Content-Type", "image/svg+xml") + + if !result.Next() { + http.Error(w, "Request commit hash has no corresponding badge", 400) + //TODO send unknown svg instead + return + } + + var percentage float64 + result.Scan(&percentage) + + fillColour := percentageToRGB(percentage) + + template.New("svgConv") + pill := CoveragePill{ + toOneDecimal(percentage * 100), + baseColour, + fillColour, + } + baseTemplate.Execute(w, pill) +} + +func BadgeFromProject(w http.ResponseWriter, r *http.Request) { + projectName := mux.Vars(r)["project"] + tagName := mux.Vars(r)["tag"] + + result, _ := db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", projectName, tagName) + defer result.Close() + + var commit_hash string + result.Scan(&commit_hash) + fmt.Println(commit_hash) + mux.Vars(r)["commit"] = commit_hash + BadgeFromCommit(w, r) +} diff --git a/connection.go b/connection.go new file mode 100644 index 0000000..fc2c978 --- /dev/null +++ b/connection.go @@ -0,0 +1,22 @@ +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB +var alreadyUsingMemory bool + +func init() { + openDatabase("./database.sqlite") +} + +func openDatabase(fileLocation string) { + db, _ = sql.Open("sqlite3", fileLocation) + db.SetMaxOpenConns(1) + + // Initializes the tables + db.Exec(`CREATE TABLE IF NOT EXISTS badge (commit_hash VARCHAR(32) NOT NULL UNIQUE PRIMARY KEY, percentage FLOAT NOT NULL)`) + db.Exec(`CREATE TABLE IF NOT EXISTS alias (commit_hash VARCHAR(32) NOT NULL UNIQUE PRIMARY KEY, project_name TEXT NOT NULL, project_tag TEXT NOT NULL)`) +} diff --git a/database.sqlite b/database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..d42716859e10bbbc891ce4f3859a9cc8b719f63d GIT binary patch literal 20480 zcmeI&yHDFd9Ki9rBoH12R~_ns5HB-9$@st6N^Nh+AEOLByuJWAq9h>h;d z{b#!H2k;N@Hz0MY#K`3Y0tPyC4Byked_Kwd`}h_=_R4W9GwQI{@90b|i?>2b@mVP$ zMA4QpTOLx*b_x#%wv|u%FLjGzab;r0yA_4m3*nW$tJw>0{-w!`BY*$`2q1s}0tg_0 z00IcS5`pvg`J(Up^5=A>Yi*<3Ej@5mVIzscN~9{`dK@V?q!#MEPN$WnM|yCizJj6B=-`9;-tQfodX{!|$EZrQZ>Qc*e>|7z z=JQiQP#E{ud?{MphB+OawCy8vs()lXcbvNQq)V#&xS{_I z!Sw5yxma~!(BCFi}Y@U%_Ia6KmY**5I_I{1Q0*~ z0R#{j0fBciKUdVXdczzxKmGZAy?XiP*OZM*dVht@Bm@vZ009ILKmY**5I_I{1P~Y{ zfvNo5 ` +const ( + baseColour = "#555" + modifier float64 = 0.2 + maxValue = 220 + blue = 55 +) + type CoveragePill struct { Percentage string BaseColour string @@ -57,7 +65,7 @@ func round(num float64) int { return int(num + math.Copysign(0.5, num)) } -func toFixed(num float64, precision int) float64 { - output := math.Pow(10, float64(precision)) - return float64(round(num*output)) / output +func toOneDecimal(num float64) string { + output := math.Pow(10, float64(5)) + return fmt.Sprintf("%.1f", float64(round(num*output))/output) } diff --git a/upload.go b/upload.go new file mode 100644 index 0000000..b20aa4f --- /dev/null +++ b/upload.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" +) + +func uploadGo(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, "Request parameters not parsed", 500) + return + } + err = r.ParseMultipartForm(5 << 20) + if err != nil { + http.Error(w, "Request parameters not parsed", 500) + return + } + + project := r.Form.Get("project") + tag := r.Form.Get("tag") + id := r.Form.Get("id") + + if project == "" || tag == "" || id == "" { + http.Error(w, "Request parameters [secret, project, tag, id] not found", 400) + return + } + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Request parameter file not found", 400) + return + } + b, _ := ioutil.ReadAll(file) + coverage := parseCoverage(string(b)) + + db.Exec("INSERT INTO badge (commit_hash, percentage) VALUES (?, ?)", id, coverage) + db.Exec("INSERT INTO alias (commit_hash, project_name, project_tag) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE commit=?", id, project, tag, id) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"success": "true", "coverage": `+toOneDecimal(coverage*100)+`}`) +} -- 2.38.4 From 9f1e45c1d5a99d352d00adff20d4ce8fb7f8270d Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Tue, 13 Aug 2019 16:23:23 +0700 Subject: [PATCH 4/6] Implement endpoints /badge/commit/{commit} and /badge/{project_name}/{project_tag}, and show unknown coverage when database record is missing, and formalize all commit messages in /upload/go to json --- Badge.go | 57 ------------------------------------- badge.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ database.sqlite | Bin 20480 -> 20480 bytes main.go | 7 +++-- pill.go | 18 ++++++++++-- upload.go | 29 +++++++++++++------ 6 files changed, 114 insertions(+), 71 deletions(-) delete mode 100644 Badge.go create mode 100644 badge.go diff --git a/Badge.go b/Badge.go deleted file mode 100644 index d834aee..0000000 --- a/Badge.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "fmt" - "github.com/gorilla/mux" - "net/http" - "text/template" -) - -var baseTemplate *template.Template - -func init() { - baseTemplate = template.New("svgTemplate") - baseTemplate, _ = baseTemplate.Parse(svgTemplate) -} - -func BadgeFromCommit(w http.ResponseWriter, r *http.Request) { - fmt.Println(mux.Vars(r)) - commitID := mux.Vars(r)["commit"] - result, _ := db.Query("SELECT percentage FROM badge WHERE commit_hash=?", commitID) - defer result.Close() - - w.Header().Set("Content-Type", "image/svg+xml") - - if !result.Next() { - http.Error(w, "Request commit hash has no corresponding badge", 400) - //TODO send unknown svg instead - return - } - - var percentage float64 - result.Scan(&percentage) - - fillColour := percentageToRGB(percentage) - - template.New("svgConv") - pill := CoveragePill{ - toOneDecimal(percentage * 100), - baseColour, - fillColour, - } - baseTemplate.Execute(w, pill) -} - -func BadgeFromProject(w http.ResponseWriter, r *http.Request) { - projectName := mux.Vars(r)["project"] - tagName := mux.Vars(r)["tag"] - - result, _ := db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", projectName, tagName) - defer result.Close() - - var commit_hash string - result.Scan(&commit_hash) - fmt.Println(commit_hash) - mux.Vars(r)["commit"] = commit_hash - BadgeFromCommit(w, r) -} diff --git a/badge.go b/badge.go new file mode 100644 index 0000000..e4ad7e4 --- /dev/null +++ b/badge.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "fmt" + "net/http" + "text/template" + + "github.com/gorilla/mux" +) + +var baseTemplate *template.Template + +func init() { + baseTemplate = template.New("svgTemplate") + baseTemplate, _ = baseTemplate.Parse(svgTemplate) +} + +func badgeFromCommit(w http.ResponseWriter, r *http.Request) { + commitID := mux.Vars(r)["commit"] + result := svgFromHash(commitID) + + w.Header().Set("Content-Type", "image/svg+xml") + fmt.Fprint(w, result) +} + +func badgeFromProject(w http.ResponseWriter, r *http.Request) { + projectName := mux.Vars(r)["project"] + tagName := mux.Vars(r)["tag"] + commitRow, err := db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", projectName, tagName) + + if err != nil { + panic(err) + } + if !commitRow.Next() { + fmt.Fprint(w, unknownCoveragePill()) + return + } + + var commitHash string + commitRow.Scan(&commitHash) + commitRow.Close() + + result := svgFromHash(commitHash) + + w.Header().Set("Content-Type", "image/svg+xml") + fmt.Fprint(w, result) +} + +func svgFromHash(commit string) string { + result, err := db.Query("SELECT percentage FROM badge WHERE commit_hash=?", commit) + defer result.Close() + if err != nil { + panic(err) + } + + if !result.Next() { + return unknownCoveragePill() + } + + var percentage float64 + result.Scan(&percentage) + + fillColour := percentageToRGB(percentage) + pill := CoveragePill{ + toOneDecimal(percentage*100) + "%", + baseColour, + fillColour, + } + + buf := &bytes.Buffer{} + baseTemplate.Execute(buf, pill) + return buf.String() +} diff --git a/database.sqlite b/database.sqlite index d42716859e10bbbc891ce4f3859a9cc8b719f63d..441bb3a9dc3d7e1e4477eaf03dc417cbeb47bb98 100644 GIT binary patch delta 275 zcmZozz}T>Wae_1>%S0JxMwX2U%lKJNGw`3@EGTf0-`<#!IhH*w+0fWLIoZP8Jk8WB z*}}v$%^=ax#K_XzB-O&gGA+@_B+W3{$lS>Oed7=L#vck?3=9m6{8t$GuK*1>#c!s~ u9Lq_p8N6VWae_1>`$QRMM)r*f%lKK|Fz~ coverage coverage - {{.Percentage}}% - {{.Percentage}}% + {{.Percentage}} + {{.Percentage}} ` @@ -34,12 +35,25 @@ const ( blue = 55 ) +//Pill used for template type CoveragePill struct { Percentage string BaseColour string FillColour string } +func unknownCoveragePill() string { + pill := CoveragePill{ + "?", + baseColour, + baseColour, // Using the base colour for fill colour because unknown + } + + buf := &bytes.Buffer{} + baseTemplate.Execute(buf, pill) + return buf.String() +} + func percentageToRGB(percentage float64) string { red := modifier + clamp(2-2*math.Pow(percentage, 2), 0, 1)*(1-modifier) green := modifier + clamp(2*math.Pow(percentage, 2), 0, 1)*(1-modifier) diff --git a/upload.go b/upload.go index b20aa4f..418c3e9 100644 --- a/upload.go +++ b/upload.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" "fmt" "io/ioutil" "net/http" @@ -10,36 +11,46 @@ func uploadGo(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { return } + w.Header().Set("Content-Type", "application/json") err := r.ParseForm() if err != nil { - http.Error(w, "Request parameters not parsed", 500) + http.Error(w, `{"success": false, "message": "Request parameters not parsed"}`, 500) return } err = r.ParseMultipartForm(5 << 20) if err != nil { - http.Error(w, "Request parameters not parsed", 500) + http.Error(w, `{"success": false, "message": "Request parameters too large"}`, 500) return } - project := r.Form.Get("project") + name := r.Form.Get("project-name") tag := r.Form.Get("tag") id := r.Form.Get("id") - if project == "" || tag == "" || id == "" { - http.Error(w, "Request parameters [secret, project, tag, id] not found", 400) + if name == "" || tag == "" || id == "" { + http.Error(w, `{"success": false, "message": "Request parameters [secret, name, tag, id] not found"}`, 400) return } file, _, err := r.FormFile("file") if err != nil { - http.Error(w, "Request parameter file not found", 400) + http.Error(w, `{"success": false, "message": "Request parameter file not found"}`, 400) return } b, _ := ioutil.ReadAll(file) coverage := parseCoverage(string(b)) db.Exec("INSERT INTO badge (commit_hash, percentage) VALUES (?, ?)", id, coverage) - db.Exec("INSERT INTO alias (commit_hash, project_name, project_tag) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE commit=?", id, project, tag, id) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"success": "true", "coverage": `+toOneDecimal(coverage*100)+`}`) + + _, err = db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", name, tag) + + if err == nil { + db.Exec("UPDATE ALIAS SET commit_hash=? WHERE project_name=? AND project_tag=?", id, name, tag) + } else if err == sql.ErrNoRows { + db.Exec("INSERT INTO alias (commit_hash, project_name, project_tag VALUES (?, ?, ?)", id, name, tag) + } else { + panic(err) + } + + fmt.Fprintf(w, `{"success": "true", "coverage": %s%%}`, toOneDecimal(coverage*100)) } -- 2.38.4 From 2783ec1d0b4d93b03bb0dde9cc5c23caad8774ae Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Tue, 30 Mar 2021 22:17:14 +0700 Subject: [PATCH 5/6] feat: add server side of things --- .gitignore | 1 + cmd/server/Dockerfile | 10 ++++ cmd/server/main.go | 38 ++++++++++++++ db/connection.go | 33 ++++++++++++ db/query.go | 75 +++++++++++++++++++++++++++ db/upload.go | 66 ++++++++++++++++++++++++ go.mod | 8 +++ go.sum | 4 ++ pill/badge.go | 1 + pill/pill.go | 79 ++++++++++++++++++++++++++++ pill/pill.svg | 11 ++++ server/query.go | 116 ++++++++++++++++++++++++++++++++++++++++++ server/server.go | 53 +++++++++++++++++++ server/upload.go | 76 +++++++++++++++++++++++++++ 14 files changed, 571 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/server/Dockerfile create mode 100644 cmd/server/main.go create mode 100644 db/connection.go create mode 100644 db/query.go create mode 100644 db/upload.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pill/badge.go create mode 100644 pill/pill.go create mode 100644 pill/pill.svg create mode 100644 server/query.go create mode 100644 server/server.go create mode 100644 server/upload.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c4176f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.db \ No newline at end of file diff --git a/cmd/server/Dockerfile b/cmd/server/Dockerfile new file mode 100644 index 0000000..422b10a --- /dev/null +++ b/cmd/server/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.16 AS build +WORKDIR /root/ +COPY . . +RUN go build -o coverage . + +FROM golang:1.16 +WORKDIR /root/ +COPY --from=build /root/coverage . +EXPOSE 8080 +CMD ["./coverage"] diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..0fadeda --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + + "gitea.teamortix.com/team-ortix/coverage/db" + "gitea.teamortix.com/team-ortix/coverage/server" +) + +var ( + port = os.Getenv("COVERAGE_PORT") + dbFile = os.Getenv("DB_FILE") +) + +func main() { + if port == "" { + port = "8080" + } + if dbFile == "" { + dbFile = "coverage.db" + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + go func() { + db.OpenDatabase(dbFile) + server.StartServer(port) + }() + + log.Printf("server starting. at http://0.0.0.0:%s", port) + + <-c + fmt.Println("\nAborting...") +} diff --git a/db/connection.go b/db/connection.go new file mode 100644 index 0000000..835b046 --- /dev/null +++ b/db/connection.go @@ -0,0 +1,33 @@ +package db + +import ( + "database/sql" + "log" + + // Required to connect to database. + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB + +func OpenDatabase(fileLocation string) { + var err error + db, err = sql.Open("sqlite3", fileLocation) + if err != nil { + log.Fatalf("could not open database: %v", err) + } + + // Initializes the table. + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS badge ( + namespace VARCHAR(64) NOT NULL, + project_name VARCHAR(64) NOT NULL, + coverage INTEGER NOT NULL, + html TEXT NOT NULL, + + branch VARCHAR(64), + pull INTEGER + )`) + if err != nil { + log.Fatalf("Could not create table: %v", err) + } +} diff --git a/db/query.go b/db/query.go new file mode 100644 index 0000000..4c988d1 --- /dev/null +++ b/db/query.go @@ -0,0 +1,75 @@ +package db + +import ( + "database/sql" + "errors" + "strconv" +) + +var ErrNoData = errors.New("no badge found with the provided data") + +type CoverageData struct { + Namespace string + Project string + Coverage float64 + HTML string +} + +// QueryByBranch returns CoverageData for the provided branch for the project. +// If there is no such data, it returns ErrNoData. +func QueryByBranch(namespace, project, branch string) (CoverageData, error) { + result, err := db.Query(`SELECT coverage,html FROM badge WHERE + namespace=? AND + project_name=? AND + branch=?`, + namespace, project, branch) + if err != nil { + return CoverageData{}, err + } + + return parseRows(result, namespace, project) +} + +// QueryByBranch returns CoverageData for the provided pull request of the project. +// If there is no such data, it returns ErrNoData. +func QueryByPull(namespace, project, pullStr string) (CoverageData, error) { + pull, err := strconv.Atoi(pullStr) + if err != nil { + return CoverageData{}, ErrNoData + } + + result, err := db.Query(`SELECT coverage, html FROM badge WHERE + namespace=? AND + project_name=? AND + pull=?`, + namespace, project, pull) + if err != nil { + return CoverageData{}, err + } + + return parseRows(result, namespace, project) +} + +func parseRows(result *sql.Rows, namespace, project string) (CoverageData, error) { + if !result.Next() { + return CoverageData{}, ErrNoData + } + + var coverage float64 + var html string + err := result.Scan(&coverage, &html) + if err != nil { + return CoverageData{}, err + } + + if err = result.Close(); err != nil { + return CoverageData{}, ErrNoData + } + + return CoverageData{ + Namespace: namespace, + Project: project, + Coverage: coverage, + HTML: html, + }, nil +} diff --git a/db/upload.go b/db/upload.go new file mode 100644 index 0000000..73189be --- /dev/null +++ b/db/upload.go @@ -0,0 +1,66 @@ +package db + +import ( + "fmt" + "strconv" +) + +func UploadBranchData(namespace, project, branch, coverageStr, html string) error { + coverage, err := strconv.ParseFloat(coverageStr, 64) + if err != nil { + return fmt.Errorf("coverage param must be valid float, instead got %s", coverageStr) + } + + // Because we don't have have a primary key, we must do the upsert manually. + _, err = QueryByBranch(namespace, project, branch) + if err == ErrNoData { + _, err = db.Exec("INSERT INTO badge (namespace, project_name, branch, coverage, html) VALUES (?, ?, ?, ?, ?)", + namespace, project, branch, coverage, html) + if err != nil { + return fmt.Errorf("could not update database: %v", err) + } + } + if err != nil { + return err + } + + _, err = db.Exec("UPDATE badge SET coverage=?, html=? WHERE namespace=? AND project_name=? AND branch=?", + coverage, html, namespace, project, branch) + if err != nil { + return fmt.Errorf("could not update database: %v", err) + } + + return nil +} + +func UploadPullData(namespace, project, pullStr, coverageStr, html string) error { + pull, err := strconv.Atoi(pullStr) + if err != nil { + return fmt.Errorf("pull param must be valid int, instead got %s", pullStr) + } + coverage, err := strconv.ParseFloat(coverageStr, 64) + if err != nil { + return fmt.Errorf("coverage param must be valid float, instead got %s", coverageStr) + } + + // Because we don't have have a primary key, we must do the upsert manually. + _, err = QueryByPull(namespace, project, pullStr) + if err == ErrNoData { + _, err = db.Exec("INSERT INTO badge (namespace, project_name, pull, coverage, html) VALUES (?, ?, ?, ?, ?)", + namespace, project, pull, coverage, html) + if err != nil { + return fmt.Errorf("could not update database: %v", err) + } + } + if err != nil { + return err + } + + _, err = db.Exec("UPDATE badge SET coverage=?, html=? WHERE namespace=? AND project_name=? AND pull=?", + coverage, html, namespace, project, pull) + if err != nil { + return fmt.Errorf("could not update database: %v", err) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..573805c --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module gitea.teamortix.com/team-ortix/coverage + +go 1.16 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-sqlite3 v1.14.6 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..27294bc --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/pill/badge.go b/pill/badge.go new file mode 100644 index 0000000..bfcec92 --- /dev/null +++ b/pill/badge.go @@ -0,0 +1 @@ +package pill diff --git a/pill/pill.go b/pill/pill.go new file mode 100644 index 0000000..3a57ef1 --- /dev/null +++ b/pill/pill.go @@ -0,0 +1,79 @@ +package pill + +import ( + // Used for embedding pill. + _ "embed" + "fmt" + "io" + "math" + "text/template" +) + +var ( + //go:embed pill.svg + pill string + + tmpl = template.New("svgTemplate") +) + +const ( + modifier = 0.2 + max = 190 + blue = 55 +) + +// Pill is used to execute the pill.svg template. +// Width is dependent on whether the percentage is 2 digits or 3 digits. +type Pill struct { + Percentage string + Colour string + Width string +} + +// NewPill creates creates a Pill, automatically calculating the colour, width, and formatting the percentage. +// Note: %02.0f gives us a float with 0 decimals and 0 padding for 2 numbers less than 10. +func NewPill(percentDecimal float64) Pill { + square := math.Pow(percentDecimal, 2) + red := modifier + clamp(2-2*square, 0, 1)*(1-modifier) + green := modifier + clamp(2*square, 0, 1)*(1-modifier) + + percentage := fmt.Sprintf("%.0f", percentDecimal*100) + colour := fmt.Sprintf("rgb(%.0f, %.0f, %d)", red*max, green*max, blue) + width := "250" + if len(percentage) == 1 { + percentage = " " + percentage + width = "220" + } + if len(percentage) == 3 { + width = "300" + } + + return Pill{ + Percentage: percentage, + Colour: colour, + Width: width, + } +} + +// Execute runs the pill template with the data and writes the result to the writer given. +func (p Pill) Execute(w io.Writer) error { + var err error + tmpl, err = tmpl.Parse(pill) + if err != nil { + return err + } + + err = tmpl.Execute(w, p) + return err +} + +func clamp(n, min, max float64) float64 { + switch { + case n > max: + return max + case n < min: + return min + default: + return n + } +} diff --git a/pill/pill.svg b/pill/pill.svg new file mode 100644 index 0000000..ff9012c --- /dev/null +++ b/pill/pill.svg @@ -0,0 +1,11 @@ + + coverage: {{.Percentage}}% + + + + + + coverage + {{.Percentage}}% + + \ No newline at end of file diff --git a/server/query.go b/server/query.go new file mode 100644 index 0000000..7d57172 --- /dev/null +++ b/server/query.go @@ -0,0 +1,116 @@ +package server + +import ( + "fmt" + "log" + "net/http" + + "gitea.teamortix.com/team-ortix/coverage/db" + "gitea.teamortix.com/team-ortix/coverage/pill" + "github.com/gorilla/mux" +) + +type endpointData struct { + thirdParam string + queryFunc func(string, string, string) (db.CoverageData, error) + handle func(db.CoverageData) +} + +func (d endpointData) run(w http.ResponseWriter, r *http.Request) { + params, ok := getAllParams(w, r, d.thirdParam) + if !ok { + return + } + + data, err := d.queryFunc(params[0], params[1], params[2]) + if err == db.ErrNoData { + http.Error(w, "not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, fmt.Sprintf("could not query from db: %v", err), http.StatusInternalServerError) + log.Printf("error occurred while querying data from db: %v", err) + return + } + + d.handle(data) +} + +func handleBranchReport(w http.ResponseWriter, r *http.Request) { + endpointData{ + thirdParam: "branch", + queryFunc: db.QueryByBranch, + handle: func(cd db.CoverageData) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, cd.HTML) + }, + }.run(w, r) +} + +func handleBadgeBranch(w http.ResponseWriter, r *http.Request) { + endpointData{ + thirdParam: "branch", + queryFunc: db.QueryByBranch, + handle: func(cd db.CoverageData) { + p := pill.NewPill(cd.Coverage) + + w.Header().Set("Content-Type", "image/svg+xml") + err := p.Execute(w) + if err != nil { + http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) + return + } + }, + }.run(w, r) +} + +func handlePullReport(w http.ResponseWriter, r *http.Request) { + endpointData{ + thirdParam: "pull", + queryFunc: db.QueryByPull, + handle: func(cd db.CoverageData) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, cd.HTML) + }, + }.run(w, r) +} + +func handlePullBadge(w http.ResponseWriter, r *http.Request) { + endpointData{ + thirdParam: "pull", + queryFunc: db.QueryByPull, + handle: func(cd db.CoverageData) { + p := pill.NewPill(cd.Coverage) + + w.Header().Set("Content-Type", "image/svg+xml") + err := p.Execute(w) + if err != nil { + http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) + return + } + }, + }.run(w, r) +} + +func getAllParams(w http.ResponseWriter, r *http.Request, thirdParam string) ([3]string, bool) { + vars := mux.Vars(r) + namespace, ok := vars["namespace"] + if !ok { + http.Error(w, "Path 'namespace' not found", http.StatusBadRequest) + return [3]string{}, false + } + + project, ok := vars["project"] + if !ok { + http.Error(w, "Path 'project' not found", http.StatusBadRequest) + return [3]string{}, false + } + + third, ok := vars[thirdParam] + if !ok { + http.Error(w, fmt.Sprintf("Path '%s' not found", thirdParam), http.StatusBadRequest) + return [3]string{}, false + } + + return [3]string{namespace, project, third}, true +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..daae28e --- /dev/null +++ b/server/server.go @@ -0,0 +1,53 @@ +package server + +import ( + "fmt" + "log" + "net/http" + "strconv" + + "gitea.teamortix.com/team-ortix/coverage/pill" + "github.com/gorilla/mux" +) + +func StartServer(port string) { + r := mux.NewRouter() + r.HandleFunc("/badge/{percentage}", handleBadge) + + r.HandleFunc("/report/branch/{namespace}/{project}/{branch}", handleBranchReport) + r.HandleFunc("/report/pulls/{namespace}/{project}/{pull}", handlePullReport) + r.HandleFunc("/badge/branch/{namespace}/{project}/{branch}", handleBadgeBranch) + r.HandleFunc("/badge/pulls/{namespace}/{project}/{pull}", handlePullBadge) + + r.HandleFunc("/upload/branch/{namespace}/{project}/{branch}", uploadBranch) + r.HandleFunc("/upload/pulls/{namespace}/{project}/{branch}", uploadPull) + + portStr := fmt.Sprintf(":%s", port) + err := http.ListenAndServe(portStr, r) + if err != nil { + log.Fatalf("could not start server: %v", err) + } +} + +func handleBadge(w http.ResponseWriter, r *http.Request) { + percentString, ok := mux.Vars(r)["percentage"] + if !ok { + http.Error(w, "could not find percentage in path", http.StatusBadRequest) + return + } + + percent, err := strconv.ParseFloat(percentString, 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not convert %s to a number", percentString), http.StatusBadRequest) + return + } + + p := pill.NewPill(percent / 100) + + w.Header().Set("Content-Type", "image/svg+xml") + err = p.Execute(w) + if err != nil { + http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) + return + } +} diff --git a/server/upload.go b/server/upload.go new file mode 100644 index 0000000..75b26fd --- /dev/null +++ b/server/upload.go @@ -0,0 +1,76 @@ +package server + +import ( + "fmt" + "io" + "net/http" + "os" + + "gitea.teamortix.com/team-ortix/coverage/db" +) + +const coverageSecret = "COVERAGE_SECRET" + +var secretEnv = os.Getenv(coverageSecret) + +func uploadBranch(w http.ResponseWriter, r *http.Request) { + upload(w, r, "branch", db.UploadBranchData) +} + +func uploadPull(w http.ResponseWriter, r *http.Request) { + upload(w, r, "pull", db.UploadPullData) +} + +func upload(w http.ResponseWriter, r *http.Request, + uploadType string, uploadFunc func(string, string, string, string, string) error) { + if r.Method != "POST" { + return + } + + err := r.ParseMultipartForm(5 << 20) + if err != nil { + http.Error(w, "file too large", http.StatusRequestEntityTooLarge) + return + } + err = r.ParseForm() + if err != nil { + http.Error(w, "could not parse form", http.StatusBadRequest) + return + } + + secret := r.Form.Get("secret") + namespace := r.Form.Get("namespace") + project := r.Form.Get("project") + uploadValue := r.Form.Get(uploadType) + coverage := r.Form.Get("coverage") + + if secretEnv != secret && secretEnv != "" { + http.Error(w, "invalid secret provided", http.StatusUnauthorized) + return + } + + if namespace == "" || project == "" || uploadValue == "" || coverage == "" { + http.Error(w, fmt.Sprintf("request params [namespace project, %s, coverage] must all be present", uploadType), + http.StatusBadRequest) + return + } + + report, _, err := r.FormFile("report") + if err != nil { + http.Error(w, "could not parse report parameter", http.StatusBadRequest) + return + } + bytes, err := io.ReadAll(report) + if err != nil { + http.Error(w, fmt.Sprintf("could not read file: %v", err), http.StatusBadRequest) + return + } + + err = uploadFunc(namespace, project, uploadValue, coverage, string(bytes)) + if err != nil { + http.Error(w, fmt.Sprintf("could not upload data to database: %v", err), http.StatusInternalServerError) + return + } + + fmt.Fprintln(w, "uploaded") +} -- 2.38.4 From bf3480e9e3cc35c487a52586669ed1ebabae3244 Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Tue, 30 Mar 2021 22:45:34 +0700 Subject: [PATCH 6/6] fix: update paths to not use 404, only require coverage as form data param --- pill/pill.go | 2 +- server/query.go | 24 ------------------------ server/server.go | 25 ++++++++++++++++++++++++- server/upload.go | 14 +++++++------- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/pill/pill.go b/pill/pill.go index 3a57ef1..5edf9db 100644 --- a/pill/pill.go +++ b/pill/pill.go @@ -31,7 +31,7 @@ type Pill struct { } // NewPill creates creates a Pill, automatically calculating the colour, width, and formatting the percentage. -// Note: %02.0f gives us a float with 0 decimals and 0 padding for 2 numbers less than 10. +// Note: %.0f gives us a float with 0 decimals and 0 padding for 2 numbers less than 10. func NewPill(percentDecimal float64) Pill { square := math.Pow(percentDecimal, 2) red := modifier + clamp(2-2*square, 0, 1)*(1-modifier) diff --git a/server/query.go b/server/query.go index 7d57172..a73d6a0 100644 --- a/server/query.go +++ b/server/query.go @@ -7,7 +7,6 @@ import ( "gitea.teamortix.com/team-ortix/coverage/db" "gitea.teamortix.com/team-ortix/coverage/pill" - "github.com/gorilla/mux" ) type endpointData struct { @@ -91,26 +90,3 @@ func handlePullBadge(w http.ResponseWriter, r *http.Request) { }, }.run(w, r) } - -func getAllParams(w http.ResponseWriter, r *http.Request, thirdParam string) ([3]string, bool) { - vars := mux.Vars(r) - namespace, ok := vars["namespace"] - if !ok { - http.Error(w, "Path 'namespace' not found", http.StatusBadRequest) - return [3]string{}, false - } - - project, ok := vars["project"] - if !ok { - http.Error(w, "Path 'project' not found", http.StatusBadRequest) - return [3]string{}, false - } - - third, ok := vars[thirdParam] - if !ok { - http.Error(w, fmt.Sprintf("Path '%s' not found", thirdParam), http.StatusBadRequest) - return [3]string{}, false - } - - return [3]string{namespace, project, third}, true -} diff --git a/server/server.go b/server/server.go index daae28e..2fe3953 100644 --- a/server/server.go +++ b/server/server.go @@ -20,7 +20,7 @@ func StartServer(port string) { r.HandleFunc("/badge/pulls/{namespace}/{project}/{pull}", handlePullBadge) r.HandleFunc("/upload/branch/{namespace}/{project}/{branch}", uploadBranch) - r.HandleFunc("/upload/pulls/{namespace}/{project}/{branch}", uploadPull) + r.HandleFunc("/upload/pulls/{namespace}/{project}/{pull}", uploadPull) portStr := fmt.Sprintf(":%s", port) err := http.ListenAndServe(portStr, r) @@ -51,3 +51,26 @@ func handleBadge(w http.ResponseWriter, r *http.Request) { return } } + +func getAllParams(w http.ResponseWriter, r *http.Request, thirdParam string) ([3]string, bool) { + vars := mux.Vars(r) + namespace, ok := vars["namespace"] + if !ok { + http.Error(w, "Path 'namespace' not found", http.StatusBadRequest) + return [3]string{}, false + } + + project, ok := vars["project"] + if !ok { + http.Error(w, "Path 'project' not found", http.StatusBadRequest) + return [3]string{}, false + } + + third, ok := vars[thirdParam] + if !ok { + http.Error(w, fmt.Sprintf("Path '%s' not found", thirdParam), http.StatusBadRequest) + return [3]string{}, false + } + + return [3]string{namespace, project, third}, true +} diff --git a/server/upload.go b/server/upload.go index 75b26fd..98c2ec5 100644 --- a/server/upload.go +++ b/server/upload.go @@ -39,19 +39,19 @@ func upload(w http.ResponseWriter, r *http.Request, } secret := r.Form.Get("secret") - namespace := r.Form.Get("namespace") - project := r.Form.Get("project") - uploadValue := r.Form.Get(uploadType) + params, ok := getAllParams(w, r, uploadType) coverage := r.Form.Get("coverage") if secretEnv != secret && secretEnv != "" { http.Error(w, "invalid secret provided", http.StatusUnauthorized) return } + if !ok { + return + } - if namespace == "" || project == "" || uploadValue == "" || coverage == "" { - http.Error(w, fmt.Sprintf("request params [namespace project, %s, coverage] must all be present", uploadType), - http.StatusBadRequest) + if coverage == "" { + http.Error(w, "request param coverage must be present", http.StatusBadRequest) return } @@ -66,7 +66,7 @@ func upload(w http.ResponseWriter, r *http.Request, return } - err = uploadFunc(namespace, project, uploadValue, coverage, string(bytes)) + err = uploadFunc(params[0], params[1], params[2], coverage, string(bytes)) if err != nil { http.Error(w, fmt.Sprintf("could not upload data to database: %v", err), http.StatusInternalServerError) return -- 2.38.4