├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── config.go ├── config ├── default.json └── development.json ├── config_test.go ├── docs ├── jenkins-integration.png └── scm-polling.png ├── glide.lock ├── glide.yaml ├── health.go ├── health_test.go ├── metrics.go ├── metrics_test.go ├── uberalls.go ├── uberalls_suite_test.go └── uberalls_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | uberalls 3 | *.test 4 | 5 | # Fake source dir for jenkins 6 | src/ 7 | *.sqlite 8 | coverage.out 9 | *.xml 10 | *.cov 11 | *.html 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.7 6 | - tip 7 | 8 | before_install: 9 | - go get -u github.com/axw/gocov/gocov 10 | - go get -u github.com/mattn/goveralls 11 | - go get golang.org/x/tools/cmd/cover 12 | - go get github.com/Masterminds/glide 13 | - glide install 14 | 15 | script: 16 | - $HOME/gopath/bin/goveralls -service=travis-ci 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Uber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Uber Technologies, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | 22 | .DEFAULT_GOAL := test-console 23 | 24 | html_report := coverage.html 25 | test := gocov test 26 | coverfile := profile.cov 27 | 28 | test-console: 29 | $(test) | gocov report 30 | 31 | test: 32 | go test -covermode=count -coverprofile $(coverfile) 33 | 34 | testhtml: clean 35 | $(test) | gocov-html > $(html_report) && open $(html_report) 36 | 37 | clean: 38 | @rm $(html_report) || true 39 | 40 | travis: test 41 | goveralls -coverprofile=$(coverfile) -service=travis-ci 42 | 43 | .PHONY: test test-console lint testhtml clean travis 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uberalls [![Build Status](https://travis-ci.org/uber/uberalls.svg?branch=master)](https://travis-ci.org/uber/uberalls) [![Coverage Status](https://coveralls.io/repos/uber/uberalls/badge.svg)](https://coveralls.io/r/uber/uberalls) 2 | 3 | Code coverage metric storage service. Provide coverage metrics on differentials 4 | with [Phabricator][] and [Jenkins][], just like [Coveralls][] does for GitHub 5 | and TravisCI. 6 | 7 | [Phabricator]: http://phabricator.org/ 8 | [Jenkins]: https://jenkins-ci.org/ 9 | [Coveralls]: https://coveralls.io/ 10 | 11 | ## Running 12 | 13 | Configure your database by editing `config/default.json` or specify a different 14 | file by passing an `UBERALLS_CONFIG` environment variable. The development and 15 | test runs use a SQLite database. 16 | 17 | If you'd like to use MySQL, you could use the following configuration: 18 | 19 | ```json 20 | { 21 | "dbType": "mysql", 22 | "dbLocation": "user:password@/dbname?charset=utf8", 23 | "listenPort": 8080, 24 | "listenAddress": "0.0.0.0" 25 | } 26 | ``` 27 | 28 | ## Jenkins integration 29 | 30 | Uberalls works best when paired with our [Phabricator Jenkins Plugin][], which 31 | will record Cobertura data on master runs, and compare coverage to the base 32 | revision on differentials. 33 | 34 | ![Jenkins Integration](/docs/jenkins-integration.png) 35 | 36 | [Phabricator Jenkins Plugin]: https://github.com/uber/phabricator-jenkins-plugin 37 | 38 | On differentials, the delta in coverage is calculated by taking the base commit 39 | (from conduit metadata) and comparing that to the current coverage amount. 40 | 41 | In order to have a baseline to compare against, you must also have jenkins build 42 | your project on your mainline branch ("master" by default). You can either 43 | create a separate job, or enable SCM polling under Build Triggers: 44 | 45 | ![scm polling](/docs/scm-polling.png) 46 | 47 | ## Development 48 | 49 | Get the source 50 | 51 | ```bash 52 | go get github.com/uber/uberalls 53 | ``` 54 | 55 | Install Glide and dependencies 56 | 57 | ```bash 58 | cd $GOPATH/src/github.com/uber/uberalls 59 | go get github.com/Masterminds/glide 60 | glide install 61 | ``` 62 | 63 | Run the thing 64 | 65 | ```bash 66 | go build && ./uberalls 67 | ``` 68 | 69 | Run the tests 70 | 71 | ```bash 72 | go test . 73 | ``` 74 | 75 | ## License 76 | 77 | MIT Licensed 78 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "io/ioutil" 27 | "log" 28 | "os" 29 | 30 | "github.com/jinzhu/gorm" 31 | 32 | _ "github.com/go-sql-driver/mysql" 33 | _ "github.com/mattn/go-sqlite3" 34 | ) 35 | 36 | // DefaultConfig holds the default config path 37 | const DefaultConfig = "config/default.json" 38 | 39 | var configLocations = [...]string{ 40 | "UBERALLS_CONFIG", 41 | "UBERALLS_SECRETS", 42 | } 43 | 44 | // Config holds application configuration 45 | type Config struct { 46 | DBType string 47 | DBLocation string 48 | ListenPort int 49 | ListenAddress string 50 | db *gorm.DB 51 | } 52 | 53 | // ConnectionString returns a TCP string for the HTTP server to bind to 54 | func (c Config) ConnectionString() string { 55 | return fmt.Sprintf("%s:%d", c.ListenAddress, c.ListenPort) 56 | } 57 | 58 | // DB returns a database connection based on configuration 59 | func (c *Config) DB() (*gorm.DB, error) { 60 | if c.db == nil { 61 | newdb, err := gorm.Open(c.DBType, c.DBLocation) 62 | if err != nil { 63 | return nil, err 64 | } 65 | c.db = &newdb 66 | } 67 | return c.db, nil 68 | } 69 | 70 | // Automigrate runs migrations automatically 71 | func (c Config) Automigrate() error { 72 | db, err := c.DB() 73 | if err != nil { 74 | return err 75 | } 76 | model := new(Metric) 77 | db.AutoMigrate(model) 78 | 79 | db.Model(model).AddIndex( 80 | "idx_metrics_repository_sha_timestamp", 81 | "repository", 82 | "sha", 83 | "timestamp", 84 | ) 85 | db.Model(model).AddIndex( 86 | "idx_metrics_repository_branch_timestamp", 87 | "repository", 88 | "branch", 89 | "timestamp", 90 | ) 91 | return nil 92 | } 93 | 94 | // LoadConfigs loads from multiple config files, or default 95 | func LoadConfigs(c *Config, configPaths []string) (*Config, error) { 96 | if len(configPaths) == 0 { 97 | return LoadConfig(c, "") 98 | } 99 | for _, path := range configPaths { 100 | if _, err := LoadConfig(c, path); err != nil { 101 | return nil, err 102 | } 103 | } 104 | return c, nil 105 | } 106 | 107 | // LoadConfig loads configuration from a file into a Config type 108 | func LoadConfig(c *Config, configPath string) (*Config, error) { 109 | if configPath == "" { 110 | configPath = DefaultConfig 111 | } 112 | 113 | log.Print("Loading configuration from '", configPath, "'") 114 | content, err := ioutil.ReadFile(configPath) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if err = json.Unmarshal(content, c); err != nil { 120 | return nil, err 121 | } 122 | 123 | return c, nil 124 | } 125 | 126 | // GetLocationsFromEnvironment returns a list of possible configuration locations 127 | func GetLocationsFromEnvironment() []string { 128 | configPaths := make([]string, 0, len(configLocations)) 129 | for _, env := range configLocations { 130 | if envValue := os.Getenv(env); envValue != "" { 131 | configPaths = append(configPaths, envValue) 132 | } 133 | } 134 | return configPaths 135 | } 136 | 137 | // Configure sets up the app 138 | func Configure() (*Config, error) { 139 | log.Println("Configuring...") 140 | config := Config{} 141 | 142 | paths := GetLocationsFromEnvironment() 143 | return LoadConfigs(&config, paths) 144 | } 145 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbType": "sqlite3", 3 | "dbLocation": "development.sqlite", 4 | "listenAddress": "127.0.0.1", 5 | "listenPort": 14740 6 | } 7 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | default.json -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main_test 22 | 23 | import ( 24 | "os" 25 | 26 | . "github.com/uber/uberalls" 27 | 28 | . "github.com/onsi/ginkgo" 29 | . "github.com/onsi/gomega" 30 | ) 31 | 32 | var _ = Describe("File loading", func() { 33 | var c *Config 34 | 35 | BeforeEach(func() { 36 | c = &Config{} 37 | }) 38 | 39 | It("Should load a default file", func() { 40 | LoadConfig(c, "") 41 | Expect(c.DBType).ToNot(BeEmpty()) 42 | }) 43 | 44 | It("Should error on non-existent files", func() { 45 | _, err := LoadConfig(c, "non-existent") 46 | Expect(err).To(HaveOccurred()) 47 | Expect(c.DBType).To(BeEmpty()) 48 | }) 49 | 50 | It("Should error on bad paths", func() { 51 | _, err := LoadConfigs(c, []string{"non-existent"}) 52 | Expect(err).To(HaveOccurred()) 53 | }) 54 | 55 | It("Should work with good paths", func() { 56 | _, err := LoadConfigs(c, []string{DefaultConfig}) 57 | Expect(err).ToNot(HaveOccurred()) 58 | }) 59 | 60 | It("should configure", func() { 61 | _, err := Configure() 62 | Expect(err).ToNot(HaveOccurred()) 63 | }) 64 | 65 | Context("with environment", func() { 66 | var oldConfig string 67 | 68 | BeforeEach(func() { 69 | oldConfig = os.Getenv("UBERALLS_CONFIG") 70 | }) 71 | 72 | AfterEach(func() { 73 | os.Setenv("UBERALLS_CONFIG", oldConfig) 74 | }) 75 | 76 | It("should try loading a config", func() { 77 | os.Setenv("UBERALLS_CONFIG", "aoeu") 78 | _, err := Configure() 79 | Expect(err).To(HaveOccurred()) 80 | }) 81 | }) 82 | 83 | }) 84 | 85 | var _ = Describe("Database connections", func() { 86 | It("Should have a connection string", func() { 87 | c := Config{ 88 | ListenAddress: "somehost", 89 | ListenPort: 1, 90 | } 91 | 92 | Expect(c.ConnectionString()).To(Equal("somehost:1")) 93 | }) 94 | 95 | Context("With an invalid connection", func() { 96 | var c *Config 97 | 98 | BeforeEach(func() { 99 | c = &Config{ 100 | DBType: "unknown", 101 | DBLocation: "mars", 102 | } 103 | }) 104 | 105 | It("Should throw an error", func() { 106 | conn, err := c.DB() 107 | Expect(conn).To(BeNil()) 108 | Expect(err).To(HaveOccurred()) 109 | }) 110 | 111 | It("Should error trying to automigrate", func() { 112 | Expect(c.Automigrate()).ToNot(Succeed()) 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /docs/jenkins-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uberalls/0ed7abfad746fa2ba8688b1f696dd4ce419027f2/docs/jenkins-integration.png -------------------------------------------------------------------------------- /docs/scm-polling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uberalls/0ed7abfad746fa2ba8688b1f696dd4ce419027f2/docs/scm-polling.png -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 806b7990676ebce531a2aab79ab070717f8fc44a0841d6c00d35127cebea71ab 2 | updated: 2016-12-09T12:08:35.379451859+11:00 3 | imports: 4 | - name: github.com/go-sql-driver/mysql 5 | version: 9543750295406ef070f7de8ae9c43ccddd44e15e 6 | - name: github.com/jinzhu/gorm 7 | version: 52ba1858c31dcbe1573f46f994d3cdf334729ff4 8 | - name: github.com/lib/pq 9 | version: 2fe70547fcfbd6deedadd4af3702af16c0297723 10 | subpackages: 11 | - hstore 12 | - name: github.com/mattn/go-sqlite3 13 | version: da2bf8a0f356364fb8189a5a276b5acf77839d30 14 | - name: github.com/onsi/ginkgo 15 | version: e57363034d6f4d61cd9ef0b6917f69ad03977914 16 | subpackages: 17 | - config 18 | - internal/codelocation 19 | - internal/containernode 20 | - internal/failer 21 | - internal/leafnodes 22 | - internal/remote 23 | - internal/spec 24 | - internal/specrunner 25 | - internal/suite 26 | - internal/testingtproxy 27 | - internal/writer 28 | - reporters 29 | - reporters/stenographer 30 | - types 31 | - name: github.com/onsi/gomega 32 | version: 982c859aeeffd81ab8717403fb03c379b6c92b5f 33 | subpackages: 34 | - format 35 | - internal/assertion 36 | - internal/asyncassertion 37 | - internal/testingtsupport 38 | - matchers 39 | - matchers/support/goraph/bipartitegraph 40 | - matchers/support/goraph/edge 41 | - matchers/support/goraph/node 42 | - matchers/support/goraph/util 43 | - types 44 | testImports: [] 45 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/uber/uberalls 2 | import: 3 | - package: github.com/go-sql-driver/mysql 4 | version: 9543750295406ef070f7de8ae9c43ccddd44e15e 5 | - package: github.com/jinzhu/gorm 6 | version: 52ba1858c31dcbe1573f46f994d3cdf334729ff4 7 | - package: github.com/lib/pq 8 | version: 2fe70547fcfbd6deedadd4af3702af16c0297723 9 | subpackages: 10 | - hstore 11 | - package: github.com/mattn/go-sqlite3 12 | version: da2bf8a0f356364fb8189a5a276b5acf77839d30 13 | - package: github.com/onsi/ginkgo 14 | version: e57363034d6f4d61cd9ef0b6917f69ad03977914 15 | - package: github.com/onsi/gomega 16 | version: 982c859aeeffd81ab8717403fb03c379b6c92b5f 17 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "net/http" 25 | 26 | "github.com/jinzhu/gorm" 27 | ) 28 | 29 | // HealthHandler handles HTTP requests for health 30 | type HealthHandler struct { 31 | db *gorm.DB 32 | } 33 | 34 | // NewHealthHandler instantiates a new handler for health checking 35 | func NewHealthHandler(db *gorm.DB) HealthHandler { 36 | return HealthHandler{db: db} 37 | } 38 | 39 | // ServeHTP handles the health endpoint 40 | func (h HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 41 | w.Header().Set("Content-Type", "text/plain") 42 | if err := h.db.DB().Ping(); err != nil { 43 | w.WriteHeader(http.StatusInternalServerError) 44 | w.Write([]byte(err.Error())) 45 | return 46 | } 47 | w.Write([]byte(";-)\n")) 48 | } 49 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main_test 22 | 23 | import ( 24 | "net/http" 25 | "net/http/httptest" 26 | 27 | . "github.com/onsi/ginkgo" 28 | . "github.com/onsi/gomega" 29 | . "github.com/uber/uberalls" 30 | ) 31 | 32 | var _ = Describe("Health handler", func() { 33 | var response *httptest.ResponseRecorder 34 | var c Config 35 | 36 | JustBeforeEach(func() { 37 | request, _ := http.NewRequest("GET", "/health", nil) 38 | response = httptest.NewRecorder() 39 | 40 | db, err := c.DB() 41 | Expect(err).ToNot(HaveOccurred()) 42 | 43 | handler := NewHealthHandler(db) 44 | handler.ServeHTTP(response, request) 45 | }) 46 | 47 | Context("With a valid DB connection", func() { 48 | BeforeEach(func() { 49 | c = Config{ 50 | DBType: "sqlite3", 51 | DBLocation: "test.sqlite", 52 | } 53 | }) 54 | 55 | It("Should be HTTP OK", func() { 56 | Expect(response.Code).To(Equal(http.StatusOK)) 57 | }) 58 | }) 59 | 60 | Context("With an invalid DB connection", func() { 61 | BeforeEach(func() { 62 | c = Config{ 63 | DBType: "mysql", 64 | DBLocation: ":1111", 65 | } 66 | }) 67 | 68 | It("should not be OK", func() { 69 | Expect(response.Code).To(Equal(http.StatusInternalServerError)) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "io" 28 | "log" 29 | "net/http" 30 | "net/url" 31 | "time" 32 | 33 | "github.com/jinzhu/gorm" 34 | ) 35 | 36 | // Metric represents code coverage 37 | type Metric struct { 38 | ID int64 `gorm:"primary_key:yes" json:"id"` 39 | Repository string `sql:"not null" json:"repository"` 40 | Sha string `sql:"not null" json:"sha"` 41 | Branch string `json:"branch"` 42 | PackageCoverage float64 `sql:"not null" json:"packageCoverage"` 43 | FilesCoverage float64 `sql:"not null" json:"filesCoverage"` 44 | ClassesCoverage float64 `sql:"not null" json:"classesCoverage"` 45 | MethodCoverage float64 `sql:"not null" json:"methodCoverage"` 46 | LineCoverage float64 `sql:"not null" json:"lineCoverage"` 47 | ConditionalCoverage float64 `sql:"not null" json:"conditionalCoverage"` 48 | Timestamp int64 `sql:"not null" json:"timestamp"` 49 | LinesCovered int64 `sql:"not null" json:"linesCovered"` 50 | LinesTested int64 `sql:"not null" json:"linesTested"` 51 | } 52 | 53 | type errorResponse struct { 54 | Error string `json:"error"` 55 | } 56 | 57 | // MetricsHandler represents a metrics handler 58 | type MetricsHandler struct { 59 | db *gorm.DB 60 | } 61 | 62 | const defaultBranch = "origin/master" 63 | 64 | func writeError(w io.Writer, message string, err error) { 65 | formattedMessage := fmt.Sprintf("%s: %v", message, err) 66 | 67 | log.Println(formattedMessage) 68 | 69 | errorMsg := errorResponse{ 70 | Error: formattedMessage, 71 | } 72 | 73 | errorString, encodingError := json.Marshal(errorMsg) 74 | if encodingError != nil { 75 | encodingErrorMessage := fmt.Sprintf("Unable to encode response message %v", encodingError) 76 | log.Printf(encodingErrorMessage) 77 | } 78 | 79 | w.Write(errorString) 80 | } 81 | 82 | func respondWithMetric(w http.ResponseWriter, m Metric) { 83 | bodyString, err := json.Marshal(m) 84 | if err != nil { 85 | w.WriteHeader(http.StatusBadRequest) 86 | writeError(w, "unable to encode response", err) 87 | return 88 | } 89 | 90 | w.Write([]byte(bodyString)) 91 | } 92 | 93 | // ExtractMetricQuery extracts a query from the request 94 | func ExtractMetricQuery(form url.Values) Metric { 95 | repository := form["repository"][0] 96 | query := Metric{ 97 | Repository: repository, 98 | } 99 | 100 | if len(form["sha"]) < 1 { 101 | if len(form["branch"]) < 1 { 102 | query.Branch = defaultBranch 103 | } else { 104 | query.Branch = form["branch"][0] 105 | } 106 | } else { 107 | query.Sha = form["sha"][0] 108 | } 109 | return query 110 | } 111 | 112 | func (mh MetricsHandler) handleMetricsQuery(w http.ResponseWriter, r *http.Request) { 113 | if err := r.ParseForm(); err != nil { 114 | w.WriteHeader(http.StatusBadRequest) 115 | writeError(w, "error parsing params", err) 116 | return 117 | } 118 | log.Printf("Handling incoming request: %s", r.Form) 119 | 120 | if len(r.Form["repository"]) < 1 { 121 | w.WriteHeader(http.StatusBadRequest) 122 | writeError(w, "missing 'repository'", errors.New("need repository")) 123 | return 124 | } 125 | 126 | query := ExtractMetricQuery(r.Form) 127 | 128 | m := new(Metric) 129 | dbQuery := mh.db.Where(&query) 130 | if len(r.Form["until"]) > 0 { 131 | dbQuery = dbQuery.Where("timestamp <= ? ", r.Form["until"][0]) 132 | } 133 | dbQuery.Order("timestamp desc").First(m) 134 | 135 | if m.ID == 0 { 136 | w.WriteHeader(http.StatusNotFound) 137 | writeError(w, "no rows found", errors.New("-")) 138 | return 139 | } 140 | 141 | respondWithMetric(w, *m) 142 | } 143 | 144 | func (mh MetricsHandler) handleMetricsSave(w http.ResponseWriter, r *http.Request) { 145 | if r.Body == nil { 146 | w.WriteHeader(http.StatusBadRequest) 147 | writeError(w, "no response body", errors.New("nil body")) 148 | return 149 | } 150 | 151 | decoder := json.NewDecoder(r.Body) 152 | m := new(Metric) 153 | if err := decoder.Decode(m); err != nil { 154 | w.WriteHeader(http.StatusBadRequest) 155 | writeError(w, "unable to decode body", err) 156 | return 157 | } 158 | log.Printf("Recording metric %v", m) 159 | 160 | if err := mh.RecordMetric(m); err != nil { 161 | w.WriteHeader(http.StatusBadRequest) 162 | writeError(w, "error recording metric", err) 163 | } else { 164 | respondWithMetric(w, *m) 165 | } 166 | } 167 | 168 | // RecordMetric saves a Metric to the database 169 | func (mh MetricsHandler) RecordMetric(m *Metric) error { 170 | if m.Repository == "" || m.Sha == "" { 171 | return errors.New("missing required field") 172 | } 173 | 174 | if m.Timestamp == 0 { 175 | m.Timestamp = time.Now().Unix() 176 | } 177 | 178 | mh.db.Create(m) 179 | return nil 180 | } 181 | 182 | type handler func(w http.ResponseWriter, r *http.Request) 183 | 184 | // NewMetricsHandler creates a new MetricsHandler 185 | func NewMetricsHandler(db *gorm.DB) MetricsHandler { 186 | return MetricsHandler{ 187 | db: db, 188 | } 189 | } 190 | 191 | // ServeHTTP handles an HTTP request for metrics 192 | func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 193 | w.Header().Set("Content-Type", "application/json") 194 | 195 | if r.Method == "GET" { 196 | mh.handleMetricsQuery(w, r) 197 | } else { 198 | mh.handleMetricsSave(w, r) 199 | } 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main_test 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "net/http" 27 | "net/http/httptest" 28 | "net/url" 29 | "strings" 30 | 31 | "github.com/jinzhu/gorm" 32 | 33 | . "github.com/onsi/ginkgo" 34 | . "github.com/onsi/gomega" 35 | . "github.com/uber/uberalls" 36 | ) 37 | 38 | func getMetricsResponse(method string, body *strings.Reader, params string, db *gorm.DB) *httptest.ResponseRecorder { 39 | var request *http.Request 40 | url := "/metrics" 41 | if params != "" { 42 | url = fmt.Sprintf("%s?%s", url, params) 43 | } 44 | if body == nil { 45 | request, _ = http.NewRequest(method, url, nil) 46 | } else { 47 | request, _ = http.NewRequest(method, url, body) 48 | } 49 | response := httptest.NewRecorder() 50 | handler := NewMetricsHandler(db) 51 | handler.ServeHTTP(response, request) 52 | return response 53 | } 54 | 55 | var _ = Describe("/metrics handler", func() { 56 | var ( 57 | c *Config 58 | db *gorm.DB 59 | ) 60 | 61 | BeforeEach(func() { 62 | c = &Config{ 63 | DBType: "sqlite3", 64 | DBLocation: "test.sqlite", 65 | } 66 | db, _ = c.DB() 67 | }) 68 | 69 | var response *httptest.ResponseRecorder 70 | It("Should automigrate", func() { 71 | Expect(c.Automigrate()).To(Succeed()) 72 | }) 73 | 74 | It("Should generate a 404 for non-existent repos", func() { 75 | response = getMetricsResponse("GET", nil, "repository=foo", db) 76 | Expect(response.Code).To(Equal(http.StatusNotFound)) 77 | }) 78 | 79 | Context("With an empty POST body", func() { 80 | response = getMetricsResponse("POST", nil, "", db) 81 | 82 | It("Should be non-OK", func() { 83 | Expect(response.Code).ToNot(Equal(http.StatusOK)) 84 | }) 85 | }) 86 | 87 | Context("With valid JSON", func() { 88 | dummyJSON := `{ 89 | "repository": "test", "packageCoverage": 38, "filesCoverage": 39, 90 | "classesCoverage": 40, "methodCoverage": 41, "lineCoverage": 42, 91 | "conditionalCoverage": 43, "sha": "deadbeef"}` 92 | 93 | BeforeEach(func() { 94 | response = getMetricsResponse("POST", strings.NewReader(dummyJSON), "", db) 95 | }) 96 | 97 | It("Should be HTTP OK", func() { 98 | Expect(response.Code).To(Equal(http.StatusOK)) 99 | }) 100 | 101 | It("Should decode a metric", func() { 102 | metric := new(Metric) 103 | decoder := json.NewDecoder(response.Body) 104 | Expect(decoder.Decode(metric)).To(Succeed()) 105 | Expect(metric.ID).To(BeNumerically(">", 0)) 106 | }) 107 | 108 | Context("Retrieving the metric", func() { 109 | var metric *Metric 110 | 111 | BeforeEach(func() { 112 | response = getMetricsResponse("GET", nil, "repository=test&sha=deadbeef", db) 113 | }) 114 | 115 | It("Should be HTTP OK", func() { 116 | Expect(response.Code).To(Equal(http.StatusOK)) 117 | }) 118 | 119 | It("Should decode the metric", func() { 120 | metric = new(Metric) 121 | decoder := json.NewDecoder(response.Body) 122 | Expect(decoder.Decode(metric)).To(Succeed()) 123 | }) 124 | 125 | It("Should have the correct values", func() { 126 | Expect(metric.PackageCoverage).To(Equal(38.)) 127 | Expect(metric.FilesCoverage).To(Equal(39.)) 128 | Expect(metric.ClassesCoverage).To(Equal(40.)) 129 | Expect(metric.MethodCoverage).To(Equal(41.)) 130 | Expect(metric.LineCoverage).To(Equal(42.)) 131 | Expect(metric.ConditionalCoverage).To(Equal(43.)) 132 | }) 133 | }) 134 | }) 135 | 136 | Context("With multiple records for a repository", func() { 137 | record1 := `{ 138 | "repository": "test2", "packageCoverage": 38, "filesCoverage": 39, 139 | "classesCoverage": 40, "methodCoverage": 41, "lineCoverage": 42, 140 | "conditionalCoverage": 43, "sha": "deadbeef", 141 | "timestamp": 1480636700}` 142 | record2 := `{ 143 | "repository": "test2", "packageCoverage": 38, "filesCoverage": 39, 144 | "classesCoverage": 40, "methodCoverage": 41, "lineCoverage": 42, 145 | "conditionalCoverage": 46, "sha": "deadbeef", 146 | "timestamp": 1480636760}` 147 | 148 | BeforeEach(func() { 149 | response = getMetricsResponse("POST", strings.NewReader(record1), "", db) 150 | Expect(response.Code).To(Equal(http.StatusOK)) 151 | response = getMetricsResponse("POST", strings.NewReader(record2), "", db) 152 | Expect(response.Code).To(Equal(http.StatusOK)) 153 | }) 154 | 155 | Context("Retrieving the latest metric", func() { 156 | It("Should have the correct values", func() { 157 | response = getMetricsResponse("GET", nil, "repository=test2&sha=deadbeef", db) 158 | metric := new(Metric) 159 | json.NewDecoder(response.Body).Decode(metric) 160 | 161 | Expect(metric.Timestamp).To(Equal(int64(1480636760))) 162 | Expect(metric.ConditionalCoverage).To(Equal(46.)) 163 | }) 164 | }) 165 | 166 | Context("Retrieving historical metric", func() { 167 | It("Should have the correct values", func() { 168 | response = getMetricsResponse("GET", nil, "repository=test2&sha=deadbeef&until=1480636730", db) 169 | metric := new(Metric) 170 | json.NewDecoder(response.Body).Decode(metric) 171 | 172 | Expect(metric.Timestamp).To(Equal(int64(1480636700))) 173 | Expect(metric.ConditionalCoverage).To(Equal(43.)) 174 | }) 175 | }) 176 | 177 | Context("Retrieving historical metric at exact moment", func() { 178 | It("Should have the correct values", func() { 179 | response = getMetricsResponse("GET", nil, "repository=test2&sha=deadbeef&until=1480636700", db) 180 | metric := new(Metric) 181 | json.NewDecoder(response.Body).Decode(metric) 182 | 183 | Expect(metric.Timestamp).To(Equal(int64(1480636700))) 184 | Expect(metric.ConditionalCoverage).To(Equal(43.)) 185 | }) 186 | }) 187 | 188 | Context("Retrieving from before metrics exist", func() { 189 | It("Should be HTTP 404", func() { 190 | response = getMetricsResponse("GET", nil, "repository=test2&sha=deadbeef&until=1480636699", db) 191 | Expect(response.Code).To(Equal(http.StatusNotFound)) 192 | }) 193 | }) 194 | }) 195 | 196 | Context("With invalid JSON", func() { 197 | badJSON := `{}` 198 | 199 | BeforeEach(func() { 200 | response = getMetricsResponse("POST", strings.NewReader(badJSON), "", db) 201 | }) 202 | 203 | It("Should not be OK posting an empty JSON body", func() { 204 | Expect(response.Code).ToNot(Equal(http.StatusOK)) 205 | }) 206 | }) 207 | 208 | Context("With bad configuration", func() { 209 | badConfig := Config{ 210 | DBType: "unknown", 211 | } 212 | 213 | BeforeEach(func() { 214 | db, _ := badConfig.DB() 215 | response = getMetricsResponse("GET", nil, "", db) 216 | }) 217 | 218 | It("Should not be OK", func() { 219 | Expect(response.Code).ToNot(Equal(http.StatusOK)) 220 | }) 221 | }) 222 | }) 223 | 224 | var _ = Describe("ExtractMetricsQuery", func() { 225 | Context("When branch is specified in the query", func() { 226 | values := url.Values{ 227 | "repository": []string{"foo"}, 228 | "branch": []string{"master"}, 229 | } 230 | query := ExtractMetricQuery(values) 231 | 232 | It("Should extract the master branch", func() { 233 | Expect(query.Branch).To(Equal("master")) 234 | }) 235 | }) 236 | 237 | Context("When no branch is specified", func() { 238 | values := url.Values{ 239 | "repository": []string{"foo"}, 240 | } 241 | query := ExtractMetricQuery(values) 242 | 243 | It("Should extract the default branch", func() { 244 | Expect(query.Branch).To(Equal("origin/master")) 245 | }) 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /uberalls.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "log" 25 | "net/http" 26 | ) 27 | 28 | // MakeServeMux instantiates an http ServeMux for the server 29 | func MakeServeMux(config *Config) *http.ServeMux { 30 | db, err := config.DB() 31 | if err != nil { 32 | log.Fatalf("Unable to initialize DB connection: %v", err) 33 | } 34 | 35 | if err := config.Automigrate(); err != nil { 36 | log.Fatalf("Could not establish database connection: %v", err) 37 | } 38 | mux := http.NewServeMux() 39 | mux.Handle("/health", NewHealthHandler(db)) 40 | mux.Handle("/metrics", NewMetricsHandler(db)) 41 | 42 | return mux 43 | } 44 | 45 | func main() { 46 | config, err := Configure() 47 | if err != nil { 48 | log.Fatalf("Unable to load configuration: %s", err) 49 | } 50 | 51 | mux := MakeServeMux(config) 52 | listenString := config.ConnectionString() 53 | log.Printf("Listening on %s... ", listenString) 54 | 55 | log.Fatal(http.ListenAndServe(listenString, mux)) 56 | } 57 | -------------------------------------------------------------------------------- /uberalls_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main_test 22 | 23 | import ( 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | 27 | "github.com/onsi/ginkgo/reporters" 28 | "testing" 29 | ) 30 | 31 | func TestUberalls(t *testing.T) { 32 | RegisterFailHandler(Fail) 33 | junitReporter := reporters.NewJUnitReporter("junit.xml") 34 | RunSpecsWithDefaultAndCustomReporters(t, "Uberalls Suite", []Reporter{junitReporter}) 35 | } 36 | -------------------------------------------------------------------------------- /uberalls_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main_test 22 | 23 | import ( 24 | "net/http" 25 | "os" 26 | 27 | . "github.com/onsi/ginkgo" 28 | . "github.com/onsi/gomega" 29 | . "github.com/uber/uberalls" 30 | ) 31 | 32 | var _ = Describe("Uberalls main", func() { 33 | var mux *http.ServeMux 34 | BeforeEach(func() { 35 | config, err := Configure() 36 | Expect(err).ToNot(HaveOccurred()) 37 | mux = MakeServeMux(config) 38 | }) 39 | 40 | It("should exist", func() { 41 | Expect(mux).ToNot(BeNil()) 42 | }) 43 | 44 | It("should configure", func() { 45 | _, err := Configure() 46 | Expect(err).ToNot(HaveOccurred()) 47 | }) 48 | 49 | Context("with environment", func() { 50 | var oldConfig string 51 | 52 | BeforeEach(func() { 53 | oldConfig = os.Getenv("UBERALLS_CONFIG") 54 | }) 55 | 56 | AfterEach(func() { 57 | os.Setenv("UBERALLS_CONFIG", oldConfig) 58 | }) 59 | 60 | It("should try loading a config", func() { 61 | os.Setenv("UBERALLS_CONFIG", "aoeu") 62 | _, err := Configure() 63 | Expect(err).To(HaveOccurred()) 64 | }) 65 | }) 66 | }) 67 | --------------------------------------------------------------------------------