No Description

main.go 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package main
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "flag"
  6. "fmt"
  7. "io"
  8. "io/ioutil"
  9. "log"
  10. "net/http"
  11. "os"
  12. "path/filepath"
  13. "regexp"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. )
  18. var (
  19. configFile = flag.String("c", "conf.json", "config file location")
  20. parsedconfig = conf{}
  21. Random *os.File
  22. lineRe = regexp.MustCompile(`^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`)
  23. )
  24. type conf struct {
  25. ListenPort string `json:"listenPort"`
  26. ShieldURL string `json:"shieldServerURL"`
  27. }
  28. type CoverageResult struct {
  29. Percent float64
  30. }
  31. type Profile struct {
  32. FileName string
  33. Mode string
  34. Blocks []ProfileBlock
  35. }
  36. type byFileName []*Profile
  37. func (p byFileName) Len() int { return len(p) }
  38. func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName }
  39. func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
  40. // ProfileBlock represents a single block of profiling data.
  41. type ProfileBlock struct {
  42. StartLine, StartCol int
  43. EndLine, EndCol int
  44. NumStmt, Count int
  45. }
  46. type blocksByStart []ProfileBlock
  47. func (b blocksByStart) Len() int { return len(b) }
  48. func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
  49. func (b blocksByStart) Less(i, j int) bool {
  50. bi, bj := b[i], b[j]
  51. return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol
  52. }
  53. func init() {
  54. f, err := os.Open("/dev/urandom")
  55. if err != nil {
  56. log.Fatal(err)
  57. }
  58. Random = f
  59. }
  60. func percentCovered(p *Profile) float64 {
  61. var total, covered int64
  62. for _, b := range p.Blocks {
  63. total += int64(b.NumStmt)
  64. if b.Count > 0 {
  65. covered += int64(b.NumStmt)
  66. }
  67. }
  68. if total == 0 {
  69. return 0
  70. }
  71. return float64(covered) / float64(total) * 100
  72. }
  73. // ParseProfiles parses profile data in the specified file and returns a
  74. // Profile for each source file described therein.
  75. func ParseProfiles(fileName string) ([]*Profile, error) {
  76. pf, err := os.Open(fileName)
  77. if err != nil {
  78. return nil, err
  79. }
  80. defer pf.Close()
  81. files := make(map[string]*Profile)
  82. buf := bufio.NewReader(pf)
  83. // First line is "mode: foo", where foo is "set", "count", or "atomic".
  84. // Rest of file is in the format
  85. // encoding/base64/base64.go:34.44,37.40 3 1
  86. // where the fields are: name.go:line.column,line.column numberOfStatements count
  87. s := bufio.NewScanner(buf)
  88. mode := ""
  89. for s.Scan() {
  90. line := s.Text()
  91. if mode == "" {
  92. const p = "mode: "
  93. if !strings.HasPrefix(line, p) || line == p {
  94. return nil, fmt.Errorf("bad mode line: %v", line)
  95. }
  96. mode = line[len(p):]
  97. continue
  98. }
  99. m := lineRe.FindStringSubmatch(line)
  100. if m == nil {
  101. return nil, fmt.Errorf("line %q doesn't match expected format: %v", m, lineRe)
  102. }
  103. fn := m[1]
  104. p := files[fn]
  105. if p == nil {
  106. p = &Profile{
  107. FileName: fn,
  108. Mode: mode,
  109. }
  110. files[fn] = p
  111. }
  112. p.Blocks = append(p.Blocks, ProfileBlock{
  113. StartLine: toInt(m[2]),
  114. StartCol: toInt(m[3]),
  115. EndLine: toInt(m[4]),
  116. EndCol: toInt(m[5]),
  117. NumStmt: toInt(m[6]),
  118. Count: toInt(m[7]),
  119. })
  120. }
  121. if err := s.Err(); err != nil {
  122. return nil, err
  123. }
  124. for _, p := range files {
  125. sort.Sort(blocksByStart(p.Blocks))
  126. }
  127. // Generate a sorted slice.
  128. profiles := make([]*Profile, 0, len(files))
  129. for _, profile := range files {
  130. profiles = append(profiles, profile)
  131. }
  132. sort.Sort(byFileName(profiles))
  133. return profiles, nil
  134. }
  135. func toInt(s string) int {
  136. i, err := strconv.Atoi(s)
  137. if err != nil {
  138. panic(err)
  139. }
  140. return i
  141. }
  142. func httpErrorf(w http.ResponseWriter, format string, a ...interface{}) {
  143. err := fmt.Errorf(format, a...)
  144. log.Println(err)
  145. http.Error(w, err.Error(), http.StatusInternalServerError)
  146. }
  147. func statusColor(coveragePct string) string {
  148. pctInt := toInt(coveragePct)
  149. switch {
  150. case pctInt <= 30:
  151. return "red"
  152. case pctInt > 30 && pctInt <= 75:
  153. return "yellow"
  154. case pctInt > 75:
  155. return "green"
  156. default:
  157. return "blue"
  158. }
  159. }
  160. func uploadHandler(config conf) http.Handler {
  161. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  162. if r.Method != "POST" {
  163. http.Error(w, "method not supported", http.StatusMethodNotAllowed)
  164. return
  165. }
  166. repoName := r.URL.Query().Get("repo")
  167. if _, err := os.Stat(repoName); err != nil {
  168. if os.IsNotExist(err) {
  169. log.Printf("Directory for %s does not exist, creating", repoName)
  170. if err := os.MkdirAll(repoName, 0755); err != nil {
  171. log.Printf("error creating directory for %s: %s", repoName, err)
  172. return
  173. }
  174. } else {
  175. log.Println("error inspecting: ", err)
  176. }
  177. }
  178. reader, err := r.MultipartReader()
  179. if err != nil {
  180. httpErrorf(w, "error creating multipart reader: %s", err)
  181. return
  182. }
  183. var dst *os.File
  184. for {
  185. part, err := reader.NextPart()
  186. if err == io.EOF {
  187. break
  188. }
  189. if part.FileName() == "" {
  190. continue
  191. }
  192. dst, err = os.Create(filepath.Join(repoName, "coverage.out"))
  193. if err != nil {
  194. httpErrorf(w, "error creating coverage file: %s", err)
  195. return
  196. }
  197. defer dst.Close()
  198. if _, err := io.Copy(dst, part); err != nil {
  199. httpErrorf(w, "error writing coverage file: %s", err)
  200. return
  201. }
  202. }
  203. profs, err := ParseProfiles(dst.Name())
  204. if err != nil {
  205. log.Println("Error parsing profile file: ", err)
  206. }
  207. percentCovered := percentCovered(profs[0])
  208. // redirect to shields API server
  209. roundedFloat := fmt.Sprintf("%.0f", percentCovered)
  210. log.Println("Coverage percent: ", roundedFloat)
  211. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  212. w.Header().Set("Pragma", "no-cache")
  213. w.Header().Set("Expires", "0")
  214. http.Redirect(w, r, config.ShieldURL+"/coverage-"+roundedFloat+"%25-"+statusColor(roundedFloat)+".svg", http.StatusSeeOther)
  215. })
  216. }
  217. func displayHandler(config conf) http.Handler {
  218. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  219. if r.Method != "GET" {
  220. http.Error(w, "method not supported", http.StatusMethodNotAllowed)
  221. return
  222. }
  223. repoName := r.URL.Query().Get("repo")
  224. if repoName == "" {
  225. http.Redirect(w, r, config.ShieldURL+"/coverage-NaN-red.svg", http.StatusSeeOther)
  226. return
  227. }
  228. profs, err := ParseProfiles(filepath.Join(repoName, "coverage.out"))
  229. if err != nil {
  230. httpErrorf(w, "Error parsing profile file: %s", err)
  231. return
  232. }
  233. percentCovered := percentCovered(profs[0])
  234. // get image from shields API server
  235. roundedFloat := fmt.Sprintf("%.0f", percentCovered)
  236. log.Println("Coverage percent: ", roundedFloat)
  237. log.Println("URL: ", config.ShieldURL+"/coverage-"+roundedFloat+"%25-"+statusColor(roundedFloat)+".svg")
  238. reqImg, err := http.Get(config.ShieldURL + "/coverage-" + roundedFloat + "%25-" + statusColor(roundedFloat) + ".svg")
  239. if err != nil {
  240. httpErrorf(w, "Error loading SVG from shield: %s", err)
  241. return
  242. }
  243. w.Header().Set("Content-Type", reqImg.Header.Get("Content-Type"))
  244. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  245. w.Header().Set("Pragma", "no-cache")
  246. w.Header().Set("Expires", "0")
  247. if _, err = io.Copy(w, reqImg.Body); err != nil {
  248. httpErrorf(w, "Error writing SVG to ResponseWriter: %s", err)
  249. return
  250. }
  251. reqImg.Body.Close()
  252. })
  253. }
  254. func main() {
  255. flag.Parse()
  256. file, err := ioutil.ReadFile(*configFile)
  257. if err != nil {
  258. log.Fatal("unable to read config file, exiting...")
  259. }
  260. if err := json.Unmarshal(file, &parsedconfig); err != nil {
  261. log.Fatal("unable to marshal config file, exiting...")
  262. }
  263. http.Handle("/upload", uploadHandler(parsedconfig))
  264. http.Handle("/display", displayHandler(parsedconfig))
  265. log.Println("running without SSL enabled")
  266. log.Fatal(http.ListenAndServe(":"+parsedconfig.ListenPort, nil))
  267. }