├── .bumpversion.cfg ├── .codeclimate.yml ├── .fgo ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── LICENSES.md ├── Makefile ├── README.md ├── assets ├── Dockerfile ├── Dockerfile-go ├── Dockerfile-python ├── entrypoint ├── entrypoint-go └── entrypoint-python ├── command ├── archive.go ├── archive_test.go ├── assets.go ├── display.go ├── docker.go ├── docker_test.go ├── entrypoint.go ├── entrypoint_test.go ├── errset.go ├── errset_test.go ├── run.go ├── run_test.go ├── travis.go ├── travis_go.go ├── travis_go_test.go ├── travis_python.go ├── travis_python_test.go ├── travis_test.go └── yaml.go ├── commands.go ├── docs ├── .gitignore ├── config.toml ├── content │ ├── README.md │ └── info │ │ └── LICENSES.md ├── layouts │ └── partials │ │ ├── footer.html │ │ └── header.html └── static │ └── img │ ├── dna.png │ ├── favicon.ico │ ├── gopher.png │ ├── icon_16.png │ ├── icon_32.png │ ├── icon_96.png │ ├── image.png │ ├── python.png │ ├── small-logo.png │ ├── small_h-trans.png │ ├── travis-ci-small.png │ └── travis-ci.png ├── main.go ├── version.go └── wercker.yml /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.5.3 3 | commit = True 4 | 5 | [bumpversion:file:README.md] 6 | 7 | [bumpversion:file:version.go] 8 | 9 | [bumpversion:file:docs/content/README.md] 10 | 11 | [bumpversion:file:homebrew/README.md] 12 | 13 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | fixme: 3 | enabled: true 4 | config: 5 | strings: 6 | - FIXME 7 | - BUG 8 | - TODO 9 | gofmt: 10 | enabled: true 11 | golint: 12 | enabled: true 13 | govet: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.go" 18 | exclude_paths: 19 | - "**_test.go" 20 | -------------------------------------------------------------------------------- /.fgo: -------------------------------------------------------------------------------- 1 | package="pkg" 2 | homebrew="homebrew" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | homebrew 3 | *.bak 4 | loci 5 | .wercker 6 | 7 | ### https://raw.github.com/github/gitignore/9f044c43d2fa998acb2d468367a349fff609dc84/go.gitignore 8 | 9 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 10 | *.o 11 | *.a 12 | *.so 13 | 14 | # Folders 15 | _obj 16 | _test 17 | 18 | # Architecture specific extensions/prefixes 19 | *.[568vq] 20 | [568vq].out 21 | 22 | *.cgo1.go 23 | *.cgo2.c 24 | _cgo_defun.c 25 | _cgo_gotypes.go 26 | _cgo_export.* 27 | 28 | _testmain.go 29 | 30 | *.exe 31 | *.test 32 | *.prof 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/themes/github-project-landing-page"] 2 | path = docs/themes/github-project-landing-page 3 | url = https://github.com/jkawamoto/github-project-landing-page.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8.3 4 | env: 5 | - TEST_ENV=true TEST_ENV2=false 6 | - TEST_ENV=false TEST_ENV2=true 7 | install: 8 | - make get-deps 9 | script: 10 | - echo $TEST_ENV 11 | - if [[ $TEST_ENV = true ]]; then make test; fi 12 | notifications: 13 | slack: 14 | secure: JGVcOt3k+ehYGgtmpRtSeTFNV+y5nnJzKAlt4KXctE0vfQZbvTXnkYYzVVbPCqcx8JaGkuNHMdNBiELAmzYoQfFjo6u6kVKYv+1kzHwVmhwDpr0zXLDWnoRslNVsyIxEzBXi0UwXHEDyG/g2IyUsSu3jX0HExIZkN0aMUI0mPZ38FtHgd997kPN9nW8CaPVWJNH3xIIZc5wKgP6IpzczmR85r3yugHIB6XODzX1C0Xjx5ZZYYib9Fj1n3u35bVSaLaID2JZ+IbvSl05ljo04d+tOSLvzoB0SvlAY4+tftMANjzC0UfjkzcmZEL9F8NDxOzR0PMnnMWiQwQz7ivI+q4JQf/QwpQKMmMOsnIPTZZ0H1vwr33aDuI8SJFQaaQSQG2dX/JxZcMvQe/1IH+n4Js0G4WxUJg//V2uMjj7/y9zrhirozwC5McMJG94NNsDbKNsymYh5PVOoA/QILbABa8w1uqF52uar5q0hj6NG/lHW1AADCl0Uq0pbJFTjP0zoQwsSdS4hIlZCvWUlyNAKZGp2lnO/7T4Zd8ay1zi8j0QiruyuDOLIl5nS5XMime2Av+1qiHlrIPx9ev/IAkWfOj60ODnIeOmIoqyRbyC9+u7hwTMFQlysxN1ykaQ790I914deVcBhBRwoQy9N+5NuNQqiy31LdhobR8bcGmjwEYc= 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.3 (2017-09-18) 2 | ### Update 3 | - Use Ubuntu Trusty as the base image according to Travis CI's 4 | [update](https://twitter.com/travisci/status/888472266455089152). 5 | 6 | ### Fixed 7 | - Follow [gocui](https://github.com/jroimartin/gocui) library method rename. (PR #5) 8 | 9 | 10 | ## 0.5.2 (2017-07-18) 11 | ### Update 12 | - Support remote docker servers. 13 | - Output logging messages whenever some of tests are failed. 14 | 15 | ### Fixed 16 | - Missing global environment variables. 17 | - Missing packages and reducing sandbox image sizes. 18 | 19 | 20 | ## 0.5.1 (2017-06-29) 21 | ### Update 22 | - Skipping tests for unsupported python versions 23 | 24 | ### Fixed 25 | - Failed to parse attribute env if it contains secret values 26 | - Missing apt package ccache 27 | - Bugs about user interface 28 | 29 | 30 | ## 0.5.0 (2017-06-24) 31 | ### Update 32 | - Support parallel running tests. 33 | - Support `--select`/`-s` flag to specify a runtime version tests will run on. 34 | - Delete `--verbose` flag but add `--log` flag to store logging information to files. 35 | 36 | ### Fixed 37 | - no-color mode. 38 | 39 | 40 | ## 0.4.5 (2017-06-19) 41 | ### Update 42 | - Improve test preparation time. 43 | - Use sub shell to execute each command. 44 | 45 | 46 | ## 0.4.4 (2017-06-09) 47 | ### Update 48 | - To follow the update of docker client library. 49 | 50 | ### Fixed 51 | - Provide sudo command in travis scripts. 52 | - Parse quoted env. 53 | 54 | 55 | ## 0.4.3 (2017-02-10) 56 | ### Added 57 | - Install sudo command to support old style Travis's configuration files. 58 | 59 | ### Fixed 60 | - Parsing quoted environment variables. 61 | 62 | 63 | ## 0.4.2 (2017-02-01) 64 | ### Added 65 | - Support no-color and no-build-cache options. 66 | 67 | ### Fixed 68 | - before_install, install, before_script, and script attributes support both a single string and a list of strings. 69 | 70 | 71 | ## 0.4.1 (2017-01-31) 72 | ### Update 73 | - Allow unbounded variables in Travis's scripts. 74 | 75 | 76 | ## 0.4.0 (2017-01-31) 77 | ### Added 78 | - Build images based on installed runtime to reduce preparation time. 79 | 80 | ### Fixed 81 | - Avoid installing apt packages when another version number is given. 82 | - env attribute in .travis.yml allows both a string and a list of strings, 83 | - Before install step allows define environment variables, 84 | - Garbled characters in outputs of containers. 85 | 86 | 87 | ## 0.3.5 (2017-01-26) 88 | ### Fixed 89 | - To lower repository names to create a docker image since docker tags allow only lower characters. 90 | 91 | 92 | ## 0.3.4 (2017-01-25) 93 | ### Fixed 94 | - Stop testing when building a test image is failed. 95 | 96 | ### Update 97 | - Use aria2 to prepare virtual environments for matrix evaluations for Python. 98 | 99 | 100 | ## 0.3.3 (2017-01-10) 101 | ### Fixed 102 | - Declaring environment variables in startup scripts. 103 | 104 | 105 | ## 0.3.2 (2017-01-09) 106 | ### Fixed 107 | - Running containers weren't stopped when loci is canceled, 108 | - Use newer python version when an ambiguous version given, 109 | - Fix garbled characters from outputs. 110 | 111 | 112 | ## 0.3.1 (2017-01-07) 113 | ### Added 114 | - Support build matrices for go projects. 115 | - Use repository names as a part of tag names of container images. 116 | 117 | ### Fixed 118 | - Use correct archived source files. 119 | 120 | 121 | ## 0.3.0 (2016-12-14) 122 | ### Added 123 | - Support matrix build for python projects. 124 | 125 | ### Fixed 126 | - Set `GOPATH` and add $GOPATH/bin to `PATH`. 127 | 128 | 129 | ## 0.2.1 (2016-08-13) 130 | ### Fixed 131 | - Dead lock when tarballing raises errors, 132 | - List up functions gave directories to tarballing. 133 | 134 | 135 | ## 0.2.0 (2016-07-28) 136 | ### Added 137 | Proxy supports: 138 | - `--apt-proxy` flag sets a proxy URL for `apt-get` command, 139 | - `--pypi-proxy` flag sets a proxy URL for `pip` command, 140 | - `--http-proxy`, `--https-proxy`, and `--no-proxy` flags set http and https 141 | proxies. 142 | 143 | 144 | ## 0.1.5 (2016-07-22) 145 | ### Fixed 146 | - Git directory will be added to a container so that test program can access 147 | repository information. 148 | 149 | 150 | ## 0.1.4 (2016-07-21) 151 | ### Added 152 | - Support golang. 153 | 154 | 155 | ## 0.1.3 (2016-07-19) 156 | ### Added 157 | - Support verbose mode, 158 | - Use customized Dockerfile for each language, 159 | - Support base flag to switch base image. 160 | 161 | If choose an image previous run, some installations might be omitted. 162 | It could reduce running time. 163 | 164 | 165 | ## 0.1.2 (2016-07-19) 166 | ### Added 167 | - Support name and tag option to set container name and image tag. 168 | 169 | 170 | ## 0.1.1 (2016-07-18) 171 | ### Fixed 172 | - Bugs about temporary directories, forget creating and deleting them. 173 | 174 | 175 | ## 0.1.0 (2016-07-18) 176 | Initial release 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Junpei Kawamoto 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 | -------------------------------------------------------------------------------- /LICENSES.md: -------------------------------------------------------------------------------- 1 | # License 2 | This software is released under the MIT License. 3 | 4 | > The MIT License (MIT) 5 | > 6 | > Copyright (c) 2016-2017 Junpei Kawamoto 7 | > 8 | > Permission is hereby granted, free of charge, to any person obtaining a copy 9 | > of this software and associated documentation files (the "Software"), to deal 10 | > in the Software without restriction, including without limitation the rights 11 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | > copies of the Software, and to permit persons to whom the Software is 13 | > furnished to do so, subject to the following conditions: 14 | > 15 | > The above copyright notice and this permission notice shall be included in all 16 | > copies or substantial portions of the Software. 17 | > 18 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | > SOFTWARE. 25 | 26 | 27 | # Notices for libraries 28 | This software uses the following open source libraries: 29 | 30 | ## [chalk](https://github.com/ttacon/chalk) 31 | 32 | > Copyright (c) 2014 Trey Tacon 33 | > 34 | > Licensed under the MIT License. 35 | > 36 | > Permission is hereby granted, free of charge, to any person obtaining a copy 37 | > of this software and associated documentation files (the "Software"), to deal 38 | > in the Software without restriction, including without limitation the rights 39 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | > copies of the Software, and to permit persons to whom the Software is 41 | > furnished to do so, subject to the following conditions: 42 | > 43 | > The above copyright notice and this permission notice shall be included in all 44 | > copies or substantial portions of the Software. 45 | > 46 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 52 | > SOFTWARE. 53 | 54 | ## [cli](https://github.com/urfave/cli) 55 | 56 | > Copyright (c) 2016 Jeremy Saenz & Contributors 57 | > 58 | > Licensed under the MIT License. 59 | > 60 | > Permission is hereby granted, free of charge, to any person obtaining a copy 61 | > of this software and associated documentation files (the "Software"), to deal 62 | > in the Software without restriction, including without limitation the rights 63 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 64 | > copies of the Software, and to permit persons to whom the Software is 65 | > furnished to do so, subject to the following conditions: 66 | > 67 | > The above copyright notice and this permission notice shall be included in all 68 | > copies or substantial portions of the Software. 69 | > 70 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 72 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 73 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 74 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 75 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 76 | > SOFTWARE. 77 | 78 | ## [docker](https://github.com/docker/docker) 79 | 80 | > Copyright 2013-2016 Docker, Inc. 81 | > 82 | > Licensed under the Apache License, Version 2.0 (the "License"); 83 | > you may not use this file except in compliance with the License. 84 | > You may obtain a copy of the License at 85 | > 86 | > https://www.apache.org/licenses/LICENSE-2.0 87 | > 88 | > Unless required by applicable law or agreed to in writing, software 89 | > distributed under the License is distributed on an "AS IS" BASIS, 90 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 91 | > See the License for the specific language governing permissions and 92 | > limitations under the License. 93 | 94 | ## [go-colorable](https://github.com/mattn/go-colorable) 95 | 96 | >The MIT License (MIT) 97 | > 98 | > Copyright (c) 2016 Yasuhiro Matsumoto 99 | > 100 | > Permission is hereby granted, free of charge, to any person obtaining a copy 101 | > of this software and associated documentation files (the "Software"), to deal 102 | > in the Software without restriction, including without limitation the rights 103 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 104 | > copies of the Software, and to permit persons to whom the Software is 105 | > furnished to do so, subject to the following conditions: 106 | > 107 | > The above copyright notice and this permission notice shall be included in all 108 | > copies or substantial portions of the Software. 109 | > 110 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 111 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 112 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 113 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 114 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 115 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 116 | > SOFTWARE. 117 | 118 | ## [go-cui](https://github.com/jroimartin/gocui) 119 | 120 | > Copyright (c) 2014 The gocui Authors. All rights reserved. 121 | > 122 | > Redistribution and use in source and binary forms, with or without 123 | > modification, are permitted provided that the following conditions are met: 124 | > * Redistributions of source code must retain the above copyright 125 | > notice, this list of conditions and the following disclaimer. 126 | > * Redistributions in binary form must reproduce the above copyright 127 | > notice, this list of conditions and the following disclaimer in the 128 | > documentation and/or other materials provided with the distribution. 129 | > * Neither the name of the gocui Authors nor the names of its contributors 130 | > may be used to endorse or promote products derived from this software 131 | > without specific prior written permission. 132 | > 133 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 134 | > ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 135 | > WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 136 | > DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 137 | > ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 138 | > (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 139 | > LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 140 | > ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 141 | > (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 142 | > SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 143 | 144 | ## [go-gitconfig](https://github.com/tcnksm/go-gitconfig) 145 | 146 | > Copyright (c) 2014 tcnksm 147 | > 148 | > Licensed under the MIT License. 149 | > 150 | > Permission is hereby granted, free of charge, to any person obtaining 151 | > a copy of this software and associated documentation files (the 152 | > "Software"), to deal in the Software without restriction, including 153 | > without limitation the rights to use, copy, modify, merge, publish, 154 | > distribute, sublicense, and/or sell copies of the Software, and to 155 | > permit persons to whom the Software is furnished to do so, subject to 156 | > the following conditions: 157 | > 158 | > The above copyright notice and this permission notice shall be 159 | > included in all copies or substantial portions of the Software. 160 | > 161 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 162 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 163 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 164 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 165 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 166 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 167 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 168 | 169 | ## [Termbox](https://github.com/nsf/termbox-go) 170 | 171 | > Copyright (C) 2012 termbox-go authors 172 | > 173 | > Permission is hereby granted, free of charge, to any person obtaining a copy 174 | > of this software and associated documentation files (the "Software"), to deal 175 | > in the Software without restriction, including without limitation the rights 176 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 177 | > copies of the Software, and to permit persons to whom the Software is 178 | > furnished to do so, subject to the following conditions: 179 | > 180 | > The above copyright notice and this permission notice shall be included in 181 | > all copies or substantial portions of the Software. 182 | > 183 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 184 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 185 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 186 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 187 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 188 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 189 | > THE SOFTWARE. 190 | 191 | ## [YAML support for the Go language](https://github.com/go-yaml/yaml) 192 | 193 | > Copyright 2011-2016 Canonical Ltd. 194 | > 195 | > Licensed under the Apache License, Version 2.0 (the "License"); 196 | > you may not use this file except in compliance with the License. 197 | > You may obtain a copy of the License at 198 | > 199 | > http://www.apache.org/licenses/LICENSE-2.0 200 | > 201 | > Unless required by applicable law or agreed to in writing, software 202 | > distributed under the License is distributed on an "AS IS" BASIS, 203 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | > See the License for the specific language governing permissions and 205 | > limitations under the License. 206 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile 3 | # 4 | # Copyright (c) 2016 Junpei Kawamoto 5 | # 6 | # This software is released under the MIT License. 7 | # 8 | # http://opensource.org/licenses/mit-license.php 9 | # 10 | VERSION = snapshot 11 | GHRFLAGS = 12 | 13 | default: build 14 | 15 | .PHONY: asset 16 | asset: 17 | go-bindata -pkg command -o command/assets.go -nometadata assets 18 | 19 | .PHONY: build 20 | build: asset 21 | goxc -os="darwin linux windows" -d=pkg -pv=$(VERSION) 22 | 23 | .PHONY: release 24 | release: 25 | ghr -u jkawamoto $(GHRFLAGS) v$(VERSION) pkg/$(VERSION) 26 | 27 | .PHONY: test 28 | test: asset 29 | go test -v ./... 30 | 31 | .PHONY: local 32 | local: asset 33 | go build 34 | go install 35 | 36 | .PHONY: get-deps 37 | get-deps: 38 | go get -u github.com/jteeuwen/go-bindata/... 39 | go get -d -t -v . 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loci: Testing remote CI scripts locally 2 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 3 | [![Build Status](https://travis-ci.org/jkawamoto/loci.svg?branch=master)](https://travis-ci.org/jkawamoto/loci) 4 | [![wercker status](https://app.wercker.com/status/25b462a013ed96bf51254862938e7659/s/master "wercker status")](https://app.wercker.com/project/byKey/25b462a013ed96bf51254862938e7659) 5 | [![Release](https://img.shields.io/badge/release-0.5.3-brightgreen.svg)](https://github.com/jkawamoto/loci/releases/tag/v0.5.3) 6 | [![Japanese](https://img.shields.io/badge/qiita-%E6%97%A5%E6%9C%AC%E8%AA%9E-brightgreen.svg)](http://qiita.com/jkawamoto/items/a409dd9cd6e63034aa28) 7 | 8 | [![Loci Logo](https://jkawamoto.github.io/loci/img/image.png)](https://jkawamoto.github.io/loci/) 9 | 10 | Loci runs CI tests locally to make sure your commits will pass such tests 11 | *before* pushing to a remote repository. 12 | 13 | Loci currently supports [Travis](https://travis-ci.org/)'s CI scripts 14 | for [Python](https://www.python.org/) and [Go](https://golang.org/) projects. 15 | Loci also requires [Docker](https://www.docker.com/) to run tests in a sandbox. 16 | 17 | ## Demo 18 | [![asciicast](https://asciinema.org/a/126089.png)](https://asciinema.org/a/126089) 19 | 20 | ## Usage 21 | If your current directory has `.travis.yml`, run just `loci` like 22 | 23 | ```shell 24 | $ loci 25 | ``` 26 | 27 | If your `.travis.yml` specifies more than two runtime versions, Loci will run 28 | those tests palatally. If you want to run tests on a selected one runtime 29 | version, use `--select`/`-s` flag. For example, the following command runs tests 30 | on only Python 3.6: 31 | 32 | ```shell 33 | $ loci -s 3.6 34 | ``` 35 | 36 | Here is the help text of the `loci` command: 37 | 38 | ~~~ 39 | loci [global options] [script file] 40 | 41 | If script file isn't given, .travis.yml will be used. 42 | 43 | GLOBAL OPTIONS: 44 | --name NAME, -n NAME base NAME of containers running tests. 45 | If not given, containers will be deleted. 46 | --select VERSION, -s VERSION select specific runtime VERSION where tests 47 | running on. 48 | --tag TAG, -t TAG specify a TAG name of the docker image to 49 | be build. 50 | --max-processors value, -p value max processors used to run tests. 51 | --log, -l store logging information to files. 52 | --base TAG, -b TAG use image TAG as the base image. 53 | (default: "ubuntu:latest") 54 | --apt-proxy URL URL for a proxy server of apt repository. 55 | If environment variable APT_PROXY exists, 56 | that value will be used by default. 57 | --pypi-proxy URL URL for a proxy server of pypi repository. 58 | If environment variable PYPI_PROXY exists, 59 | that value will be used by default. 60 | --http-proxy URL URL for a http proxy server. 61 | If environment variable HTTP_PROXY exists, 62 | that value will be used by default. 63 | --https-proxy URL URL for a https proxy server. 64 | If environment variable HTTPS_PROXY exists, 65 | that value will be used by default. 66 | --no-proxy LIST Comma separated URL LIST for which proxies 67 | won't be used. 68 | If environment variable NO_PROXY exists, 69 | that value will be used by default. 70 | --no-build-cache Do not use cache when building the image. 71 | --no-color Omit to print color codes. 72 | --help, -h show help 73 | --version, -v print the version 74 | ~~~ 75 | 76 | 77 | ## Installation 78 | Loci works with [docker](https://www.docker.com/). 79 | If your environment doesn't have docker, install it first. 80 | The minimum required docker version is 1.12.0 (API version: 1.24). 81 | 82 | If you're a [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/) 83 | user, you can install Loci by the following commands: 84 | 85 | ```shell 86 | $ brew tap jkawamoto/loci 87 | $ brew install loci 88 | ``` 89 | 90 | To build the newest version of Loci, use `go get` command: 91 | 92 | ```shell 93 | $ go get github.com/jkawamoto/loci 94 | ``` 95 | 96 | Otherwise, compiled binaries are also available in 97 | [Github](https://github.com/jkawamoto/loci/releases). 98 | 99 | 100 | # License 101 | This software is released under the MIT License, see [LICENSES](LICENSES.md). 102 | -------------------------------------------------------------------------------- /assets/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Dockerfile 3 | # 4 | # Copyright (c) 2016-2017 Junpei Kawamoto 5 | # 6 | # This software is released under the MIT License. 7 | # 8 | # http://opensource.org/licenses/mit-license.php 9 | # 10 | {{define "base"}} 11 | FROM {{.BaseImage}} 12 | MAINTAINER Junpei Kawamoto 13 | 14 | ENV TERM vt100 15 | ENV DEBIAN_FRONTEND noninteractive 16 | {{with .HTTPProxy}} 17 | ENV HTTP_PROXY {{.}} 18 | {{end}} 19 | {{with .HTTPSProxy}} 20 | ENV HTTPS_PROXY {{.}} 21 | {{end}} 22 | {{with .NoProxy}} 23 | ENV NO_PROXY {{.}} 24 | {{end}} 25 | ENV TRAVIS_OS_NAME linux 26 | 27 | {{with .AptProxy}} 28 | RUN echo "Acquire::http { Proxy \"{{.}}\"; };" >> /etc/apt/apt.conf.d/01proxy 29 | {{end}} 30 | 31 | {{with .PypiProxy}} 32 | RUN PYPI_PROXY={{.}} && \ 33 | IPPORT=${PYPI_PROXY#*//} && \ 34 | mkdir -p ~/.pip/ && \ 35 | echo "[global]\nindex-url=$PYPI_PROXY/root/pypi\ntrusted-host=${IPPORT%:*}" >> ~/.pip/pip.conf 36 | {{end}} 37 | 38 | # Install Common APT packages. 39 | RUN apt-get update && \ 40 | apt-get install -y apt-utils git curl wget sudo unzip ccache pkg-config xvfb libgtk2.0-dev freeglut3-dev && \ 41 | rm -rf /var/lib/apt/lists/* 42 | # Install language specific packages. 43 | {{block "package" .}} 44 | {{end}} 45 | # Install runtimes to be used in tests. 46 | ARG VERSION 47 | {{block "version" .}} 48 | {{end}} 49 | # Install required packages. 50 | {{if .Travis.Addons.Apt.Packages}} 51 | RUN apt-get update && \ 52 | apt-get install -y {{range .Travis.Addons.Apt.Packages}} {{.}} {{end}} && \ 53 | rm -rf /var/lib/apt/lists/* 54 | {{end}} 55 | 56 | {{block "source" .}} 57 | ADD {{.Archive}} /data 58 | WORKDIR /data 59 | {{end}} 60 | 61 | ADD entrypoint.sh /root 62 | ENTRYPOINT ["bash", "/root/entrypoint.sh"] 63 | {{end}} 64 | -------------------------------------------------------------------------------- /assets/Dockerfile-go: -------------------------------------------------------------------------------- 1 | # 2 | # Dockerfile-go 3 | # 4 | # Copyright (c) 2016-2017 Junpei Kawamoto 5 | # 6 | # This software is released under the MIT License. 7 | # 8 | # http://opensource.org/licenses/mit-license.php 9 | # 10 | {{define "package"}} 11 | ENV GOPATH /data 12 | RUN apt-get update && \ 13 | apt-get install -y golang && \ 14 | rm -rf /var/lib/apt/lists/* 15 | 16 | # Install gimme 17 | RUN mkdir -p $GOPATH/bin && \ 18 | curl -sL -o $GOPATH/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme && \ 19 | chmod +x $GOPATH/bin/gimme 20 | {{end}} 21 | 22 | {{define "version"}} 23 | # Install a specific version of Go 24 | RUN PATH="$GOPATH/bin/:$PATH" && \ 25 | echo "Set Go $VERSION" && \ 26 | [ $VERSION = "any" ] || eval "$(GIMME_GO_VERSION=$VERSION gimme)" 27 | {{end}} 28 | 29 | {{define "source"}} 30 | {{if .Travis.GoImportPath}} 31 | ADD {{.Archive}} $GOPATH/src/{{.Travis.GoImportPath}} 32 | {{else}} 33 | ADD {{.Archive}} $GOPATH/src/{{.Repository}} 34 | {{end}} 35 | WORKDIR $GOPATH/src/{{.Repository}} 36 | {{end}} 37 | -------------------------------------------------------------------------------- /assets/Dockerfile-python: -------------------------------------------------------------------------------- 1 | # 2 | # Dockerfile-python 3 | # 4 | # Copyright (c) 2016-2017 Junpei Kawamoto 5 | # 6 | # This software is released under the MIT License. 7 | # 8 | # http://opensource.org/licenses/mit-license.php 9 | # 10 | {{define "package"}} 11 | # Install apt packages. 12 | RUN apt-get update && \ 13 | apt-get install -y python python-pip aria2 zlib1g-dev libreadline6-dev \ 14 | libbz2-dev libsqlite3-dev libssl-dev && \ 15 | rm -rf /var/lib/apt/lists/* 16 | 17 | # Update pip 18 | RUN pip install --upgrade pip 19 | # Install pyenv 20 | RUN curl -sSL https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash 21 | {{end}} 22 | 23 | {{define "version"}} 24 | # Install a specific python version 25 | ENV PYTHON_BUILD_ARIA2_OPTS "-x 10 -k 1M" 26 | RUN PATH="/root/.pyenv/bin:$PATH" && \ 27 | echo "Set Python $VERSION" && \ 28 | eval "$(pyenv init -)" && \ 29 | eval "$(pyenv virtualenv-init -)" && \ 30 | PYVERSION=$(pyenv install -l | sed "s/ //g" | grep -E "^$VERSION($|[^-])" | tail -1) && \ 31 | env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install -s -v $PYVERSION && \ 32 | pyenv local $PYVERSION 33 | {{end}} 34 | -------------------------------------------------------------------------------- /assets/entrypoint: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | #!/bin/bash 3 | # 4 | # entrypoint 5 | # 6 | # Copyright (c) 2016-2017 Junpei Kawamoto 7 | # 8 | # This software is released under the MIT License. 9 | # 10 | # http://opensource.org/licenses/mit-license.php 11 | # 12 | 13 | # Run before install steps, install steps, before script steps, and script 14 | # steps in this order. If any parameters are given, execute the parameters 15 | # instead. For example, to debug this script, run this with `bash` as the 16 | # parameter and get a shell access. 17 | set -e 18 | {{block "env" .}} 19 | {{end}} 20 | if [[ $# != 0 ]]; then 21 | exec $@ 22 | fi 23 | {{block "prepare" .}} 24 | {{end}} 25 | 26 | {{block "before_install" .}} 27 | echo -e "\e[33mBefore Install Steps:\e[m" 28 | {{range .BeforeInstall}} 29 | echo "{{.}}" 30 | ({{.}}) 31 | {{end}} 32 | {{end}} 33 | 34 | {{block "install" .}} 35 | echo -e "\e[33mInstall Steps:\e[m" 36 | {{range .Install}} 37 | echo "{{.}}" 38 | ({{.}}) 39 | {{end}} 40 | {{end}} 41 | 42 | {{block "before_script" .}} 43 | echo -e "\e[33mBefore Script Steps:\e[m" 44 | {{range .BeforeScript}} 45 | echo "{{.}}" 46 | ({{.}}) 47 | {{end}} 48 | {{end}} 49 | 50 | {{block "script" .}} 51 | echo -e "\e[33mScript Steps:\e[m" 52 | {{range .Script}} 53 | echo "{{.}}" 54 | ({{.}}) 55 | {{end}} 56 | {{end}} 57 | 58 | {{end}} 59 | -------------------------------------------------------------------------------- /assets/entrypoint-go: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # entrypoint-go 4 | # 5 | # Copyright (c) 2016-2017 Junpei Kawamoto 6 | # 7 | # This software is released under the MIT License. 8 | # 9 | # http://opensource.org/licenses/mit-license.php 10 | # 11 | 12 | # Run before install steps, install steps, before script steps, and script 13 | # steps in this order. This entry point script focuses on running tests for 14 | # Go applications. If any parameters are given, execute the parameters 15 | # instead. For example, to debug this script, run this with `bash` as the 16 | # parameter and get a shell access. 17 | {{define "env"}} 18 | PATH="$GOPATH/bin:$PATH" 19 | declare -x GIMME_GO_VERSION=$(gimme -l) 20 | if [[ -n "$GIMME_GO_VERSION" ]]; then 21 | eval "$(gimme)" 22 | fi 23 | readonly TRAVIS_GO_VERSION=$(go version | cut -f 3 -d " " | sed s/go//g) 24 | {{end}} 25 | 26 | {{define "install"}} 27 | echo -e "\e[33mInstall Steps:\e[m" 28 | git config --global http.https://gopkg.in.followRedirects true 29 | {{if .Install}} 30 | {{range .Install}} 31 | echo "{{.}}" 32 | ({{.}}) 33 | {{end}} 34 | {{else}} 35 | echo "go get -t ./..." 36 | go get -t ./... 37 | {{end}} 38 | {{end}} 39 | 40 | {{define "script"}} 41 | echo -e "\e[33mScript Steps:\e[m" 42 | {{/* "if script isn't given, use make or the default golang test command." */}} 43 | {{if .Script}} 44 | {{range .Script}} 45 | echo "{{.}}" 46 | ({{.}}) 47 | {{end}} 48 | {{else}} 49 | if [[ -e GNUMakefile ]] || [[ -e Makefile ]] || [[ -e BSDmakefile ]] || [[ -e makefile ]]; then 50 | make 51 | else 52 | {{if .GoBuildArgs}} 53 | go test {{.GoBuildArgs}} 54 | {{else}} 55 | go test -v ./... 56 | {{end}} 57 | fi 58 | {{end}} 59 | {{end}} 60 | -------------------------------------------------------------------------------- /assets/entrypoint-python: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # entrypoint-python 4 | # 5 | # Copyright (c) 2016-2017 Junpei Kawamoto 6 | # 7 | # This software is released under the MIT License. 8 | # 9 | # http://opensource.org/licenses/mit-license.php 10 | # 11 | 12 | # Run before install steps, install steps, before script steps, and script 13 | # steps in this order. This entry point script focuses on running tests for 14 | # Python applications. If any parameters are given, execute the parameters 15 | # instead. For example, to debug this script, run this with `bash` as the 16 | # parameter and get a shell access. 17 | {{define "env"}} 18 | PATH="/root/.pyenv/bin:$PATH" 19 | eval "$(pyenv init -)" && \ 20 | eval "$(pyenv virtualenv-init -)" 21 | {{end}} 22 | 23 | {{define "prepare"}} 24 | TERM=xterm 25 | readonly PYVERSION=$(pyenv local) 26 | {{end}} 27 | 28 | {{define "install"}} 29 | echo -e "\e[33mInstall Steps:\e[m" 30 | if [[ -e requirements.txt ]]; then 31 | pip install -r requirements.txt 32 | fi 33 | {{range .Install}} 34 | echo "{{.}}" 35 | ({{.}}) 36 | {{end}} 37 | {{end}} 38 | -------------------------------------------------------------------------------- /command/archive.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/archive.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "archive/tar" 15 | "bufio" 16 | "compress/gzip" 17 | "context" 18 | "fmt" 19 | "io" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "strings" 24 | 25 | "golang.org/x/sync/errgroup" 26 | ) 27 | 28 | // pathListupFunc defines a function which lists up paths and put them to a 29 | // given channel. This function is used with parallelListup. 30 | type pathListupFunc func(context.Context, chan<- string) error 31 | 32 | // Archive makes a tar.gz file consists of files maintained a git repository. 33 | func Archive(ctx context.Context, dir string, filename string) (err error) { 34 | 35 | writeFile, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600) 36 | if err != nil { 37 | return 38 | } 39 | defer writeFile.Close() 40 | 41 | zipWriter, err := gzip.NewWriterLevel(writeFile, gzip.BestCompression) 42 | if err != nil { 43 | return 44 | } 45 | defer zipWriter.Close() 46 | 47 | tarWriter := tar.NewWriter(zipWriter) 48 | defer tarWriter.Close() 49 | 50 | // Change dir and run. 51 | cd, err := os.Getwd() 52 | if err != nil { 53 | return 54 | } 55 | if err = os.Chdir(dir); err != nil { 56 | return 57 | } 58 | defer os.Chdir(cd) 59 | 60 | // Listing up and write to a tarball. 61 | wg, ctx := errgroup.WithContext(ctx) 62 | ch := make(chan string) 63 | wg.Go(func() error { 64 | defer close(ch) 65 | 66 | eg, ctx := errgroup.WithContext(ctx) 67 | eg.Go(func() error { 68 | return listupGitRepository(ctx, ch) 69 | }) 70 | eg.Go(func() error { 71 | return listupGitSources(ctx, ch) 72 | }) 73 | return eg.Wait() 74 | 75 | }) 76 | 77 | wg.Go(func() error { 78 | return tarballing(tarWriter, ch) 79 | }) 80 | 81 | return wg.Wait() 82 | 83 | } 84 | 85 | // tarballing is a go-routine which write a file given via ch to a tar writer. 86 | func tarballing(writer *tar.Writer, ch <-chan string) (err error) { 87 | 88 | var info os.FileInfo 89 | var header *tar.Header 90 | for path := range ch { 91 | 92 | // For Windows: Replace path delimiters. 93 | path = filepath.ToSlash(path) 94 | 95 | // Write a file header. 96 | info, err = os.Stat(path) 97 | if err != nil { 98 | err = fmt.Errorf("Cannot find %s (%s)", path, err.Error()) 99 | break 100 | } 101 | 102 | header, err = tar.FileInfoHeader(info, path) 103 | if err != nil { 104 | break 105 | } 106 | 107 | if strings.HasPrefix(path, "../") { 108 | header.Name = path[3:] 109 | } else { 110 | header.Name = path 111 | } 112 | writer.WriteHeader(header) 113 | 114 | // Write the body. 115 | if err = copyFile(path, writer); err != nil { 116 | break 117 | } 118 | } 119 | 120 | return 121 | 122 | } 123 | 124 | // listupGitRepository lists up git repository and puts founded paths to a given ch. 125 | func listupGitRepository(ctx context.Context, ch chan<- string) error { 126 | 127 | return filepath.Walk(".git", func(path string, info os.FileInfo, err error) error { 128 | 129 | // Check the given context is still alive. 130 | select { 131 | case <-ctx.Done(): 132 | return ctx.Err() 133 | default: 134 | } 135 | 136 | // If an error is passed, propagate it. 137 | if err != nil { 138 | return err 139 | } 140 | 141 | if !info.IsDir() { 142 | ch <- path 143 | } 144 | return nil 145 | 146 | }) 147 | 148 | } 149 | 150 | // listupGitSources lists up git sources and puts finding paths to a given ch. 151 | func listupGitSources(ctx context.Context, ch chan<- string) (err error) { 152 | 153 | ctx, cancel := context.WithCancel(ctx) 154 | defer cancel() 155 | 156 | cmd := exec.CommandContext(ctx, "git", "ls-files") 157 | stdout, err := cmd.StdoutPipe() 158 | if err != nil { 159 | return 160 | } 161 | 162 | err = cmd.Start() 163 | if err != nil { 164 | return 165 | } 166 | 167 | var info os.FileInfo 168 | s := bufio.NewScanner(stdout) 169 | for s.Scan() { 170 | 171 | path := s.Text() 172 | if info, err = os.Stat(path); err == nil && !info.IsDir() { 173 | ch <- path 174 | } 175 | 176 | } 177 | 178 | err = s.Err() 179 | if err != nil { 180 | return 181 | } 182 | 183 | return cmd.Wait() 184 | 185 | } 186 | 187 | // copyFile opens a given file and put its body to a given writer. 188 | func copyFile(path string, writer io.Writer) (err error) { 189 | 190 | // Prepare to write a file body. 191 | fp, err := os.Open(path) 192 | if err != nil { 193 | return 194 | } 195 | defer fp.Close() 196 | 197 | _, err = io.Copy(writer, fp) 198 | return 199 | 200 | } 201 | -------------------------------------------------------------------------------- /command/archive_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/archive_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "archive/tar" 15 | "compress/gzip" 16 | "context" 17 | "io" 18 | "os" 19 | "path" 20 | "path/filepath" 21 | "testing" 22 | ) 23 | 24 | func TestArchive(t *testing.T) { 25 | 26 | temp := os.TempDir() 27 | target := path.Join(temp, "test.tar.gz") 28 | t.Logf("Creating an archive file: %s", target) 29 | 30 | if err := Archive(context.Background(), "..", target); err != nil { 31 | t.Error(err.Error()) 32 | return 33 | } 34 | if _, err := os.Stat(target); err != nil { 35 | t.Error(err.Error()) 36 | return 37 | } 38 | defer os.Remove(target) 39 | 40 | fp, err := os.Open(target) 41 | if err != nil { 42 | t.Error(err.Error()) 43 | return 44 | } 45 | defer fp.Close() 46 | 47 | zip, err := gzip.NewReader(fp) 48 | if err != nil { 49 | t.Error(err.Error()) 50 | return 51 | } 52 | 53 | reader := tar.NewReader(zip) 54 | for { 55 | 56 | info, err := reader.Next() 57 | if err != io.EOF { 58 | break 59 | } else if err != nil { 60 | t.Error(err.Error()) 61 | return 62 | } 63 | 64 | original, err := os.Stat(filepath.Join("..", info.Name)) 65 | if err != nil { 66 | t.Error(err.Error()) 67 | return 68 | } 69 | if info.Size != original.Size() { 70 | t.Errorf("%s seems broken. (%d != %d)", info.Name, info.Size, original.Size()) 71 | return 72 | } 73 | 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /command/assets.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. 2 | // sources: 3 | // assets/Dockerfile 4 | // assets/Dockerfile-go 5 | // assets/Dockerfile-python 6 | // assets/entrypoint 7 | // assets/entrypoint-go 8 | // assets/entrypoint-python 9 | // DO NOT EDIT! 10 | 11 | package command 12 | 13 | import ( 14 | "bytes" 15 | "compress/gzip" 16 | "fmt" 17 | "io" 18 | "io/ioutil" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | func bindataRead(data []byte, name string) ([]byte, error) { 26 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 27 | if err != nil { 28 | return nil, fmt.Errorf("Read %q: %v", name, err) 29 | } 30 | 31 | var buf bytes.Buffer 32 | _, err = io.Copy(&buf, gz) 33 | clErr := gz.Close() 34 | 35 | if err != nil { 36 | return nil, fmt.Errorf("Read %q: %v", name, err) 37 | } 38 | if clErr != nil { 39 | return nil, err 40 | } 41 | 42 | return buf.Bytes(), nil 43 | } 44 | 45 | type asset struct { 46 | bytes []byte 47 | info os.FileInfo 48 | } 49 | 50 | type bindataFileInfo struct { 51 | name string 52 | size int64 53 | mode os.FileMode 54 | modTime time.Time 55 | } 56 | 57 | func (fi bindataFileInfo) Name() string { 58 | return fi.name 59 | } 60 | func (fi bindataFileInfo) Size() int64 { 61 | return fi.size 62 | } 63 | func (fi bindataFileInfo) Mode() os.FileMode { 64 | return fi.mode 65 | } 66 | func (fi bindataFileInfo) ModTime() time.Time { 67 | return fi.modTime 68 | } 69 | func (fi bindataFileInfo) IsDir() bool { 70 | return false 71 | } 72 | func (fi bindataFileInfo) Sys() interface{} { 73 | return nil 74 | } 75 | 76 | var _assetsDockerfile = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\x71\x6f\xdb\xb6\x13\xfd\x5f\x9f\xe2\xa0\xe4\x57\xfc\x16\x4c\x92\x93\x01\x1b\x90\x2e\xc1\xd4\xda\xdd\xb4\x2e\xb2\x20\x6b\xd9\x82\xba\x08\x68\xea\x2c\xdd\x2c\x91\x1c\x49\x39\xf1\x04\xed\xb3\x0f\x92\x55\xdb\x2d\xb6\x62\xfb\xc3\xb0\x8e\x7c\xf7\xee\x3d\xf2\x8e\x67\xce\x19\x4c\x25\xdf\xa0\x5e\x53\x85\x4e\x1f\xbe\x96\x6a\xa7\xa9\x28\x2d\xfc\x9f\x7f\x01\x57\x93\xcb\xaf\xbd\xab\xc9\xe5\x37\xf0\x63\x23\x14\x12\xbc\x65\x4f\xac\x96\x56\x0e\xd8\xac\x24\x03\x46\xae\xed\x13\xd3\x08\x64\x40\x63\x85\xcc\x60\x0e\x8d\xc8\x51\x83\x2d\x11\xee\xa2\x0c\x7e\x22\x8e\xc2\xa0\x3f\x24\x95\xd6\xaa\xeb\x20\x90\x0a\x85\x91\x8d\xe6\xe8\x4b\x5d\x04\xd5\x1e\x62\x82\x9a\xac\x37\x06\xbe\x2a\x95\x73\xe6\xb4\x6d\x8e\x6b\x12\x08\xee\x8a\x19\x74\xbb\xce\x79\x93\xce\xef\xa0\x6d\xfd\x57\xcc\x60\x54\xb3\x02\xbb\xce\xb9\x0b\xa3\x38\x0b\xa3\x78\x96\x7e\x2a\x15\xbe\xdd\x8c\x5f\xfe\x6f\xc3\xce\x77\x45\xcd\xa8\xf2\xb9\xac\x6f\x1d\x67\x16\xdf\x43\x36\x4b\xef\x60\x6b\x2f\x27\x93\x21\x9c\xce\x5e\x45\x61\xfc\xf8\x26\x9d\xc7\xd9\x2c\x9e\x82\x90\x82\x84\x45\xcd\xb8\xa5\x2d\x3a\x6d\xfb\x44\xb6\x04\xff\x87\x2c\x4b\x12\x2d\x9f\x77\x5d\x37\xa4\xf5\xf1\x63\x92\xce\x7f\x7d\xe8\xb5\x75\x9d\xd3\xb6\x28\xf2\xe1\xff\x98\xb0\xf8\x34\x63\xf1\xd9\x94\x58\x9e\xe2\xe3\xf9\xdf\x82\x07\x0b\x69\x78\x1f\x2d\x1e\xe7\x8b\xc7\x38\xbc\x9b\x41\x45\xa2\x79\x76\x0e\x34\xa1\xb2\x1f\x78\xd2\x9f\x63\x40\x5e\x4a\x70\x43\xfe\x7b\x43\x1a\xaf\xaf\xfb\x1b\x81\x16\x06\x04\x2c\xdd\x81\x7c\xe9\xbe\x84\xee\xa5\x0b\xb7\xb7\x10\xa0\xe5\x01\x53\xb6\xff\xf9\x5c\x8a\xb5\x9f\x07\x93\x4b\xd5\xa3\x0f\x12\x0e\x95\x92\x9d\xa2\xd3\x52\xc9\x43\x12\xed\x45\xdf\x0c\xbc\xf0\xe2\x05\x2c\x1d\x00\x80\x28\x49\xe6\x69\x76\x73\xde\x1e\x21\x67\x17\x41\x70\x82\xa8\x37\x39\x69\xf0\x14\xfc\x19\xf8\x8a\x54\x70\xdc\xd9\x1b\x78\x57\x54\x72\xc5\xaa\xf7\x4b\x41\x22\xc7\x67\xaf\xd1\xd5\xcd\xf9\x91\x2d\xd0\x52\xda\x40\xed\x14\x2d\x85\xd5\x8d\xb1\x98\x7b\xa5\x34\xf6\xe6\xbc\xdd\xd7\xfe\xdf\xf5\x45\x37\x38\x1c\xf9\x15\xa9\xc1\xdf\xd1\xd5\x19\x44\xc2\x58\x56\x55\xf0\x5a\xd6\xb5\x14\x10\x26\x19\x28\xc6\x37\xac\x40\xe3\x0f\x06\x99\xb2\x5e\x81\x16\x1a\x95\x33\x8b\x47\x89\x1f\xd6\x69\x24\xf0\x76\xc3\x52\x63\xa9\x32\x50\x90\x05\xde\xe8\x0a\x9e\x7a\x88\x69\x72\x09\x8d\xf8\x83\x14\x70\xce\x78\x89\xa0\x36\x85\xd7\x2b\xa1\x02\x9e\xb7\xeb\x15\x54\xb4\x2a\xec\xe6\xca\x9f\x78\x39\x6e\x61\xad\x11\x8b\xaa\xb1\x5f\x0d\xd1\xa1\xa2\xae\xc1\xd3\x6b\x08\xb6\x4c\x07\x15\xad\x86\x2b\xab\xc8\x58\x13\x5c\x9c\x18\xa9\x98\x28\x1a\x56\x20\x18\x85\x9c\xd6\xc4\x4f\xfc\xb4\xed\xaa\x92\x7c\x03\xee\xb8\xe4\xc2\x69\x9b\x1d\x39\x74\x23\x2c\xd5\x68\xc0\x4a\x58\x21\x34\xfd\xc4\x93\x00\x8b\xc6\x1a\xdf\x09\xd3\xef\xe1\x7e\x96\x2e\xa2\x79\x7c\x64\xdc\xa2\x36\x24\xc5\x3f\x32\xe2\xd0\x8b\xf9\x47\x62\x68\x0d\x7e\xa6\xd9\x96\x8c\x1f\xe6\xb9\x14\xa6\xef\x63\x3f\x19\x11\x63\x83\xfd\x87\xf3\x6f\x5b\xcd\x44\x81\x9f\x27\xdd\xcf\x16\x8c\x12\xff\xdd\xe9\x9e\x4c\xc1\x68\x77\xff\xb2\xed\xdd\x86\xd3\x69\x4f\x1a\x6a\x5e\xd2\x16\xbb\x0e\x82\x9c\x59\xe6\xfc\x32\x4f\xdf\x4e\xa3\x74\x8c\x0e\x14\x3d\x1a\x85\xd5\x3b\x25\x49\x58\xdf\x94\x30\xf4\xb1\x33\x8b\xb3\xf4\x21\x99\x47\x71\x06\xef\xfa\x87\xb0\x74\xbf\x04\x77\xdf\xe2\x1f\xc1\xdd\xf7\x07\xae\xbf\x02\x00\x00\xff\xff\xba\x68\xdb\x23\xde\x05\x00\x00") 77 | 78 | func assetsDockerfileBytes() ([]byte, error) { 79 | return bindataRead( 80 | _assetsDockerfile, 81 | "assets/Dockerfile", 82 | ) 83 | } 84 | 85 | func assetsDockerfile() (*asset, error) { 86 | bytes, err := assetsDockerfileBytes() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | info := bindataFileInfo{name: "assets/Dockerfile", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 92 | a := &asset{bytes: bytes, info: info} 93 | return a, nil 94 | } 95 | 96 | var _assetsDockerfileGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x92\xdf\x6b\xdb\x3e\x14\xc5\xdf\xf5\x57\x1c\x9c\x50\xda\xef\x17\x5b\x6d\x1f\x36\x28\xf4\xa1\x2c\x25\xcb\xfa\x23\x25\xcd\xba\x87\x6d\x14\x55\xbe\xb6\x45\x6d\x49\x48\x72\xb2\xa0\xfa\x7f\x1f\x71\x42\x12\xd8\x06\x7d\xb3\xef\x3d\x9f\x7b\xef\x39\x68\xc0\x06\x18\x19\xf9\x4a\xae\x50\x35\xa5\xa5\x61\xeb\xca\x27\x63\x57\x4e\x95\x55\xc0\xb1\x3c\xc1\xf9\xe9\xd9\x87\xf4\xfc\xf4\xec\x23\xbe\xb4\xda\x92\xc2\x8d\x58\x8a\xc6\x84\x8d\x76\x5e\x29\x0f\x6f\x8a\xb0\x14\x8e\xa0\x3c\x1c\xd5\x24\x3c\xe5\x68\x75\x4e\x0e\xa1\x22\xdc\x4d\xe6\xb8\x55\x92\xb4\xa7\xac\x87\xaa\x10\xec\x05\xe7\xc6\x92\xf6\xa6\x75\x92\x32\xe3\x4a\x5e\x6f\x24\x9e\x37\x2a\xa4\xdb\x9f\xcc\x56\x96\x0d\x58\x8c\x39\x15\x4a\x13\x12\x2b\xe4\xab\x28\x29\xe9\x3a\x76\x7d\xff\x84\xf1\xf4\xe1\x6a\xfe\x19\x3c\x17\x41\xb0\xd9\xd7\x7b\x08\x1b\xd2\x92\x02\x5a\x9b\x8b\x40\x38\x3a\xc2\x0f\x06\x60\x57\x57\xda\x07\x51\xd7\x48\x57\x28\x4d\x2d\x74\xb9\x97\xb8\x06\xa9\x2b\xc0\x17\xc2\xf1\x5a\xbd\x70\x61\x03\xaf\x95\x0f\x9e\xff\xc7\xd8\x00\x93\x2d\x59\xaa\xa6\xa1\x7e\x57\xf3\x9a\x2b\x87\xd4\x62\xb8\x39\x83\xbf\x28\xbd\x1f\x27\x5b\x57\x23\xf5\xb7\x48\xcd\xa1\x80\xf7\x7c\x1f\x81\xbf\xe0\xdc\x89\x65\x56\xaa\x50\xb5\x2f\xad\x27\x27\x8d\x0e\xa4\x43\x26\x4d\xc3\x83\x13\x0b\xe5\x53\xa9\x36\x04\x6f\x84\x0f\xe4\xb6\xf8\x7e\x4b\xd5\x98\x1c\xff\xff\xfa\x73\x05\x8b\x91\x74\xde\x75\xec\x20\xbd\x05\x39\xaf\x8c\x5e\xa7\xb7\x37\x24\xe0\x2d\x49\x55\x28\x89\x6d\x1f\xa6\xc0\xd8\xf4\x1e\xd7\x33\x2f\x93\xc3\xe1\x17\xc3\xf5\x67\xb2\x3f\x81\x64\x65\x90\x3c\x52\xc0\xd8\x60\xf8\x74\x3d\x7b\x9c\x4c\xef\x0f\xfa\xdf\x77\x45\x5c\x22\x11\x7a\x95\xe0\x27\xde\xde\x40\x0b\x51\x23\x19\x1e\x8f\x27\x77\x77\xd7\xcf\xe3\xe9\xf3\x56\x75\xb9\x93\xf7\x3e\x4e\x92\xbf\x39\xd9\xbc\x9b\xb5\x91\x18\x55\x81\x6c\xde\xa7\x95\x8d\xcd\xa4\xb1\xc6\x85\x07\x11\xaa\xae\x63\x57\xa3\x11\x62\xcc\xae\x9c\xac\xd4\x82\xba\x6e\x97\x92\x77\x92\xc7\xf8\x0f\x2a\x46\xaa\x3d\xbd\x03\x9f\x91\x35\x5e\x05\xe3\x56\x1b\xaa\x3f\xf2\xdb\x74\x76\x33\x9a\xcc\xde\xa5\xfd\x1d\x00\x00\xff\xff\x30\x98\xba\x6b\x80\x03\x00\x00") 97 | 98 | func assetsDockerfileGoBytes() ([]byte, error) { 99 | return bindataRead( 100 | _assetsDockerfileGo, 101 | "assets/Dockerfile-go", 102 | ) 103 | } 104 | 105 | func assetsDockerfileGo() (*asset, error) { 106 | bytes, err := assetsDockerfileGoBytes() 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | info := bindataFileInfo{name: "assets/Dockerfile-go", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 112 | a := &asset{bytes: bytes, info: info} 113 | return a, nil 114 | } 115 | 116 | var _assetsDockerfilePython = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x53\x5d\x6f\xda\x4a\x10\x7d\xf7\xaf\x18\x19\x14\x85\x2b\x2d\x1b\xb8\x52\xae\x14\x29\x0f\xb9\x09\x6d\x68\x13\x40\x7c\x44\x8a\xda\x26\x5a\xdb\x83\x3d\xca\xb2\xbb\xdd\x5d\x3b\x25\xc0\x7f\xaf\xb0\xcd\x87\x50\xeb\x17\x7b\xe7\x9c\x39\x7b\xe6\x8c\xdc\x08\x1a\x70\xa7\xe3\x37\xb4\x73\x92\xc8\xcc\xd2\x67\x5a\x05\xdb\xea\xad\x36\x4b\x4b\x69\xe6\xe1\x3c\x6e\x41\xf7\xa2\x73\xc9\xba\x17\x9d\xff\xe0\x4b\xae\x0c\x12\x7c\x15\xef\x62\xa1\xbd\x2e\xb9\xd3\x8c\x1c\x38\x3d\xf7\xef\xc2\x22\x90\x03\x8b\x12\x85\xc3\x04\x72\x95\xa0\x05\x9f\x21\x3c\xf6\xa7\xf0\x40\x31\x2a\x87\xed\xb2\x29\xf3\xde\x5c\x71\xae\x0d\x2a\xa7\x73\x1b\x63\x5b\xdb\x94\xcb\x8a\xe2\xf8\x82\x3c\xab\x0f\x6d\x93\x99\xa0\x11\xac\x56\x09\xce\x49\x21\x84\x46\xc4\x6f\x22\xc5\x70\xb3\x09\x1a\xd0\x57\xce\x0b\x29\x41\x18\x0f\x35\xe0\xda\xc1\x78\x36\xd8\x56\x58\x8a\x1e\x72\x93\x08\x8f\x70\x76\x06\xdf\x03\x00\xd8\xd7\xa9\xee\x64\x4b\xa8\x06\xaf\x5f\xcc\x90\x01\x61\x49\x74\xe1\x43\x52\xd4\x49\x59\x82\x05\x48\x8a\x2c\x8a\x44\x92\xc2\xcb\xb2\x50\x89\xfd\xe1\x91\x14\x45\x1f\xdd\x5d\x93\xfb\x29\xc9\xe3\xbf\xfb\xa3\x93\xe5\xe7\xde\x8d\x5d\x00\xb3\x73\xe0\x85\xb0\x5c\x52\xc4\x85\xf1\x5c\x92\xf3\x8e\xff\x13\x04\x0d\x98\x55\xe6\x0d\x99\x72\xa6\xad\xb5\xbd\x6f\x96\x9b\xd4\x8a\xa4\x42\x0f\x49\x98\x25\xaa\xa2\x64\xc7\xb9\x95\xc0\xdc\xe4\xa1\x4c\xdb\x5d\x71\x6e\xc5\x7b\x3b\x25\x9f\xe5\x51\xee\xd0\xc6\x5a\x79\x54\xbe\x1d\xeb\x05\x5f\x2e\xf3\x9c\x97\xad\xac\xbe\x00\x2d\x5f\x08\xe7\xd1\xf2\x88\xd4\x29\x04\x6b\x88\x84\xcb\x82\xd5\x0a\x55\xb2\xd9\x04\x47\xeb\x29\xd0\x3a\xd2\xea\x64\x3d\xe0\x0c\xc6\x34\xa7\x78\x17\x76\x4d\x0b\x7a\x83\x27\x18\x3d\x4f\xef\x87\x83\xd7\xff\x67\xfd\x87\xbb\xd7\x9b\x71\xff\xa6\xfb\x3a\x1c\x4d\x27\x10\xb2\x5f\xd0\xb9\x00\xf6\x06\x9d\xc7\xb0\x1c\x69\x74\x33\xbd\xbf\x0e\xb9\xd5\xda\xf3\x76\xe9\x69\xeb\xee\xaa\xb9\xad\x87\x87\x54\x31\xce\x34\x84\x13\xf4\x30\xaa\x6e\x6b\x3e\xf5\xc6\x93\xfe\x70\x70\xcc\x29\x84\x84\xb0\x79\x5e\xaa\x00\x29\xf2\xc0\x5a\x7f\xc5\x0b\xb2\x3e\x17\xb2\x0a\xe1\x94\x3a\x7a\xae\xe5\xaf\x0f\x72\xf5\x92\x24\xac\x61\xfb\x2b\x84\x8e\x03\xe7\x69\x08\x6b\x48\x2d\x1a\x60\x3d\x08\x5f\x76\xae\xce\x9b\xeb\x6f\x2f\xec\x47\x6b\x8b\x7a\x41\x12\x58\xa7\x75\x64\x44\x15\xbb\x84\x6e\x87\x83\x4f\xfd\xcf\xb3\x71\xaf\xcc\xe7\x3a\x64\x0c\x95\x88\x24\x32\x97\x09\x8b\x49\x08\x27\xb7\x3b\x60\x05\x34\xf7\xf6\x0e\x9a\x15\x4f\xea\x58\xc8\x23\x7c\xbf\xce\xdf\x01\x00\x00\xff\xff\x50\xf3\xe1\x63\x19\x04\x00\x00") 117 | 118 | func assetsDockerfilePythonBytes() ([]byte, error) { 119 | return bindataRead( 120 | _assetsDockerfilePython, 121 | "assets/Dockerfile-python", 122 | ) 123 | } 124 | 125 | func assetsDockerfilePython() (*asset, error) { 126 | bytes, err := assetsDockerfilePythonBytes() 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | info := bindataFileInfo{name: "assets/Dockerfile-python", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 132 | a := &asset{bytes: bytes, info: info} 133 | return a, nil 134 | } 135 | 136 | var _assetsEntrypoint = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x92\x5f\x6b\x9c\x40\x14\xc5\xdf\xe7\x53\x9c\x68\x1e\x12\xd8\xd5\x4d\x02\x2d\xa4\x14\x4a\x0b\x85\xed\x9f\x97\x26\x6f\x9b\xd0\x8c\x7a\xd5\xa1\x3a\x33\xcc\x5c\xb3\x59\xc4\xef\x5e\x74\x4c\x97\xb6\x20\x24\x4f\x7a\xaf\x67\xee\xef\x9e\xe3\xf4\x7d\x41\xa5\xd2\x84\x28\x93\x9e\xa2\x61\x10\xf1\x49\x9a\x29\x9d\x66\xd2\xd7\x22\x16\x31\x48\xb3\x3b\x58\xa3\x34\x4f\xe5\x27\x63\x0f\x4e\x55\x35\xe3\x2c\x3f\xc7\xe5\xe6\xe2\xcd\xfa\x72\x73\xf1\x16\x5f\x3a\x6d\x49\xe1\xab\xdc\xcb\xd6\xb0\x99\xb4\xb7\xb5\xf2\xf0\xa6\xe4\xbd\x74\x04\xe5\xe1\xa8\x21\xe9\xa9\x40\xa7\x0b\x72\xe0\x9a\xf0\x7d\x7b\x8b\x6f\x2a\x27\xed\x29\x99\x0e\xd5\xcc\xf6\x3a\x4d\x8d\x25\xed\x4d\xe7\x72\x4a\x8c\xab\xd2\x26\x48\x7c\xda\x2a\x5e\xcf\x45\x62\x6b\x2b\x62\x21\x62\xfc\xe8\x34\x32\x2a\xcd\x48\xd1\x9e\x65\xd3\xc0\x33\x59\xbf\xfa\xb7\x9c\x45\x3e\x77\xca\xf2\x73\x53\xea\x62\xee\x88\x38\xf4\xa0\x34\x78\x5c\xde\xb8\x82\x5c\x82\x6d\x09\xa9\x0f\xb0\xd2\xc9\x96\x98\x9c\xc7\x68\xa8\x52\x8f\xa4\x57\xa0\x27\xca\x3b\xa6\xc9\xcd\x51\x21\xe2\x89\x4d\xb2\x48\xf0\xd9\x38\xd0\x93\x6c\x6d\x43\x2b\xb0\x41\x41\x59\x57\x05\x40\xe0\xae\xe0\xba\x99\xb8\x57\x5c\xe3\x61\x4c\xff\x01\xd2\x8f\x43\x45\x7c\x1c\x3b\xed\x5a\x11\x43\xc2\xd7\xd4\x34\x90\x79\x4e\xde\x27\xc2\x13\x63\x4d\xa2\xef\xb3\xc6\xe4\xbf\x10\x91\x7e\x8c\x90\x0c\x83\xe8\x7b\xd2\xc5\x30\x08\x55\x62\xb7\xc3\x69\x8c\x93\xf7\xd8\xe0\xfe\xfe\xdd\x38\x5a\x0b\x4c\xeb\xe3\xf4\x83\x28\xd5\xf1\xb4\x75\x64\xa5\xa3\xbf\x27\x1c\x3f\x87\x14\x7f\xce\xd9\x06\x15\xe5\xb5\xc1\x9a\x10\xdd\xd1\xee\xea\xaa\xfd\x18\x82\xde\xce\xf1\xdf\x8c\xa9\x5e\xdf\xd1\xae\x8d\x44\xdf\x3b\xa9\x2b\x42\x12\x34\xb3\xe4\x79\x44\xd4\xf7\xc9\x30\x44\xe2\x6c\x7a\x9e\xff\xa1\xff\xbf\xc5\x12\x7e\x91\xfb\x6a\xe2\xec\x3b\xfc\xb3\x25\xdb\x37\xe1\x7e\x2d\xb8\x0e\x8a\x97\xaf\xb0\xc0\x5e\x82\xbe\x18\x17\x5e\x7e\x07\x00\x00\xff\xff\x71\xc3\x35\xb8\x1e\x04\x00\x00") 137 | 138 | func assetsEntrypointBytes() ([]byte, error) { 139 | return bindataRead( 140 | _assetsEntrypoint, 141 | "assets/entrypoint", 142 | ) 143 | } 144 | 145 | func assetsEntrypoint() (*asset, error) { 146 | bytes, err := assetsEntrypointBytes() 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | info := bindataFileInfo{name: "assets/entrypoint", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 152 | a := &asset{bytes: bytes, info: info} 153 | return a, nil 154 | } 155 | 156 | var _assetsEntrypointGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x53\xc1\x6e\xdc\x36\x10\xbd\xf3\x2b\x5e\x25\x03\xb5\x03\x8b\x4a\x62\xa0\x05\x5c\xe4\xe0\xb4\xe9\x76\xdb\x3a\x2e\xbc\x6e\x2e\x8e\x91\x70\xa5\x11\x45\x98\x22\x05\x92\x5a\xdb\x90\xf5\xef\x05\x25\xd9\xbb\x58\xfb\xd0\x8b\x44\x3e\x0e\x67\xde\x9b\x79\x4c\x7f\xc8\xd7\xca\xe4\x6b\xe1\x6b\x96\xb2\x14\x64\x82\x7b\x68\xad\x32\x21\x93\x76\x44\x7e\xb5\xed\x83\x53\xb2\x0e\x38\x2c\x8e\xf0\xfe\xed\xbb\x9f\xb2\xf7\x6f\xdf\xfd\x8c\x3f\x3b\xd3\x92\xc2\x5f\xe2\x4e\x34\x36\x4c\xb1\x57\xb5\xf2\xf0\xb6\x0a\x77\xc2\x11\x94\x87\x23\x4d\xc2\x53\x89\xce\x94\xe4\x10\x6a\xc2\xf9\xf2\x0a\x7f\xab\x82\x8c\x27\x3e\x5e\xaa\x43\x68\x4f\xf3\xdc\xb6\x64\xbc\xed\x5c\x41\xdc\x3a\x99\xeb\x29\xc4\xe7\x8d\x0a\xd9\xbc\xe1\x6d\xdd\xb2\x94\xb1\x14\x97\x9d\xc1\x9a\x2a\x1b\xab\x18\x1f\x84\xd6\xf0\x81\x5a\x7f\xbc\xbf\x9d\x83\x7c\xe1\x54\x1b\x9e\x40\x61\xca\x19\x61\xe9\x84\x41\x19\x84\x48\xde\xba\x92\x1c\x9f\x84\x8c\xbd\xc0\xd8\x8c\xa7\x04\x95\x2d\x3a\x4f\x1e\xd6\xc0\x75\xc6\x28\x23\x11\xc8\x07\x8f\xca\x3a\x96\x62\x61\x21\xda\x56\xab\x42\x04\x65\x8d\xe7\x58\x56\x10\xe6\x01\xad\x70\xa2\xa1\x40\xce\x23\x36\x46\xaa\x0d\x99\x63\xd0\x3d\x15\x5d\xa0\xb1\x2b\xdb\x08\x96\x8e\x1a\x48\x94\x1c\xbf\x5b\x07\xba\x17\x4d\xab\xe9\x18\xc1\xa2\xa4\x75\x27\x27\xa2\x13\xa1\xe3\x48\x63\x02\xee\x54\xa8\xf1\x3d\x0e\xf2\x3b\x84\x8f\x49\x59\xba\x4d\x3b\x6a\x96\x14\x20\xe0\x6b\xd2\x1a\xa2\x28\xc8\x7b\xce\xfa\xbe\xa4\x4a\x19\x42\x42\x66\x93\x0c\x03\xfb\xe7\xec\xea\x8f\x0f\xc9\xc1\xe2\x22\x2e\xa2\x39\x4e\x0f\xe2\x2a\x61\x25\x15\x3a\xb2\xcf\xee\xb1\x58\x9e\x9f\x7f\xfa\xb6\xb8\xf8\xf6\xe5\xd3\xe5\x6a\x79\xf1\xf9\xc3\xc1\xa1\x54\x4d\x43\xc8\xf4\x11\x53\x15\xae\xaf\x91\x19\x24\x07\xfb\x71\x09\x6e\x6e\x7e\x89\xd4\x0c\x03\x68\x23\x34\x92\xf9\xe6\x51\xc2\x2a\xc5\x1c\x89\xd2\x1a\xfd\x80\xab\xcb\xb3\x2f\xcb\xd5\x5e\x05\x8b\x0d\x39\xaf\xac\xc1\x23\x8a\x2e\x20\xab\x70\x82\xac\x44\x82\x04\x8f\x88\x2e\xf3\xb9\xb4\x79\x2e\x8f\x58\xdf\x93\x29\x87\x81\xed\xc8\x9b\x8d\x11\x25\x52\x51\x5b\x64\x84\xe4\x2b\x5d\x9f\x9c\x34\xcb\xd9\x32\xab\xe8\x84\xd3\xaf\x74\xdd\x24\x4c\xaa\x80\xc2\x9a\x4a\x49\x64\x99\xd4\x76\x2d\xf4\x68\x54\x1e\x3f\xfe\x34\xcf\xa5\x6d\x6f\x25\x57\x86\x57\x56\x6b\x7b\x77\x49\xa5\x72\x54\x04\x8f\xe0\x3a\x62\x7d\xaf\x2a\xf0\x39\xf1\x30\xb0\xbe\x77\xc2\x48\xda\x85\x46\x12\x49\xdf\xf3\x61\x48\xd8\xe1\xf8\xdf\xf2\xee\x7b\xd2\x9e\x9e\xa3\xa4\x1d\x67\x97\x05\xf0\x9c\x73\x9e\xb0\x3d\x60\xf7\xde\xbe\xee\xc9\x28\xaf\xc8\x5e\x4d\x96\xde\x55\xdd\xf7\xf9\x1b\x24\xaa\x7a\xb2\xbb\xf2\xe6\xc7\xf0\x64\xd7\xce\x13\x1a\x71\x4b\xb0\xd3\x43\x2e\xa9\x12\x9d\x0e\x90\x56\x8b\xf9\x1d\xa0\xb0\x4d\x23\x4c\xc9\x13\xbc\xc9\x47\x3a\xb1\x0d\x53\xa1\xdd\x2e\x3c\x23\xff\xaf\x09\xb3\xa3\x08\x8b\xcf\xff\x9e\x8b\x5b\xaa\x94\x26\xdc\xdc\xe0\xf1\x71\xc6\x5f\x05\x3f\xae\x7e\x6b\x5e\xc3\x77\xc0\x67\x2f\x46\x8c\xc5\x6a\x0c\x98\x48\x2f\xec\xc7\x4e\xe9\xf2\xcc\x49\x3f\x0c\x0c\x90\x76\x52\xd8\xf7\x2f\x8e\x9e\x79\x6e\xa3\xb2\xcd\x3c\x98\xf1\x74\x54\x53\xa9\x17\x53\xfa\x2f\x00\x00\xff\xff\xc3\x68\x4c\xe6\x7d\x05\x00\x00") 157 | 158 | func assetsEntrypointGoBytes() ([]byte, error) { 159 | return bindataRead( 160 | _assetsEntrypointGo, 161 | "assets/entrypoint-go", 162 | ) 163 | } 164 | 165 | func assetsEntrypointGo() (*asset, error) { 166 | bytes, err := assetsEntrypointGoBytes() 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | info := bindataFileInfo{name: "assets/entrypoint-go", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 172 | a := &asset{bytes: bytes, info: info} 173 | return a, nil 174 | } 175 | 176 | var _assetsEntrypointPython = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x52\x5f\x6b\xdb\x4e\x10\x7c\xbf\x4f\x31\x3f\x29\x84\x04\x2c\x29\x7f\xe0\x57\x48\xc9\x43\x29\x29\x75\xdb\xb4\x21\x31\x85\xe2\x04\x72\x96\x56\xd2\x81\xb4\x77\xbd\x5b\x39\x36\xc2\xdf\xbd\x48\xb6\xeb\x92\xf6\xe9\xb8\xb9\xb9\xd9\xd9\xd9\x8d\xff\xcb\x16\x86\xb3\x85\x0e\xb5\x8a\x55\x0c\x62\xf1\x6b\x67\x0d\x4b\xe2\xd6\x52\x5b\x1e\xd1\xf7\xd6\xad\xbd\xa9\x6a\xc1\x49\x7e\x8a\x8b\xb3\xf3\xff\x93\x8b\xb3\xf3\x37\xf8\xd4\xb1\x23\x83\xcf\xfa\x45\xb7\x56\xec\xc8\x9d\xd5\x26\x20\xd8\x52\x5e\xb4\x27\x98\x00\x4f\x0d\xe9\x40\x05\x3a\x2e\xc8\x43\x6a\xc2\xed\x74\x86\x2f\x26\x27\x0e\x94\x8e\x9f\x6a\x11\x77\x95\x65\xd6\x11\x07\xdb\xf9\x9c\x52\xeb\xab\xac\xd9\x52\x42\xd6\x1a\x49\x76\x97\xd4\xd5\x4e\xc5\x4a\xc5\xb8\xef\x18\x0b\x2a\xed\x50\x85\x83\xe8\xa6\x41\x10\x72\x61\xf2\xfa\xba\x23\x85\xdc\x1b\x27\x7b\x50\x73\xb1\x43\x54\xbc\xc5\x60\x18\x32\x98\xb7\xbe\x20\x9f\x6e\x1b\x19\xf3\xc0\x18\xc8\x5e\xa0\xb4\x79\x17\x28\xc0\x32\x7c\xc7\x6c\xb8\x82\x50\x90\x80\xd2\x7a\x15\xe3\x6e\x8c\x0d\xda\xb9\xc6\xe4\x5a\x8c\xe5\x90\x62\x5a\x42\xf3\x1a\x4e\x7b\xdd\x92\x90\x0f\x18\xc2\xa9\xcc\x92\x78\x02\x5a\x51\xde\x09\x8d\xc9\x1c\x18\x2a\x1e\xfb\x20\x5d\xa4\xf8\x60\x3d\x68\xa5\x5b\xd7\xd0\x04\x62\x51\xd0\xa2\xab\xb6\x66\xb7\xa6\x26\x83\x95\x2d\xf0\x62\xa4\xc6\xf3\x30\xd0\x67\xe8\x30\x88\xaa\xf8\x20\x3b\xf6\x5d\x91\x40\x23\xd4\xd4\x34\xd0\x79\x4e\x21\xa4\xaa\xef\x0b\x2a\x0d\x13\x22\xe2\x65\xb4\xd9\xa8\xbb\x77\xb3\x8f\xd7\x51\xe6\xad\x95\x2c\x75\x6b\xe2\xe5\xb0\x29\x57\x47\x03\x1e\x29\x5a\xea\x06\xd1\xd1\xc9\xf8\x00\xc3\x46\x90\x9c\x46\x38\x3e\xc6\xe3\xab\xb7\xa5\xf1\xd2\xe9\x86\x78\x99\xec\x69\xaa\xef\x89\x8b\xcd\x46\xfd\x51\xd5\x79\x72\xda\xd3\x50\x79\x76\x73\x7f\x7b\xbd\x12\xf2\xad\xf2\xa4\x0b\xcb\xcd\x1a\x77\x3f\xbe\xdf\xdc\x3f\x4c\xbf\x7d\xbd\xde\xeb\x36\x36\xd7\xcd\xe9\xbf\xa4\x76\xe3\x1f\xa4\x28\xaf\x2d\x12\x42\xf4\x48\xf3\xcb\xcb\x76\xba\x5b\x8c\x87\x61\xde\x57\x8f\x34\x6f\x23\x65\x4a\xcc\xe7\x03\xc7\xd3\xcf\xce\x78\x6a\x89\x25\xa4\xb2\x12\x3c\x3d\xbd\x1d\xe2\x63\x05\x38\xe3\x7e\x6f\x55\xe2\xff\xa2\xaa\xd2\xa8\xbe\xf7\x9a\x2b\x42\xba\x2b\xb2\xaf\x1e\xf5\x7d\xba\xd9\x44\xea\x64\x3c\x0f\x86\xf7\xe7\xaf\x00\x00\x00\xff\xff\xa4\xee\xb5\x7b\x82\x03\x00\x00") 177 | 178 | func assetsEntrypointPythonBytes() ([]byte, error) { 179 | return bindataRead( 180 | _assetsEntrypointPython, 181 | "assets/entrypoint-python", 182 | ) 183 | } 184 | 185 | func assetsEntrypointPython() (*asset, error) { 186 | bytes, err := assetsEntrypointPythonBytes() 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | info := bindataFileInfo{name: "assets/entrypoint-python", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} 192 | a := &asset{bytes: bytes, info: info} 193 | return a, nil 194 | } 195 | 196 | // Asset loads and returns the asset for the given name. 197 | // It returns an error if the asset could not be found or 198 | // could not be loaded. 199 | func Asset(name string) ([]byte, error) { 200 | cannonicalName := strings.Replace(name, "\\", "/", -1) 201 | if f, ok := _bindata[cannonicalName]; ok { 202 | a, err := f() 203 | if err != nil { 204 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 205 | } 206 | return a.bytes, nil 207 | } 208 | return nil, fmt.Errorf("Asset %s not found", name) 209 | } 210 | 211 | // MustAsset is like Asset but panics when Asset would return an error. 212 | // It simplifies safe initialization of global variables. 213 | func MustAsset(name string) []byte { 214 | a, err := Asset(name) 215 | if err != nil { 216 | panic("asset: Asset(" + name + "): " + err.Error()) 217 | } 218 | 219 | return a 220 | } 221 | 222 | // AssetInfo loads and returns the asset info for the given name. 223 | // It returns an error if the asset could not be found or 224 | // could not be loaded. 225 | func AssetInfo(name string) (os.FileInfo, error) { 226 | cannonicalName := strings.Replace(name, "\\", "/", -1) 227 | if f, ok := _bindata[cannonicalName]; ok { 228 | a, err := f() 229 | if err != nil { 230 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 231 | } 232 | return a.info, nil 233 | } 234 | return nil, fmt.Errorf("AssetInfo %s not found", name) 235 | } 236 | 237 | // AssetNames returns the names of the assets. 238 | func AssetNames() []string { 239 | names := make([]string, 0, len(_bindata)) 240 | for name := range _bindata { 241 | names = append(names, name) 242 | } 243 | return names 244 | } 245 | 246 | // _bindata is a table, holding each asset generator, mapped to its name. 247 | var _bindata = map[string]func() (*asset, error){ 248 | "assets/Dockerfile": assetsDockerfile, 249 | "assets/Dockerfile-go": assetsDockerfileGo, 250 | "assets/Dockerfile-python": assetsDockerfilePython, 251 | "assets/entrypoint": assetsEntrypoint, 252 | "assets/entrypoint-go": assetsEntrypointGo, 253 | "assets/entrypoint-python": assetsEntrypointPython, 254 | } 255 | 256 | // AssetDir returns the file names below a certain 257 | // directory embedded in the file by go-bindata. 258 | // For example if you run go-bindata on data/... and data contains the 259 | // following hierarchy: 260 | // data/ 261 | // foo.txt 262 | // img/ 263 | // a.png 264 | // b.png 265 | // then AssetDir("data") would return []string{"foo.txt", "img"} 266 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 267 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 268 | // AssetDir("") will return []string{"data"}. 269 | func AssetDir(name string) ([]string, error) { 270 | node := _bintree 271 | if len(name) != 0 { 272 | cannonicalName := strings.Replace(name, "\\", "/", -1) 273 | pathList := strings.Split(cannonicalName, "/") 274 | for _, p := range pathList { 275 | node = node.Children[p] 276 | if node == nil { 277 | return nil, fmt.Errorf("Asset %s not found", name) 278 | } 279 | } 280 | } 281 | if node.Func != nil { 282 | return nil, fmt.Errorf("Asset %s not found", name) 283 | } 284 | rv := make([]string, 0, len(node.Children)) 285 | for childName := range node.Children { 286 | rv = append(rv, childName) 287 | } 288 | return rv, nil 289 | } 290 | 291 | type bintree struct { 292 | Func func() (*asset, error) 293 | Children map[string]*bintree 294 | } 295 | var _bintree = &bintree{nil, map[string]*bintree{ 296 | "assets": &bintree{nil, map[string]*bintree{ 297 | "Dockerfile": &bintree{assetsDockerfile, map[string]*bintree{}}, 298 | "Dockerfile-go": &bintree{assetsDockerfileGo, map[string]*bintree{}}, 299 | "Dockerfile-python": &bintree{assetsDockerfilePython, map[string]*bintree{}}, 300 | "entrypoint": &bintree{assetsEntrypoint, map[string]*bintree{}}, 301 | "entrypoint-go": &bintree{assetsEntrypointGo, map[string]*bintree{}}, 302 | "entrypoint-python": &bintree{assetsEntrypointPython, map[string]*bintree{}}, 303 | }}, 304 | }} 305 | 306 | // RestoreAsset restores an asset under the given directory 307 | func RestoreAsset(dir, name string) error { 308 | data, err := Asset(name) 309 | if err != nil { 310 | return err 311 | } 312 | info, err := AssetInfo(name) 313 | if err != nil { 314 | return err 315 | } 316 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 317 | if err != nil { 318 | return err 319 | } 320 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 321 | if err != nil { 322 | return err 323 | } 324 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 325 | if err != nil { 326 | return err 327 | } 328 | return nil 329 | } 330 | 331 | // RestoreAssets restores an asset under the given directory recursively 332 | func RestoreAssets(dir, name string) error { 333 | children, err := AssetDir(name) 334 | // File 335 | if err != nil { 336 | return RestoreAsset(dir, name) 337 | } 338 | // Dir 339 | for _, child := range children { 340 | err = RestoreAssets(dir, filepath.Join(name, child)) 341 | if err != nil { 342 | return err 343 | } 344 | } 345 | return nil 346 | } 347 | 348 | func _filePath(dir, name string) string { 349 | cannonicalName := strings.Replace(name, "\\", "/", -1) 350 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 351 | } 352 | 353 | -------------------------------------------------------------------------------- /command/display.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/display.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "bufio" 15 | "context" 16 | "fmt" 17 | "io" 18 | "sort" 19 | "strings" 20 | "sync" 21 | 22 | "github.com/jroimartin/gocui" 23 | "github.com/ttacon/chalk" 24 | ) 25 | 26 | const ( 27 | headerHeight = 6 28 | ) 29 | 30 | // Header represents a header space in a display. 31 | type Header struct { 32 | body []string 33 | Logger io.Writer 34 | } 35 | 36 | func newHeader(update DisplayUpdateFunc) (header *Header) { 37 | 38 | header = new(Header) 39 | reader, writer := io.Pipe() 40 | go func() { 41 | defer reader.Close() 42 | 43 | scanner := bufio.NewScanner(reader) 44 | for scanner.Scan() { 45 | 46 | header.body = append(header.body, scanner.Text()) 47 | update(func(view *gocui.View) { 48 | _, h := view.Size() 49 | for i, line := range header.body { 50 | if len(header.body)-i > h { 51 | continue 52 | } 53 | fmt.Fprintln(view, line) 54 | } 55 | }) 56 | 57 | } 58 | }() 59 | 60 | header.Logger = writer 61 | return 62 | 63 | } 64 | 65 | // Section represents a section in a display. Each section has a header text and 66 | // several strings as the body. 67 | type Section struct { 68 | Header string 69 | Body []string 70 | update DisplayUpdateFunc 71 | } 72 | 73 | func newSection(header string, update DisplayUpdateFunc) *Section { 74 | 75 | return &Section{ 76 | Header: header, 77 | update: update, 78 | } 79 | 80 | } 81 | 82 | // Writer returns io.WriteCloser to write messages into the section. Users have 83 | // to close the returned writer. 84 | func (s *Section) Writer() io.WriteCloser { 85 | 86 | reader, writer := io.Pipe() 87 | go func() { 88 | defer reader.Close() 89 | 90 | scanner := bufio.NewScanner(reader) 91 | for scanner.Scan() { 92 | s.Body = append(s.Body, scanner.Text()) 93 | s.update(func(view *gocui.View) { 94 | _, h := view.Size() 95 | for i, line := range s.Body { 96 | if len(s.Body)-i > h { 97 | continue 98 | } 99 | fmt.Fprintln(view, line) 100 | } 101 | }) 102 | } 103 | 104 | }() 105 | 106 | return writer 107 | 108 | } 109 | 110 | // String returns a string representing this section. 111 | func (s *Section) String() string { 112 | 113 | return fmt.Sprintf( 114 | "%v\n%v", 115 | chalk.Cyan.Color(s.Header), 116 | strings.Join(s.Body, "\n")) 117 | 118 | } 119 | 120 | // Display represents a display which consists of several sections. 121 | type Display struct { 122 | MaxSection int 123 | Title string 124 | Header *Header 125 | mutex sync.Mutex 126 | sections []*Section 127 | closed bool 128 | done chan error 129 | gui *gocui.Gui 130 | } 131 | 132 | // DisplayUpdateHandler defines a handler function to update section body. 133 | type DisplayUpdateHandler func(view *gocui.View) 134 | 135 | // DisplayUpdateFunc is a function which a section calls to update the section 136 | // body. 137 | type DisplayUpdateFunc func(handler DisplayUpdateHandler) 138 | 139 | // NewDisplay creates a new display. 140 | func NewDisplay(ctx context.Context, title string, maxSection int) (display *Display, nctx context.Context, err error) { 141 | 142 | g, err := gocui.NewGui(gocui.OutputNormal) 143 | if err != nil { 144 | return 145 | } 146 | 147 | display = &Display{ 148 | MaxSection: maxSection, 149 | Title: title, 150 | gui: g, 151 | done: make(chan error), 152 | Header: newHeader(func(handler DisplayUpdateHandler) { 153 | g.Update(func(g *gocui.Gui) (err error) { 154 | v, err := g.View("header") 155 | if err != nil { 156 | return 157 | } 158 | v.Clear() 159 | handler(v) 160 | return 161 | }) 162 | }), 163 | } 164 | g.SetManager(display) 165 | 166 | err = g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { 167 | return gocui.ErrQuit 168 | }) 169 | if err != nil { 170 | g.Close() 171 | return 172 | } 173 | 174 | nctx, cancel := context.WithCancel(ctx) 175 | go func() { 176 | err := g.MainLoop() 177 | if err == gocui.ErrQuit { 178 | err = nil 179 | } 180 | cancel() 181 | display.done <- err 182 | }() 183 | 184 | return 185 | 186 | } 187 | 188 | // Layout is called every time the GUI is redrawn, it must contain the 189 | // base views and its initializations. 190 | func (d *Display) Layout(g *gocui.Gui) error { 191 | if d.closed { 192 | return fmt.Errorf("Display has been closed already") 193 | } 194 | 195 | d.mutex.Lock() 196 | defer d.mutex.Unlock() 197 | 198 | width, height := g.Size() 199 | if v, err := g.SetView("root", 0, 0, width-1, height-1); err != nil { 200 | if err != gocui.ErrUnknownView { 201 | return err 202 | } 203 | v.Title = d.Title 204 | v.Frame = true 205 | } 206 | 207 | if v, err := g.SetView("header", 0, 0, width-2, headerHeight+1); err != nil { 208 | if err != gocui.ErrUnknownView { 209 | return err 210 | } 211 | v.Frame = false 212 | v.Autoscroll = true 213 | } 214 | 215 | sectionHeight := (height - headerHeight - 2) / d.MaxSection 216 | for i, s := range d.sections { 217 | 218 | v, err := g.SetView(s.Header, 1, i*sectionHeight+headerHeight+1, width-2, (i+1)*sectionHeight+headerHeight) 219 | if err != nil { 220 | if err != gocui.ErrUnknownView { 221 | return err 222 | } 223 | v.Title = s.Header 224 | v.Autoscroll = true 225 | } 226 | 227 | } 228 | 229 | return nil 230 | 231 | } 232 | 233 | // Close closes this display. 234 | func (d *Display) Close() (err error) { 235 | d.mutex.Lock() 236 | defer d.mutex.Unlock() 237 | 238 | if !d.closed { 239 | d.gui.Update(func(g *gocui.Gui) error { 240 | return gocui.ErrQuit 241 | }) 242 | err = <-d.done 243 | d.gui.Close() 244 | d.closed = true 245 | } 246 | return 247 | 248 | } 249 | 250 | // AddSection adds a new section to this display. 251 | func (d *Display) AddSection(header string) *Section { 252 | d.mutex.Lock() 253 | defer d.mutex.Unlock() 254 | 255 | s := newSection(header, func(handler DisplayUpdateHandler) { 256 | d.gui.Update(func(g *gocui.Gui) (err error) { 257 | v, err := g.View(header) 258 | if err != nil { 259 | return 260 | } 261 | v.Clear() 262 | handler(v) 263 | return 264 | }) 265 | }) 266 | 267 | d.sections = append(d.sections, s) 268 | sort.Slice(d.sections, func(i int, j int) bool { 269 | return d.sections[i].Header < d.sections[j].Header 270 | }) 271 | 272 | d.gui.Update(d.Layout) 273 | return s 274 | } 275 | 276 | // DeleteSection deletes the given section from this display. 277 | func (d *Display) DeleteSection(sec *Section) { 278 | d.mutex.Lock() 279 | defer d.mutex.Unlock() 280 | 281 | old := d.sections 282 | d.sections = make([]*Section, 0, len(d.sections)-1) 283 | for _, s := range old { 284 | if s != sec { 285 | d.sections = append(d.sections, s) 286 | } 287 | } 288 | 289 | d.gui.Update(func(g *gocui.Gui) error { 290 | return g.DeleteView(sec.Header) 291 | }) 292 | 293 | } 294 | -------------------------------------------------------------------------------- /command/docker.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/docker.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "archive/tar" 15 | "bufio" 16 | "bytes" 17 | "compress/gzip" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "path/filepath" 24 | "strings" 25 | "text/template" 26 | 27 | "github.com/docker/docker/api/types" 28 | "github.com/docker/docker/api/types/container" 29 | client "github.com/docker/docker/client" 30 | "github.com/docker/docker/pkg/stdcopy" 31 | ) 32 | 33 | // DockerfileAsset defines a asset name for Dockerfile. 34 | const DockerfileAsset = "assets/Dockerfile" 35 | 36 | // DockerfileOpt defines option variables used in Dockerfile templates. 37 | type DockerfileOpt struct { 38 | // Customize FROM clause. 39 | BaseImage string 40 | // Git repository. 41 | Repository string 42 | // URL for an Apt proxy. 43 | AptProxy string 44 | // URL for a pypi proxy. 45 | PypiProxy string 46 | // URL for a HTTP proxy. 47 | HTTPProxy string 48 | // URL for a HTTPS proxy. 49 | HTTPSProxy string 50 | // Comma separated URL lists. 51 | NoProxy string 52 | } 53 | 54 | type travisExt struct { 55 | *Travis 56 | *DockerfileOpt 57 | Archive string 58 | } 59 | 60 | // buildLog defines the JSON format of logs from building docker images. 61 | type buildLog struct { 62 | Stream string 63 | Error string 64 | ErrorDetail struct { 65 | Code int 66 | Message string 67 | } 68 | } 69 | 70 | // pullLog defines a JSON format used in logging information of ImagePull. 71 | type pullLog struct { 72 | Status string `json:"status"` 73 | ProgressDetail struct { 74 | Current int `json:"current"` 75 | Total int `json:"total"` 76 | } `json:"progressDetail"` 77 | Progress string `json:"progress"` 78 | ID string `json:"id"` 79 | } 80 | 81 | // Dockerfile creates a Dockerfile from an instance of Travis. 82 | func Dockerfile(travis *Travis, opt *DockerfileOpt, archive string) (res []byte, err error) { 83 | 84 | var data []byte 85 | 86 | // Loading the base template. 87 | data, err = Asset(DockerfileAsset) 88 | if err != nil { 89 | return 90 | } 91 | base, err := template.New("").Parse(string(data)) 92 | if err != nil { 93 | return 94 | } 95 | 96 | // Loading a child template. 97 | name := fmt.Sprintf("%s-%s", DockerfileAsset, travis.Language) 98 | data, err = Asset(name) 99 | if err != nil { 100 | data, err = Asset(fmt.Sprintf("%s-python", DockerfileAsset)) 101 | if err != nil { 102 | return 103 | } 104 | } 105 | temp, err := base.Parse(string(data)) 106 | if err != nil { 107 | return 108 | } 109 | 110 | // Checking optional parameters. 111 | opt.PypiProxy = strings.TrimSuffix(opt.PypiProxy, "/") 112 | 113 | // Creating Dockerfile. 114 | param := travisExt{ 115 | Travis: travis, 116 | DockerfileOpt: opt, 117 | Archive: archive, 118 | } 119 | buf := bytes.Buffer{} 120 | if err = temp.ExecuteTemplate(&buf, "base", ¶m); err != nil { 121 | return 122 | } 123 | res = buf.Bytes() 124 | 125 | return 126 | 127 | } 128 | 129 | // PrepareBaseImage pulls a docker images represented by the given ref if 130 | // necessary; it also writes summarized lorring information to the given output. 131 | func PrepareBaseImage(ctx context.Context, ref string, output io.Writer) (err error) { 132 | 133 | cli, err := client.NewEnvClient() 134 | if err != nil { 135 | return 136 | } 137 | 138 | reader, err := cli.ImagePull(ctx, ref, types.ImagePullOptions{}) 139 | if err != nil { 140 | return 141 | } 142 | defer reader.Close() 143 | 144 | var log pullLog 145 | status := make(map[string]string) 146 | scanner := bufio.NewScanner(reader) 147 | for scanner.Scan() { 148 | json.Unmarshal(scanner.Bytes(), &log) 149 | if log.ID != "" { 150 | cur := status[log.ID] 151 | if cur != log.Status { 152 | status[log.ID] = log.Status 153 | fmt.Fprintln(output, log.Status, log.ID) 154 | } 155 | } else { 156 | fmt.Fprintln(output, log.Status) 157 | } 158 | } 159 | 160 | return 161 | 162 | } 163 | 164 | // Build builds a docker image from a directory. The built image named tag. 165 | // The directory must have Dockerfile. 166 | func Build(ctx context.Context, dir, tag, version string, noCache bool, output io.Writer) (err error) { 167 | 168 | // Create a docker client. 169 | cli, err := client.NewEnvClient() 170 | if err != nil { 171 | return 172 | } 173 | defer cli.Close() 174 | 175 | // Create a pipe. 176 | reader, writer := io.Pipe() 177 | 178 | // Send the build context. 179 | go func() { 180 | defer writer.Close() 181 | archiveContext(ctx, dir, writer) 182 | }() 183 | 184 | // Start to build an image. 185 | res, err := cli.ImageBuild(ctx, reader, types.ImageBuildOptions{ 186 | Tags: []string{tag}, 187 | Remove: true, 188 | BuildArgs: map[string]*string{ 189 | "VERSION": &version, 190 | }, 191 | NoCache: noCache, 192 | }) 193 | if err != nil { 194 | return 195 | } 196 | defer res.Body.Close() 197 | 198 | // Wait untile the copy ends or the context will be canceled. 199 | done := make(chan struct{}) 200 | go func() { 201 | defer close(done) 202 | 203 | s := bufio.NewScanner(res.Body) 204 | for s.Scan() { 205 | var log buildLog 206 | if json.Unmarshal(s.Bytes(), &log) == nil { 207 | if log.Error != "" { 208 | err = fmt.Errorf(log.Error) 209 | fmt.Fprintf(output, "Error: %v - %v\n", log.ErrorDetail.Code, log.ErrorDetail.Message) 210 | return 211 | } 212 | io.WriteString(output, log.Stream) 213 | } 214 | } 215 | }() 216 | 217 | select { 218 | case <-ctx.Done(): 219 | return ctx.Err() 220 | case <-done: 221 | return 222 | } 223 | 224 | } 225 | 226 | // Start runs a container to run tests with a given context. 227 | // This function creates a container from the image of the given tag name, 228 | // and the created container has the given name. If the given name is empty, 229 | // the container will have a random temporary name and be deleted after 230 | // after all steps end. env is a list of environment variables to be passed 231 | // to the created container. 232 | func Start(ctx context.Context, tag, name string, env []string, output io.Writer) (err error) { 233 | 234 | // Create a docker client. 235 | cli, err := client.NewEnvClient() 236 | if err != nil { 237 | return 238 | } 239 | defer cli.Close() 240 | 241 | // Create a docker container. 242 | config := container.Config{ 243 | Image: tag, 244 | Env: env, 245 | } 246 | c, err := cli.ContainerCreate(ctx, &config, nil, nil, name) 247 | if err != nil { 248 | return 249 | } 250 | if name == "" { 251 | // If any container name isn't given, remove the container. 252 | // Note that, the context ctx may be canceled before removing the container, 253 | // and use another context here. 254 | defer cli.ContainerRemove(context.Background(), c.ID, types.ContainerRemoveOptions{}) 255 | } 256 | 257 | // Attach stdout and stderr of the container. 258 | stream, err := cli.ContainerAttach(ctx, c.ID, types.ContainerAttachOptions{ 259 | Stream: true, 260 | Stdout: true, 261 | Stderr: true, 262 | }) 263 | if err != nil { 264 | return 265 | } 266 | defer stream.Close() 267 | go stdcopy.StdCopy(output, output, stream.Reader) 268 | 269 | // Start the container. 270 | options := types.ContainerStartOptions{} 271 | if err = cli.ContainerStart(ctx, c.ID, options); err != nil { 272 | return 273 | } 274 | 275 | // Wait until the container ends. 276 | exit, errCh := cli.ContainerWait(ctx, c.ID, container.WaitConditionNotRunning) 277 | select { 278 | case <-ctx.Done(): 279 | // Kill the running container when the context is canceled. 280 | // The context ctx has been canceled already, use another context here. 281 | cli.ContainerKill(context.Background(), c.ID, "") 282 | return ctx.Err() 283 | case err = <-errCh: 284 | // Kill the running container when ContainerWait returns an error. 285 | // The context ctx has been canceled already, use another context here. 286 | cli.ContainerKill(context.Background(), c.ID, "") 287 | return 288 | case status := <-exit: 289 | if status.StatusCode != 0 { 290 | err = fmt.Errorf("Testing container returns an error (status code: %v)", status.StatusCode) 291 | } 292 | return 293 | } 294 | 295 | } 296 | 297 | // archiveContext makes a tar.gz stream consists of files. 298 | func archiveContext(ctx context.Context, root string, writer io.Writer) (err error) { 299 | 300 | // Create a buffered writer. 301 | bufWriter := bufio.NewWriter(writer) 302 | defer bufWriter.Flush() 303 | 304 | // Create a zipped writer on the bufferd writer. 305 | zipWriter, err := gzip.NewWriterLevel(bufWriter, gzip.BestCompression) 306 | if err != nil { 307 | return 308 | } 309 | defer zipWriter.Close() 310 | 311 | // Create a tarball writer on the zipped writer. 312 | tarWriter := tar.NewWriter(zipWriter) 313 | defer tarWriter.Close() 314 | 315 | // Create a tarball. 316 | sources, err := ioutil.ReadDir(root) 317 | if err != nil { 318 | return 319 | } 320 | for _, info := range sources { 321 | 322 | select { 323 | case <-ctx.Done(): 324 | return ctx.Err() 325 | 326 | default: 327 | // Write a file header. 328 | header, err := tar.FileInfoHeader(info, info.Name()) 329 | if err != nil { 330 | return err 331 | } 332 | tarWriter.WriteHeader(header) 333 | 334 | // Write the body. 335 | if err = copyFile(filepath.Join(root, info.Name()), tarWriter); err != nil { 336 | return err 337 | } 338 | } 339 | } 340 | 341 | return 342 | 343 | } 344 | -------------------------------------------------------------------------------- /command/docker_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/docker_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "regexp" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func TestDockerfile(t *testing.T) { 21 | 22 | travis := Travis{} 23 | opt := DockerfileOpt{ 24 | BaseImage: "testimage", 25 | AptProxy: "http://apt.proxy.test/", 26 | PypiProxy: "http://pypi.proxy.test/", 27 | HTTPProxy: "http://proxy.test/", 28 | HTTPSProxy: "https://proxy.test/", 29 | NoProxy: "localhost", 30 | } 31 | 32 | res, err := Dockerfile(&travis, &opt, SourceArchive) 33 | if err != nil { 34 | t.Error("Dockerfile returns an error:", err.Error()) 35 | } 36 | dockerfile := string(res) 37 | 38 | if !strings.Contains(dockerfile, fmt.Sprintln("FROM", opt.BaseImage)) { 39 | t.Error("The base image of the Dockerfile isn't correct:", dockerfile) 40 | } 41 | 42 | if !strings.Contains(dockerfile, fmt.Sprintln("ENV HTTP_PROXY", opt.HTTPProxy)) { 43 | t.Error("Dockerfile doesn't set the correct http proxy:", dockerfile) 44 | } 45 | 46 | if !strings.Contains(dockerfile, fmt.Sprintln("ENV HTTPS_PROXY", opt.HTTPSProxy)) { 47 | t.Error("Dockerfile doesn't set the correct https proxy:", dockerfile) 48 | } 49 | 50 | if !strings.Contains(dockerfile, fmt.Sprintln("ENV NO_PROXY", opt.NoProxy)) { 51 | t.Error("Dockerfile doesn't set the correct no proxy env:", dockerfile) 52 | } 53 | 54 | if !strings.Contains(dockerfile, fmt.Sprintf("Acquire::http { Proxy \\\"%s\\\"; };", opt.AptProxy)) { 55 | t.Error("Dockerfile doesn't specify the correct apt proxy:", dockerfile) 56 | } 57 | 58 | if !strings.Contains(dockerfile, fmt.Sprintf("RUN PYPI_PROXY=%s", opt.PypiProxy)) { 59 | t.Error("Dockerfile doesn't specify the correct pypi proxy:", dockerfile) 60 | } 61 | 62 | } 63 | 64 | func TestDockerfileWithoutOptions(t *testing.T) { 65 | 66 | travis := Travis{} 67 | opt := DockerfileOpt{ 68 | BaseImage: "testimage", 69 | } 70 | 71 | res, err := Dockerfile(&travis, &opt, SourceArchive) 72 | if err != nil { 73 | t.Error("Dockerfile returns an error:", err.Error()) 74 | } 75 | dockerfile := string(res) 76 | 77 | if strings.Contains(dockerfile, "ENV HTTP_PROXY") { 78 | t.Error("Dockerfile set a http proxy:", dockerfile) 79 | } 80 | 81 | if strings.Contains(dockerfile, "ENV HTTPS_PROXY") { 82 | t.Error("Dockerfile set a https proxy:", dockerfile) 83 | } 84 | 85 | if strings.Contains(dockerfile, "ENV NO_PROXY") { 86 | t.Error("Dockerfile set no_proxy env:", dockerfile) 87 | } 88 | 89 | if strings.Contains(dockerfile, "Acquire::http") { 90 | t.Error("Dockerfile set an apt proxy:", dockerfile) 91 | } 92 | 93 | if strings.Contains(dockerfile, "RUN PYPI_PROXY=") { 94 | t.Error("Dockerfile set a pypi proxy:", dockerfile) 95 | } 96 | 97 | } 98 | 99 | func TestDockerfilePython(t *testing.T) { 100 | 101 | var travis Travis 102 | travis.Language = "python" 103 | travis.Addons.Apt.Packages = []string{"package1", "package2"} 104 | 105 | opt := DockerfileOpt{ 106 | BaseImage: "ubuntu:latest", 107 | } 108 | archive := "source.tar.gz" 109 | 110 | res, err := Dockerfile(&travis, &opt, archive) 111 | if err != nil { 112 | t.Error("Dockerfile returns an error:", err.Error()) 113 | } 114 | dockerfile := string(res) 115 | 116 | if !strings.Contains(dockerfile, "RUN pip install --upgrade pip") { 117 | t.Error("Dockerfile doesn't install pip packages:", dockerfile) 118 | } 119 | 120 | m := regexp.MustCompile(`apt-get install -y \s* package1 \s* package2`) 121 | if m.FindString(dockerfile) == "" { 122 | t.Error("Dockerfile doesn't install required packages:", dockerfile) 123 | } 124 | 125 | if !strings.Contains(dockerfile, "ADD source.tar.gz /data") { 126 | t.Error("Dockerfile doesn't add correct source files:", dockerfile) 127 | } 128 | 129 | } 130 | 131 | func TestDockerfileGo(t *testing.T) { 132 | 133 | var travis Travis 134 | travis.Language = "go" 135 | travis.Addons.Apt.Packages = []string{"package1", "package2"} 136 | 137 | opt := DockerfileOpt{ 138 | BaseImage: "ubuntu:latest", 139 | Repository: "path/to/repo", 140 | } 141 | archive := "source.tar.gz" 142 | 143 | res, err := Dockerfile(&travis, &opt, archive) 144 | if err != nil { 145 | t.Error("Dockerfile returns an error:", err.Error()) 146 | } 147 | dockerfile := string(res) 148 | 149 | if !strings.Contains(dockerfile, "wget") { 150 | t.Error("Dockerfile doesn't install required packages:", dockerfile) 151 | } 152 | if !strings.Contains(dockerfile, "package1") { 153 | t.Error("Dockerfile doesn't install required packages:", dockerfile) 154 | } 155 | if !strings.Contains(dockerfile, "package2") { 156 | t.Error("Dockerfile doesn't install required packages:", dockerfile) 157 | } 158 | 159 | if !strings.Contains(dockerfile, fmt.Sprintf("ADD %s $GOPATH/src/%s", archive, opt.Repository)) { 160 | t.Error("Dockerfile doesn't add correct source files:", dockerfile) 161 | } 162 | 163 | } 164 | 165 | func TestDockerfileGoWithGoImportPath(t *testing.T) { 166 | 167 | var travis Travis 168 | travis.Language = "go" 169 | travis.GoImportPath = "example.org/pkg/foo" 170 | 171 | opt := DockerfileOpt{ 172 | BaseImage: "ubuntu:latest", 173 | Repository: "path/to/repo", 174 | } 175 | archive := "source.tar.gz" 176 | 177 | res, err := Dockerfile(&travis, &opt, archive) 178 | if err != nil { 179 | t.Error("Dockerfile returns an error:", err.Error()) 180 | } 181 | dockerfile := string(res) 182 | 183 | if !strings.Contains(dockerfile, fmt.Sprintf("ADD %s $GOPATH/src/%s", archive, travis.GoImportPath)) { 184 | t.Error("Dockerfile doesn't add correct source files:", dockerfile) 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /command/entrypoint.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/entrypoint.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "bytes" 15 | "fmt" 16 | "text/template" 17 | ) 18 | 19 | // EntrypointAsset defines a asset name for entrypoint.sh. 20 | const EntrypointAsset = "assets/entrypoint" 21 | 22 | // Entrypoint creates an entrypoint.sh from an instance of Travis. 23 | func Entrypoint(travis *Travis) (res []byte, err error) { 24 | 25 | var ( 26 | data []byte 27 | temp *template.Template 28 | ) 29 | 30 | // Loading the base template. 31 | data, err = Asset(EntrypointAsset) 32 | if err != nil { 33 | return 34 | } 35 | base, err := template.New("").Parse(string(data)) 36 | if err != nil { 37 | return 38 | } 39 | 40 | // Loading a child template. 41 | name := fmt.Sprintf("%s-%s", EntrypointAsset, travis.Language) 42 | data, err = Asset(name) 43 | if err == nil { 44 | temp, err = base.Parse(string(data)) 45 | if err != nil { 46 | return 47 | } 48 | } else { 49 | temp = base 50 | } 51 | 52 | // Creating an entrypont. 53 | buf := bytes.Buffer{} 54 | if err = temp.ExecuteTemplate(&buf, "base", travis); err != nil { 55 | return 56 | } 57 | res = buf.Bytes() 58 | 59 | return 60 | 61 | } 62 | -------------------------------------------------------------------------------- /command/entrypoint_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/entrypoint_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | func TestEntrypointPython(t *testing.T) { 20 | 21 | travis := Travis{ 22 | Language: "python", 23 | BeforeInstall: []string{"binstall_1", "binstall_2"}, 24 | Install: []string{"install_1", "install_2"}, 25 | BeforeScript: []string{"before_1", "before_2"}, 26 | Script: []string{"script_1", "script_2"}, 27 | } 28 | 29 | res, err := Entrypoint(&travis) 30 | if err != nil { 31 | t.Error("Entrypoint returns an error:", err.Error()) 32 | } 33 | e := string(res) 34 | t.Log(e) 35 | 36 | if !strings.Contains(e, travis.BeforeInstall[0]) || !strings.Contains(e, travis.BeforeInstall[1]) { 37 | t.Error("Entrypoint doesn't have correct before install steps:", e) 38 | } 39 | 40 | if !strings.Contains(e, travis.Install[0]) || !strings.Contains(e, travis.Install[1]) { 41 | t.Error("Entrypoint doesn't have correct install steps:", e) 42 | } 43 | 44 | if !strings.Contains(e, travis.BeforeScript[0]) || !strings.Contains(e, travis.BeforeScript[1]) { 45 | t.Error("Entrypoint doesn't have correct before script steps:", e) 46 | } 47 | 48 | if !strings.Contains(e, travis.Script[0]) || !strings.Contains(e, travis.Script[1]) { 49 | t.Error("Entrypoint doesn't have correct script steps:", e) 50 | } 51 | 52 | } 53 | 54 | func TestEntrypointGo(t *testing.T) { 55 | 56 | travis := Travis{ 57 | Language: "go", 58 | BeforeInstall: []string{"binstall_1", "binstall_2"}, 59 | Install: []string{"install_1", "install_2"}, 60 | BeforeScript: []string{"before_1", "before_2"}, 61 | Script: []string{"script_1", "script_2"}, 62 | } 63 | 64 | res, err := Entrypoint(&travis) 65 | if err != nil { 66 | t.Error("Entrypoint returns an error:", err.Error()) 67 | } 68 | e := string(res) 69 | t.Log(e) 70 | 71 | if !strings.Contains(e, travis.BeforeInstall[0]) || !strings.Contains(e, travis.BeforeInstall[1]) { 72 | t.Error("Entrypoint doesn't have correct before install steps:", e) 73 | } 74 | 75 | if !strings.Contains(e, travis.Install[0]) || !strings.Contains(e, travis.Install[1]) { 76 | t.Error("Entrypoint doesn't have correct install steps:", e) 77 | } 78 | 79 | if !strings.Contains(e, travis.BeforeScript[0]) || !strings.Contains(e, travis.BeforeScript[1]) { 80 | t.Error("Entrypoint doesn't have correct before script steps:", e) 81 | } 82 | 83 | if !strings.Contains(e, travis.Script[0]) || !strings.Contains(e, travis.Script[1]) { 84 | t.Error("Entrypoint doesn't have correct script steps:", e) 85 | } 86 | 87 | } 88 | 89 | func TestEntrypointGoByDefault(t *testing.T) { 90 | 91 | travis := Travis{ 92 | Language: "go", 93 | } 94 | 95 | res, err := Entrypoint(&travis) 96 | if err != nil { 97 | t.Error("Entrypoint returns an error:", err.Error()) 98 | } 99 | e := string(res) 100 | t.Log(e) 101 | 102 | if !strings.Contains(e, "go get -t ./...") { 103 | t.Error("Entrypoint doesn't have the default install steps:", e) 104 | } 105 | 106 | if !strings.Contains(e, "go test -v ./...") { 107 | t.Error("Entrypoint doesn't have the default script steps:", e) 108 | } 109 | 110 | } 111 | 112 | func TestEntrypointGoWithGoBuildArgs(t *testing.T) { 113 | 114 | travis := Travis{ 115 | Language: "go", 116 | GoBuildArgs: "-a -b -c -d -e", 117 | } 118 | 119 | res, err := Entrypoint(&travis) 120 | if err != nil { 121 | t.Error("Entrypoint returns an error:", err.Error()) 122 | } 123 | e := string(res) 124 | 125 | if !strings.Contains(e, fmt.Sprintf("go test %s", travis.GoBuildArgs)) { 126 | t.Error("Entrypoint doesn't have go build args in the script step:", e) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /command/errset.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/errset.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "sort" 15 | "sync" 16 | ) 17 | 18 | // ErrorSet is a goroutine-safe set of errors. Each error in this set has a key 19 | // and GetList function returns a list of errors according to the order of keys. 20 | type ErrorSet struct { 21 | errors map[string]error 22 | mutex sync.Mutex 23 | } 24 | 25 | // NewErrorSet creates a new ErrorSet. 26 | func NewErrorSet() *ErrorSet { 27 | return &ErrorSet{ 28 | errors: make(map[string]error), 29 | } 30 | } 31 | 32 | // Add a new error with a key string. 33 | func (e *ErrorSet) Add(key string, err error) { 34 | e.mutex.Lock() 35 | defer e.mutex.Unlock() 36 | 37 | e.errors[key] = err 38 | } 39 | 40 | // GetList returns a list of errors in this set; the list is sorted by the order 41 | // of keys. 42 | func (e *ErrorSet) GetList() []error { 43 | e.mutex.Lock() 44 | defer e.mutex.Unlock() 45 | 46 | keys := make(sort.StringSlice, 0, len(e.errors)) // Not call Size() here, because of the lock. 47 | for k := range e.errors { 48 | keys = append(keys, k) 49 | } 50 | sort.Sort(keys) 51 | 52 | res := make([]error, 0, len(e.errors)) // Not call Size() here, because of the lock. 53 | for _, k := range keys { 54 | res = append(res, e.errors[k]) 55 | } 56 | return res 57 | } 58 | 59 | // Size returns the number of errors in this set. 60 | func (e *ErrorSet) Size() int { 61 | e.mutex.Lock() 62 | defer e.mutex.Unlock() 63 | 64 | return len(e.errors) 65 | } 66 | -------------------------------------------------------------------------------- /command/errset_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/errset_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "io" 16 | "testing" 17 | ) 18 | 19 | func TestErrorSet(t *testing.T) { 20 | 21 | errs := NewErrorSet() 22 | if errs.Size() != 0 { 23 | t.Error("New ErrorSet contains some error already.") 24 | } 25 | 26 | errs.Add("eof", io.EOF) 27 | if errs.Size() != 1 { 28 | t.Error("ErrorSet returns a wrong size.") 29 | } 30 | 31 | var ret []error 32 | ret = errs.GetList() 33 | if len(ret) != 1 { 34 | t.Error("Length of a slice returned from an ErrorSet is wrong.") 35 | } else if ret[0] != io.EOF { 36 | t.Error("Slice returned from an ErrorSet consists of wrong errors.") 37 | } 38 | 39 | another := fmt.Errorf("Another error") 40 | errs.Add("another", another) 41 | ret = errs.GetList() 42 | if len(ret) != 2 { 43 | t.Error("Length of a slice returned from an ErrorSet is wrong.") 44 | } else if ret[0] != another || ret[1] != io.EOF { 45 | t.Error("Slice returned from an ErrorSet consists in a wrong order.") 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /command/run.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/run.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "context" 15 | "crypto/md5" 16 | "encoding/binary" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "os" 21 | "os/signal" 22 | "path" 23 | "path/filepath" 24 | "strconv" 25 | "strings" 26 | "sync" 27 | "syscall" 28 | 29 | colorable "github.com/mattn/go-colorable" 30 | gitconfig "github.com/tcnksm/go-gitconfig" 31 | "github.com/ttacon/chalk" 32 | "github.com/urfave/cli" 33 | ) 34 | 35 | // SourceArchive defines a name of source archive file. 36 | const SourceArchive = "source.tar.gz" 37 | 38 | // RunOpt defines a option parameter for run function. 39 | type RunOpt struct { 40 | // Same options as DockerfileOpt. 41 | *DockerfileOpt 42 | // Travis configuration file. 43 | Filename string 44 | // Container name. 45 | Name string 46 | // Runtime version to which only versions matching will be run. 47 | Version string 48 | // Image tag. 49 | Tag string 50 | // Max processors to be used. 51 | Processors int 52 | // If true, logging information to be stored to files. 53 | OutputLog bool 54 | // If true, not using cache during buidling a docker image. 55 | NoCache bool 56 | // If true, omit printing color codes. 57 | NoColor bool 58 | // Printed on the header. 59 | Title string 60 | } 61 | 62 | // Run implements the action of this command. 63 | func Run(c *cli.Context) error { 64 | 65 | opt := RunOpt{ 66 | DockerfileOpt: &DockerfileOpt{ 67 | BaseImage: c.String("base"), 68 | AptProxy: c.String("apt-proxy"), 69 | PypiProxy: c.String("pypi-proxy"), 70 | HTTPProxy: c.String("http-proxy"), 71 | HTTPSProxy: c.String("https-proxy"), 72 | NoProxy: c.String("no-proxy"), 73 | }, 74 | Filename: c.Args().First(), 75 | Name: c.String("name"), 76 | Version: c.String("select"), 77 | Tag: c.String("tag"), 78 | Processors: c.Int("max-processors"), 79 | OutputLog: c.Bool("log"), 80 | NoCache: c.Bool("no-cache"), 81 | NoColor: c.Bool("no-color"), 82 | Title: fmt.Sprintf("%v %v", c.App.Name, c.App.Version), 83 | } 84 | if err := run(&opt); err != nil { 85 | return cli.NewExitError(err.Error(), 1) 86 | } 87 | return nil 88 | } 89 | 90 | func run(opt *RunOpt) (err error) { 91 | 92 | // Prepare to be canceled. 93 | ctx, cancel := context.WithCancel(context.Background()) 94 | defer cancel() 95 | sig := make(chan os.Signal, 1) 96 | signal.Notify(sig, os.Interrupt, os.Kill, syscall.SIGQUIT) 97 | go func() { 98 | <-sig 99 | cancel() 100 | }() 101 | 102 | // Prepare interface. 103 | display, ctx, err := NewDisplay(ctx, opt.Title, opt.Processors) 104 | if err != nil { 105 | return 106 | } 107 | defer display.Close() 108 | logger := display.Header.Logger 109 | 110 | var stdout io.Writer 111 | if opt.NoColor { 112 | stdout = colorable.NewNonColorable(os.Stdout) 113 | logger = colorable.NewNonColorable(logger) 114 | cli.ErrWriter = colorable.NewNonColorable(cli.ErrWriter) 115 | } else { 116 | stdout = colorable.NewColorableStdout() 117 | } 118 | 119 | // Load a Travis's script file. 120 | if opt.Filename == "" { 121 | opt.Filename = ".travis.yml" 122 | } 123 | fmt.Fprintln(logger, chalk.Cyan.Color("Loading .travis.yml")) 124 | 125 | travis, err := NewTravisFromFile(opt.Filename) 126 | if err != nil { 127 | return 128 | } 129 | 130 | // Get repository information. 131 | fmt.Fprintln(logger, chalk.Cyan.Color("Checking repository information")) 132 | origin, err := gitconfig.OriginURL() 133 | if err != nil { 134 | return 135 | } 136 | opt.Repository = getRepository(origin) 137 | 138 | // Set up the tag name of the container image. 139 | if opt.Tag == "" { 140 | opt.Tag = fmt.Sprintf("loci/%s", strings.ToLower(path.Base(opt.Repository))) 141 | } 142 | 143 | // Prepare docker images. 144 | fmt.Fprintln(logger, chalk.Cyan.Color("Preparing docker images for sandbox containers")) 145 | err = PrepareBaseImage(ctx, opt.BaseImage, logger) 146 | if err != nil { 147 | return 148 | } 149 | 150 | // Archive source files. 151 | fmt.Fprintln(logger, chalk.Cyan.Color("Archiving source code")) 152 | tempDir := filepath.Join(os.TempDir(), opt.Tag) 153 | if err = os.MkdirAll(tempDir, 0777); err != nil { 154 | return 155 | } 156 | defer os.RemoveAll(tempDir) 157 | pwd, err := os.Getwd() 158 | if err != nil { 159 | return 160 | } 161 | if err = Archive(ctx, pwd, filepath.Join(tempDir, SourceArchive)); err != nil { 162 | return 163 | } 164 | 165 | // Create Dockerfile. 166 | fmt.Fprintln(logger, chalk.Cyan.Color("Creating Dockerfile")) 167 | docker, err := Dockerfile(travis, opt.DockerfileOpt, SourceArchive) 168 | if err != nil { 169 | return 170 | } 171 | if err = ioutil.WriteFile(filepath.Join(tempDir, "Dockerfile"), docker, 0644); err != nil { 172 | return 173 | } 174 | 175 | // Create entrypoint.sh. 176 | fmt.Fprintln(logger, chalk.Cyan.Color("Creating entrypoint.sh")) 177 | entry, err := Entrypoint(travis) 178 | if err != nil { 179 | return 180 | } 181 | if err = ioutil.WriteFile(filepath.Join(tempDir, "entrypoint.sh"), entry, 0644); err != nil { 182 | return 183 | } 184 | 185 | argset, err := travis.ArgumentSet(logger) 186 | if err != nil { 187 | return 188 | } 189 | 190 | // Start testing with goroutines. 191 | fmt.Fprintln(logger, chalk.Cyan.Color("Building sandbox images and running tests")) 192 | var i int 193 | var wg sync.WaitGroup 194 | semaphore := make(chan struct{}, opt.Processors) 195 | errs := NewErrorSet() 196 | for version, set := range argset { 197 | 198 | if opt.Version != "" && version != opt.Version { 199 | continue 200 | } 201 | 202 | wg.Add(1) 203 | go func(version string, set []TestCase) (err error) { 204 | semaphore <- struct{}{} 205 | defer func() { 206 | <-semaphore 207 | wg.Done() 208 | }() 209 | 210 | // Build a container image. 211 | sec := display.AddSection(fmt.Sprintf("Building a docker image for %v", version)) 212 | defer display.DeleteSection(sec) 213 | 214 | var output io.Writer 215 | writer := sec.Writer() 216 | defer writer.Close() 217 | output = writer 218 | if opt.NoColor { 219 | output = colorable.NewNonColorable(output) 220 | } 221 | 222 | if opt.OutputLog { 223 | var fp *os.File 224 | fp, err = os.OpenFile(fmt.Sprintf("loci-build-%v.log", version), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 225 | if err != nil { 226 | errs.Add(version, err) 227 | return 228 | } 229 | defer fp.Close() 230 | output = io.MultiWriter(output, colorable.NewColorable(fp)) 231 | } 232 | 233 | tag := fmt.Sprintf("%v/%v", opt.Tag, version) 234 | err = Build(ctx, tempDir, tag, version, opt.NoCache, output) 235 | if err == context.Canceled { 236 | errs.Add("", err) 237 | return 238 | } else if err != nil { 239 | msg := fmt.Sprintf(chalk.Red.Color("Faild to build a docker image for %v"), version) 240 | errs.Add( 241 | version, 242 | fmt.Errorf("%v\n%v\n%v\n", msg, err.Error(), sec.String())) 243 | fmt.Fprintln(logger, msg) 244 | return 245 | } 246 | fmt.Fprintln(logger, chalk.Green.Color(fmt.Sprintf("Built a docker image for %v", version))) 247 | 248 | for _, c := range set { 249 | 250 | wg.Add(1) 251 | go func(envs []string) { 252 | semaphore <- struct{}{} 253 | defer func() { 254 | <-semaphore 255 | wg.Done() 256 | }() 257 | 258 | // Run tests in a sandbox. 259 | sec := display.AddSection(fmt.Sprintf("Running tests (%v: %v)", version, envs)) 260 | defer display.DeleteSection(sec) 261 | 262 | var output io.Writer 263 | writer := sec.Writer() 264 | defer writer.Close() 265 | output = writer 266 | if opt.NoColor { 267 | output = colorable.NewNonColorable(output) 268 | } 269 | 270 | if opt.OutputLog { 271 | var fp *os.File 272 | hash := md5.Sum([]byte(strings.Join(envs, "-"))) 273 | fp, err = os.OpenFile( 274 | fmt.Sprintf("loci-%v-%v.log", version, strconv.FormatInt(int64(binary.BigEndian.Uint64(hash[:])), 36)), 275 | os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 276 | if err != nil { 277 | errs.Add(fmt.Sprintf("%v:%v", version, envs), err) 278 | return 279 | } 280 | defer fp.Close() 281 | 282 | fmt.Fprintln(fp, "* Environment variables *") 283 | for _, v := range envs { 284 | fmt.Fprintln(fp, v) 285 | } 286 | fmt.Fprintln(fp, "") 287 | 288 | output = io.MultiWriter(output, colorable.NewColorable(fp)) 289 | } 290 | 291 | name := opt.Name 292 | if name != "" { 293 | i++ 294 | name = fmt.Sprintf("%s-%d", name, i) 295 | } 296 | 297 | err = Start(ctx, tag, name, envs, output) 298 | if err == context.Canceled { 299 | errs.Add("", err) 300 | } else if err != nil { 301 | errs.Add(fmt.Sprintf("%v:%v", version, envs), fmt.Errorf("%s\n%s", chalk.Red.Color(err.Error()), sec.String())) 302 | fmt.Fprintln(logger, chalk.Red.Color(fmt.Sprintf("Failed tests (%v: %v) ", version, envs))) 303 | } else { 304 | fmt.Fprintln(logger, chalk.Green.Color(fmt.Sprintf("Passed tests (%v: %v) ", version, envs))) 305 | } 306 | return 307 | 308 | }(c.Slice()) 309 | 310 | } 311 | 312 | return 313 | 314 | }(version, set) 315 | 316 | } 317 | 318 | wg.Wait() 319 | err = display.Close() 320 | if err != nil { 321 | errs.Add("", err) 322 | } 323 | 324 | if errs.Size() == 0 { 325 | fmt.Fprintln(stdout, chalk.Green.Color("All tests have been passed.")) 326 | } else { 327 | errList := errs.GetList() 328 | err = cli.NewMultiError(errList...) 329 | } 330 | return 331 | 332 | } 333 | 334 | // getRepository returns the repository path from a given remote URL of 335 | // origin repository. The repository path consists of a URL without 336 | // sheme, user name, password, and .git suffix. 337 | func getRepository(origin string) (res string) { 338 | 339 | switch { 340 | case strings.Contains(origin, "@"): 341 | res = strings.Replace(strings.Split(origin, "@")[1], ":", "/", 1) 342 | case strings.HasPrefix(origin, "http://"): 343 | res = origin[len("http://"):] 344 | case strings.HasPrefix(origin, "https://"): 345 | res = origin[len("https://"):] 346 | default: 347 | res = strings.Replace(origin, ":", "/", 1) 348 | } 349 | if strings.HasSuffix(res, ".git") { 350 | res = res[:len(res)-len(".git")] 351 | } 352 | 353 | return 354 | 355 | } 356 | -------------------------------------------------------------------------------- /command/run_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/run_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import "testing" 14 | 15 | func TestGetRepository(t *testing.T) { 16 | 17 | expected := "github.com/jkawamoto/loci" 18 | 19 | if res := getRepository("ssh://git@github.com/jkawamoto/loci.git"); res != expected { 20 | t.Error("Wrong repository path:", res) 21 | } 22 | 23 | if res := getRepository("ssh://git:pw@github.com/jkawamoto/loci.git"); res != expected { 24 | t.Error("Wrong repository path:", res) 25 | } 26 | 27 | if res := getRepository("http://github.com/jkawamoto/loci.git"); res != expected { 28 | t.Error("Wrong repository path:", res) 29 | } 30 | 31 | if res := getRepository("http://username@github.com/jkawamoto/loci.git"); res != expected { 32 | t.Error("Wrong repository path:", res) 33 | } 34 | 35 | if res := getRepository("http://username:pw@github.com/jkawamoto/loci.git"); res != expected { 36 | t.Error("Wrong repository path:", res) 37 | } 38 | 39 | if res := getRepository("https://github.com/jkawamoto/loci.git"); res != expected { 40 | t.Error("Wrong repository path:", res) 41 | } 42 | 43 | if res := getRepository("https://username@github.com/jkawamoto/loci.git"); res != expected { 44 | t.Error("Wrong repository path:", res) 45 | } 46 | 47 | if res := getRepository("https://username:pw@github.com/jkawamoto/loci.git"); res != expected { 48 | t.Error("Wrong repository path:", res) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /command/travis.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "io" 16 | "io/ioutil" 17 | "os" 18 | "sort" 19 | "strings" 20 | 21 | "gopkg.in/yaml.v2" 22 | ) 23 | 24 | // Travis defines the structure of .travis.yml. 25 | type Travis struct { 26 | // Base language. 27 | Language string 28 | // List of addons. 29 | Addons struct { 30 | Apt struct { 31 | Packages []string 32 | } `yaml:"apt,omitempty"` 33 | } `yaml:"addons,omitempty"` 34 | 35 | // List of commands run before install steps. 36 | BeforeInstall ListOrString `yaml:"before_install,omitempty"` 37 | 38 | // List of commands used to install packages. 39 | Install ListOrString `yaml:"install,omitempty"` 40 | 41 | // List of commands run before main scripts. 42 | BeforeScript ListOrString `yaml:"before_script,omitempty"` 43 | 44 | // List of scripts. 45 | Script ListOrString `yaml:"script,omitempty"` 46 | 47 | // List of environment variables. 48 | Env Env `yaml:"env,omitempty"` 49 | 50 | // Configuration for matrix build. 51 | Matrix Matrix `yaml:"matrix,omitempty"` 52 | 53 | // List of python versions. (used only in python) 54 | Python []string `yaml:"python,omitempty"` 55 | 56 | // List of golang versions. (used only in go) 57 | Go []string `yaml:"go,omitempty"` 58 | // Go import path. (used only in go) 59 | GoImportPath string `yaml:"go_import_path,omitempty"` 60 | // Build args for go project. (used only in go) 61 | GoBuildArgs string `yaml:"gobuild_args,omitempty"` 62 | } 63 | 64 | // Env defines the full structure of a definition of environment variables. 65 | type Env struct { 66 | Global []string `yaml:"global,omitempty"` 67 | Matrix []string `yaml:"matrix,omitempty"` 68 | } 69 | 70 | // Matrix defines the structure of matrix element in .travis.yml. 71 | type Matrix struct { 72 | Include []interface{} `yaml:"include,omitempty"` 73 | Exclude []interface{} `yaml:"exclude,omitempty"` 74 | } 75 | 76 | // TestCase is a set of environment variables and represented as a map of which 77 | // a key is a name of one environment variable and the associated value is the 78 | // value of the variable. 79 | type TestCase map[string]string 80 | 81 | // Slice returns a slice of strings representing this test case. 82 | func (c TestCase) Slice() (res []string) { 83 | for key, value := range c { 84 | res = append(res, fmt.Sprintf("%v=%v", key, value)) 85 | } 86 | sort.Strings(res) 87 | return 88 | } 89 | 90 | // Copy returns a hard copy of this test case. 91 | func (c TestCase) Copy() TestCase { 92 | res := make(TestCase) 93 | for k, v := range c { 94 | res[k] = v 95 | } 96 | return res 97 | } 98 | 99 | // Merge updates this TestCase so that it also has key and values defined in the 100 | // given test case. If both test cases have a same key, the value associated 101 | // with the key will be overwritten by the value in the given test case. 102 | func (c TestCase) Merge(o TestCase) TestCase { 103 | for k, v := range o { 104 | c[k] = v 105 | } 106 | return c 107 | } 108 | 109 | // Match returns true if and only if the given TestCase has same configuration 110 | // as this test case. 111 | func (c TestCase) Match(o TestCase) bool { 112 | 113 | if len(c) != len(o) { 114 | return false 115 | } 116 | for k, v := range o { 117 | if c[k] != v { 118 | return false 119 | } 120 | } 121 | return true 122 | 123 | } 124 | 125 | // TestCaseSet defines a set of arguments for build matrix. 126 | // The test case set is a map of which key is a version and the associated value 127 | // is a list of test cases. 128 | type TestCaseSet map[string][]TestCase 129 | 130 | // NewTravis creates a Travis object from a byte array. 131 | func NewTravis(buf []byte) (res *Travis, err error) { 132 | 133 | res = &Travis{} 134 | if err = yaml.Unmarshal(buf, res); err != nil { 135 | return 136 | } 137 | return 138 | 139 | } 140 | 141 | // NewTravisFromFile creates a Travis object from a file. 142 | func NewTravisFromFile(filename string) (res *Travis, err error) { 143 | 144 | fp, err := os.Open(filename) 145 | if err != nil { 146 | return 147 | } 148 | defer fp.Close() 149 | 150 | buf, err := ioutil.ReadAll(fp) 151 | if err != nil { 152 | return 153 | } 154 | return NewTravis(buf) 155 | 156 | } 157 | 158 | // ArgumentSet returns a set of arguments to run entrypoint based on a build 159 | // matrix. 160 | func (t *Travis) ArgumentSet(logger io.Writer) (res TestCaseSet, err error) { 161 | 162 | switch t.Language { 163 | case "python": 164 | res, err = t.argumentSetPython(logger) 165 | case "go": 166 | res, err = t.argumentSetGo() 167 | default: 168 | res = make(TestCaseSet) 169 | res[""] = []TestCase{} 170 | } 171 | 172 | return 173 | 174 | } 175 | 176 | // UnmarshalYAML defines a way to unmarshal variables of Env. 177 | func (e *Env) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { 178 | 179 | var aux interface{} 180 | if err = unmarshal(&aux); err != nil { 181 | return 182 | } 183 | 184 | switch raw := aux.(type) { 185 | case []interface{}: 186 | // If attribute env has one list instead of global and/or matrix attributes. 187 | if len(raw) == 0 { 188 | return 189 | } 190 | value := make([]string, len(raw)) 191 | for i, r := range raw { 192 | v, ok := r.(string) 193 | if !ok { 194 | return fmt.Errorf("An item in evn cannot be converted to a string: %v", aux) 195 | } 196 | value[i] = v 197 | } 198 | // If each string has more than two variables, it means matrix configuration. 199 | if len(strings.Split(strings.TrimSpace(value[0]), " ")) == 1 { 200 | e.Global = value 201 | } else { 202 | e.Matrix = value 203 | } 204 | 205 | case map[interface{}]interface{}: 206 | e.Global = parseEnvMap(raw, "global") 207 | e.Matrix = parseEnvMap(raw, "matrix") 208 | 209 | } 210 | 211 | return 212 | 213 | } 214 | 215 | // parseEnvMap parses a map of which key and value are defined as interface{}, 216 | // and returns a list of strings the given map's value, which is associated with 217 | // the given key, represents. 218 | func parseEnvMap(m map[interface{}]interface{}, key string) (res []string) { 219 | 220 | if selected, exist := m[key]; exist { 221 | switch items := selected.(type) { 222 | case string: 223 | res = []string{items} 224 | 225 | case []interface{}: 226 | for _, v := range items { 227 | if s, ok := v.(string); ok { 228 | res = append(res, s) 229 | } 230 | } 231 | } 232 | } 233 | 234 | return 235 | 236 | } 237 | 238 | // parseEnv parses a string representing a set of environment variable 239 | // definitions; and returns a TestCase. 240 | func parseEnv(env string) (c TestCase) { 241 | c = make(TestCase) 242 | 243 | b := 0 244 | quoted := false 245 | for i, v := range env { 246 | if v == '"' { 247 | quoted = !quoted 248 | } 249 | if !quoted && v == ' ' { 250 | pair := strings.SplitN(strings.Replace(env[b:i], "\"", "", 2), "=", 2) 251 | if len(pair) == 2 { 252 | c[pair[0]] = pair[1] 253 | } 254 | b = i + 1 255 | } 256 | } 257 | pair := strings.SplitN(strings.Replace(env[b:], "\"", "", 2), "=", 2) 258 | if len(pair) == 2 { 259 | c[pair[0]] = pair[1] 260 | } 261 | return 262 | 263 | } 264 | -------------------------------------------------------------------------------- /command/travis_go.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis_go.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "strings" 16 | ) 17 | 18 | // argumentSetGo returns a set of arguments to run entrypoint based on a build 19 | // matrix for Go projects. 20 | func (t *Travis) argumentSetGo() (res TestCaseSet, err error) { 21 | 22 | res = make(TestCaseSet) 23 | global := parseEnv(strings.Join(t.Env.Global, " ")) 24 | 25 | // Parse Matrix.Include. 26 | for _, v := range t.Matrix.Include { 27 | version, c, err := parseMatrixGo(v) 28 | if err != nil { 29 | return nil, err 30 | } 31 | res[version] = append(res[version], global.Copy().Merge(c)) 32 | } 33 | 34 | // Parse general environment. 35 | if len(t.Go) == 0 && len(res) == 0 { 36 | t.Go = []string{"any"} 37 | } 38 | for _, version := range t.Go { 39 | 40 | if len(t.Env.Matrix) == 0 { 41 | // If there is no matrix configuration, use only global configuration. 42 | res[version] = append(res[version], global) 43 | } else { 44 | // Look up each matrix case, and merge sprcific configuration to the 45 | // global one. 46 | for _, m := range t.Env.Matrix { 47 | c := parseEnv(m) 48 | res[version] = append(res[version], global.Copy().Merge(c)) 49 | } 50 | } 51 | 52 | } 53 | 54 | // Parse Matrix.Exclude. 55 | for _, v := range t.Matrix.Exclude { 56 | version, exclude, err := parseMatrixGo(v) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if set, ok := res[version]; ok { 61 | var new []TestCase 62 | for _, c := range set { 63 | if !c.Match(exclude) { 64 | new = append(new, c) 65 | } 66 | } 67 | res[version] = new 68 | } 69 | } 70 | 71 | return 72 | 73 | } 74 | 75 | // parseMatrixGo parses an interface representing an entry of env.matrix; 76 | // and returns the version and test case the interface specifies for golang. 77 | func parseMatrixGo(v interface{}) (version string, c TestCase, err error) { 78 | 79 | m, ok := v.(map[interface{}]interface{}) 80 | if !ok { 81 | err = fmt.Errorf("Given item is broken.") 82 | return 83 | } 84 | 85 | version = fmt.Sprint(m["go"]) 86 | 87 | variables, ok := m["env"].(string) 88 | if !ok { 89 | err = fmt.Errorf("Env of the given item is broken.") 90 | return 91 | } 92 | c = parseEnv(variables) 93 | 94 | return 95 | 96 | } 97 | -------------------------------------------------------------------------------- /command/travis_go_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis_go_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | package command 11 | 12 | import ( 13 | "io/ioutil" 14 | "testing" 15 | ) 16 | 17 | // GoCase defines a case of matrix evaluation for go projects. 18 | type GoCase struct { 19 | Go string `yaml:"go"` 20 | Env string `yaml:"env"` 21 | } 22 | 23 | func TestGoMatrixInclude(t *testing.T) { 24 | 25 | var err error 26 | travis, err := storeAndLoadTravis(&Travis{ 27 | Language: "go", 28 | Matrix: Matrix{ 29 | Include: []interface{}{ 30 | GoCase{ 31 | Go: "1.6", 32 | Env: "FOO=bar", 33 | }, GoCase{ 34 | Go: "1.7", 35 | Env: "FOO=fuga", 36 | }, 37 | }, 38 | }, 39 | }) 40 | if err != nil { 41 | t.Error(err.Error()) 42 | } 43 | if len(travis.Matrix.Include) != 2 { 44 | t.Error("Size of items in matrix.include is wrong:", travis.Matrix.Include) 45 | } 46 | 47 | res, err := travis.ArgumentSet(ioutil.Discard) 48 | if err != nil { 49 | t.Error(err.Error()) 50 | } 51 | 52 | t.Log("Arguments:", res) 53 | if len(res) != 2 { 54 | t.Fatal("Generated arguments are wrong:", res) 55 | } 56 | 57 | if set, ok := res["1.6"]; !ok { 58 | t.Error("Version is wrong:", res) 59 | } else if len(set) != 1 || len(set[0]) != 1 || set[0]["FOO"] != "bar" { 60 | t.Error("Env has wrong values:", res) 61 | } 62 | 63 | if set, ok := res["1.7"]; !ok { 64 | t.Error("Version is wrong:", res) 65 | } else if len(set) != 1 || len(set[0]) != 1 || set[0]["FOO"] != "fuga" { 66 | t.Error("Env has wrong values:", res) 67 | } 68 | 69 | } 70 | 71 | func TestGoMatrixExclude(t *testing.T) { 72 | 73 | var err error 74 | travis, err := NewTravis([]byte(`language: "go" 75 | go: 76 | - 1.6 77 | - 1.7 78 | env: 79 | - FOO=foo BAR=bar 80 | - FOO=bar BAR=foo 81 | matrix: 82 | exclude: 83 | - go: 1.7 84 | env: FOO=bar BAR=foo 85 | `)) 86 | 87 | if err != nil { 88 | t.Fatal(err.Error()) 89 | } 90 | 91 | if len(travis.Matrix.Exclude) != 1 { 92 | t.Error("Size of items in matrix.include is wrong:", travis.Matrix.Exclude) 93 | } 94 | 95 | res, err := travis.ArgumentSet(ioutil.Discard) 96 | if err != nil { 97 | t.Error(err.Error()) 98 | } 99 | 100 | t.Log("Arguments:", res) 101 | if len(res) != 2 { 102 | t.Fatal("Generated arguments are wrong:", res) 103 | } 104 | 105 | if set, ok := res["1.6"]; !ok { 106 | t.Error("Version is wrong:", res) 107 | } else if len(set) != 2 { 108 | t.Error("Env has wrong values:", res) 109 | } else { 110 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 111 | t.Error("Env has wrong values:", res) 112 | } 113 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 114 | t.Error("Env has wrong values:", res) 115 | } 116 | } 117 | if set, ok := res["1.7"]; !ok { 118 | t.Error("Version is wrong:", res) 119 | } else if len(set) != 1 { 120 | t.Error("Env has wrong values:", res) 121 | } else { 122 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 123 | t.Error("Env has wrong values:", res) 124 | } 125 | } 126 | 127 | } 128 | 129 | func TestGoArgumentSet(t *testing.T) { 130 | 131 | var ( 132 | travis *Travis 133 | res TestCaseSet 134 | err error 135 | ) 136 | 137 | travis, err = storeAndLoadTravis(&Travis{ 138 | Language: "go", 139 | }) 140 | if err != nil { 141 | t.Fatal(err.Error()) 142 | } 143 | res, err = travis.ArgumentSet(ioutil.Discard) 144 | if err != nil { 145 | t.Error(err.Error()) 146 | } 147 | t.Log("Arguments:", res) 148 | if len(res) != 1 { 149 | t.Error("Generated arguments are wrong:", res) 150 | } 151 | if set, ok := res["any"]; !ok { 152 | t.Error("Version is wrong:", res) 153 | } else if len(set) != 1 || len(set[0]) != 0 { 154 | t.Error("Env has wrong values:", set) 155 | } 156 | 157 | travis, err = NewTravis([]byte(`language: "go" 158 | env: 159 | - FOO=bar 160 | `)) 161 | if err != nil { 162 | t.Fatal(err.Error()) 163 | } 164 | res, err = travis.ArgumentSet(ioutil.Discard) 165 | if err != nil { 166 | t.Error(err.Error()) 167 | } 168 | t.Log("Arguments:", res) 169 | if len(res) != 1 { 170 | t.Error("Generated arguments are wrong:", res) 171 | } 172 | if set, ok := res["any"]; !ok { 173 | t.Error("Version is wrong:", res) 174 | } else if len(set) != 1 || len(set[0]) != 1 || set[0]["FOO"] != "bar" { 175 | t.Error("Env has wrong values:", res) 176 | } 177 | 178 | travis, err = NewTravis([]byte(`language: "go" 179 | env: 180 | - FOO=foo BAR=bar 181 | - FOO=bar BAR=foo 182 | `)) 183 | if err != nil { 184 | t.Fatal(err.Error()) 185 | } 186 | res, err = travis.ArgumentSet(ioutil.Discard) 187 | if err != nil { 188 | t.Error(err.Error()) 189 | } 190 | t.Log("Arguments:", res) 191 | if len(res) != 1 { 192 | t.Error("Generated arguments are wrong:", res) 193 | } 194 | if set, ok := res["any"]; !ok { 195 | t.Error("Version is wrong:", res) 196 | } else if len(set) != 2 { 197 | t.Error("Env has wrong values:", res) 198 | } else { 199 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 200 | t.Error("Env has wrong values:", res) 201 | } 202 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 203 | t.Error("Env has wrong values:", res) 204 | } 205 | } 206 | 207 | travis, err = storeAndLoadTravis(&Travis{ 208 | Language: "go", 209 | Go: []string{"1.6", "1.7"}, 210 | }) 211 | if err != nil { 212 | t.Fatal(err.Error()) 213 | } 214 | res, err = travis.ArgumentSet(ioutil.Discard) 215 | if err != nil { 216 | t.Error(err.Error()) 217 | } 218 | t.Log("Arguments:", res) 219 | if len(res) != 2 { 220 | t.Error("Generated arguments are wrong:", res) 221 | } 222 | if set, ok := res["1.6"]; !ok { 223 | t.Error("Version is wrong:", res) 224 | } else if len(set) != 1 || len(set[0]) != 0 { 225 | t.Error("Env has wrong values:", res) 226 | } 227 | if set, ok := res["1.7"]; !ok { 228 | t.Error("Version is wrong:", res) 229 | } else if len(set) != 1 || len(set[0]) != 0 { 230 | t.Error("Env has wrong values:", res) 231 | } 232 | 233 | travis, err = NewTravis([]byte(`language: "go" 234 | go: 235 | - 1.6 236 | - 1.7 237 | env: 238 | - FOO=foo BAR=bar 239 | - FOO=bar BAR=foo 240 | `)) 241 | if err != nil { 242 | t.Fatal(err.Error()) 243 | } 244 | res, err = travis.ArgumentSet(ioutil.Discard) 245 | if err != nil { 246 | t.Error(err.Error()) 247 | } 248 | t.Log("Arguments:", res) 249 | if len(res) != 2 { 250 | t.Error("Generated arguments are wrong:", res) 251 | } 252 | if set, ok := res["1.6"]; !ok { 253 | t.Error("Version is wrong:", res) 254 | } else if len(set) != 2 { 255 | t.Error("Env has wrong values:", res) 256 | } else { 257 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 258 | t.Error("Env has wrong values:", res) 259 | } 260 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 261 | t.Error("Env has wrong values:", res) 262 | } 263 | } 264 | if set, ok := res["1.7"]; !ok { 265 | t.Error("Version is wrong:", res) 266 | } else if len(set) != 2 { 267 | t.Error("Env has wrong values:", res) 268 | } else { 269 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 270 | t.Error("Env has wrong values:", res) 271 | } 272 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 273 | t.Error("Env has wrong values:", res) 274 | } 275 | } 276 | 277 | } 278 | 279 | func TestGoArgumentSetWithFullDescriptions(t *testing.T) { 280 | 281 | travis, err := storeAndLoadTravis(&Travis{ 282 | Language: "go", 283 | Go: []string{"1.6", "1.7"}, 284 | Env: Env{ 285 | Global: []string{"GLOBAL=global"}, 286 | Matrix: []string{"FOO=foo BAR=bar", "FOO=bar BAR=foo"}, 287 | }, 288 | }) 289 | if err != nil { 290 | t.Fatal(err.Error()) 291 | } 292 | res, err := travis.ArgumentSet(ioutil.Discard) 293 | if err != nil { 294 | t.Error(err.Error()) 295 | } 296 | t.Log("Arguments:", res) 297 | if len(res) != 2 { 298 | t.Error("Generated arguments are wrong:", res) 299 | } 300 | if set, ok := res["1.6"]; !ok { 301 | t.Error("Version is wrong:", res) 302 | } else if len(set) != 2 { 303 | t.Error("Env has wrong values:", res) 304 | } else { 305 | if len(set[0]) != 3 || set[0]["GLOBAL"] != "global" || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 306 | t.Error("Env has wrong values:", res) 307 | } 308 | if len(set[1]) != 3 || set[1]["GLOBAL"] != "global" || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 309 | t.Error("Env has wrong values:", res) 310 | } 311 | } 312 | if set, ok := res["1.7"]; !ok { 313 | t.Error("Version is wrong:", res) 314 | } else if len(set) != 2 { 315 | t.Error("Env has wrong values:", res) 316 | } else { 317 | if len(set[0]) != 3 || set[0]["GLOBAL"] != "global" || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 318 | t.Error("Env has wrong values:", res) 319 | } 320 | if len(set[1]) != 3 || set[1]["GLOBAL"] != "global" || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 321 | t.Error("Env has wrong values:", res) 322 | } 323 | } 324 | 325 | } 326 | -------------------------------------------------------------------------------- /command/travis_python.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis_python.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "io" 16 | "strings" 17 | 18 | "github.com/ttacon/chalk" 19 | ) 20 | 21 | // const ( 22 | // // PythonNightlyVersion defines a python version used as the nightly version. 23 | // PythonNightlyVersion = "3.7-dev" 24 | // ) 25 | 26 | var ( 27 | // ErrUnknownPythonVersion is returned when the given python version is not 28 | // supported. 29 | ErrUnknownPythonVersion = fmt.Errorf("Given Python version is not supported") 30 | ) 31 | 32 | // argumentSetPython returns a set of arguments to run entrypoint based on a build 33 | // matrix for python projects. 34 | func (t *Travis) argumentSetPython(logger io.Writer) (res TestCaseSet, err error) { 35 | 36 | res = make(TestCaseSet) 37 | global := parseEnv(strings.Join(t.Env.Global, " ")) 38 | 39 | // Parse Matrix.Include. 40 | for _, v := range t.Matrix.Include { 41 | version, c, err := parseMatrixPython(v) 42 | if err == ErrUnknownPythonVersion { 43 | fmt.Fprintf(logger, chalk.Yellow.Color("Python version %v is not supported\n"), version) 44 | continue 45 | } else if err != nil { 46 | return nil, err 47 | } 48 | res[version] = append(res[version], global.Copy().Merge(c)) 49 | } 50 | 51 | if len(t.Python) == 0 && len(res) == 0 { 52 | t.Python = []string{"2.7"} 53 | } 54 | for _, version := range t.Python { 55 | 56 | if len(t.Env.Matrix) == 0 { 57 | // If there is no matrix configuration, use only global configuration. 58 | res[version] = append(res[version], global) 59 | } else { 60 | // Look up each matrix case, and merge sprcific configuration to the 61 | // global one. 62 | for _, m := range t.Env.Matrix { 63 | c := parseEnv(m) 64 | res[version] = append(res[version], global.Copy().Merge(c)) 65 | } 66 | } 67 | 68 | } 69 | 70 | // Parse Matrix.Exclude. 71 | for _, v := range t.Matrix.Exclude { 72 | version, exclude, err := parseMatrixPython(v) 73 | if err == ErrUnknownPythonVersion { 74 | fmt.Fprintf(logger, chalk.Yellow.Color("Python version %v is not supported\n"), version) 75 | continue 76 | } else if err != nil { 77 | return nil, err 78 | } 79 | if set, ok := res[version]; ok { 80 | var new []TestCase 81 | for _, c := range set { 82 | if !c.Match(exclude) { 83 | new = append(new, c) 84 | } 85 | } 86 | res[version] = new 87 | } 88 | } 89 | 90 | return 91 | 92 | } 93 | 94 | // parseMatrixPython parses a given item v in an include/exclude list. 95 | // v must be castable to map[interface{}]interface{}. 96 | func parseMatrixPython(v interface{}) (version string, c TestCase, err error) { 97 | 98 | m, ok := v.(map[interface{}]interface{}) 99 | if !ok { 100 | err = fmt.Errorf("Given item is broken.") 101 | return 102 | } 103 | 104 | if _, exist := m["python"]; !exist { 105 | version = "empty" 106 | err = ErrUnknownPythonVersion 107 | return 108 | } 109 | version = fmt.Sprint(m["python"]) 110 | if version == "nightly" { 111 | // version = PythonNightlyVersion 112 | err = ErrUnknownPythonVersion 113 | return 114 | } 115 | variables, ok := m["env"].(string) 116 | if !ok { 117 | err = fmt.Errorf("Env of the given item is broken.") 118 | return 119 | } 120 | c = parseEnv(variables) 121 | 122 | return 123 | 124 | } 125 | -------------------------------------------------------------------------------- /command/travis_python_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis_python_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | package command 11 | 12 | import ( 13 | "io/ioutil" 14 | "testing" 15 | ) 16 | 17 | // PythonCase defines a case of matrix evaluation for python projects. 18 | type PythonCase struct { 19 | Python string `yaml:"python"` 20 | Env string `yaml:"env"` 21 | } 22 | 23 | func TestPythonMatrixInclude(t *testing.T) { 24 | 25 | var err error 26 | travis, err := storeAndLoadTravis(&Travis{ 27 | Language: "python", 28 | Matrix: Matrix{ 29 | Include: []interface{}{ 30 | PythonCase{ 31 | Python: "2.7", 32 | Env: "FOO=bar", 33 | }, PythonCase{ 34 | Python: "3.5", 35 | Env: "FOO=fuga", 36 | }, 37 | }, 38 | }, 39 | }) 40 | if err != nil { 41 | t.Error(err.Error()) 42 | } 43 | if len(travis.Matrix.Include) != 2 { 44 | t.Error("Size of items in matrix.include is wrong:", travis.Matrix.Include) 45 | } 46 | 47 | res, err := travis.ArgumentSet(ioutil.Discard) 48 | if err != nil { 49 | t.Error(err.Error()) 50 | } 51 | 52 | t.Log("Arguments:", res) 53 | if len(res) != 2 { 54 | t.Fatal("Generated arguments are wrong:", res) 55 | } 56 | if set, ok := res["2.7"]; !ok { 57 | t.Error("Version is wrong:", res) 58 | } else if len(set) != 1 || len(set[0]) != 1 || set[0]["FOO"] != "bar" { 59 | t.Error("Env has wrong values:", res) 60 | } 61 | if set, ok := res["3.5"]; !ok { 62 | t.Error("Version is wrong:", res) 63 | } else if len(set) != 1 || len(set[0]) != 1 || set[0]["FOO"] != "fuga" { 64 | t.Error("Env has wrong values:", res) 65 | } 66 | 67 | } 68 | 69 | func TestPythonMatrixExclude(t *testing.T) { 70 | 71 | var err error 72 | travis, err := NewTravis([]byte(`language: "python" 73 | python: 74 | - 2.7 75 | - 3.5 76 | env: 77 | - FOO=foo BAR=bar 78 | - FOO=bar BAR=foo 79 | matrix: 80 | exclude: 81 | - python: 3.5 82 | env: FOO=bar BAR=foo 83 | `)) 84 | if err != nil { 85 | t.Fatal(err.Error()) 86 | } 87 | if len(travis.Matrix.Exclude) != 1 { 88 | t.Error("Size of items in matrix.exclude is wrong:", travis.Matrix.Exclude) 89 | } 90 | 91 | res, err := travis.ArgumentSet(ioutil.Discard) 92 | if err != nil { 93 | t.Error(err.Error()) 94 | } 95 | 96 | t.Log("Arguments:", res) 97 | if len(res) != 2 { 98 | t.Fatal("Generated arguments are wrong:", res) 99 | } 100 | 101 | if set, ok := res["2.7"]; !ok { 102 | t.Error("Version is wrong:", res) 103 | } else if len(set) != 2 { 104 | t.Error("Env has wrong values:", res) 105 | } else { 106 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 107 | t.Error("Env has wrong values:", res) 108 | } 109 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 110 | t.Error("Env has wrong values:", res) 111 | } 112 | } 113 | if set, ok := res["3.5"]; !ok { 114 | t.Error("Version is wrong:", res) 115 | } else if len(set) != 1 { 116 | t.Error("Env has wrong values:", res) 117 | } else { 118 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 119 | t.Error("Env has wrong values:", res) 120 | } 121 | } 122 | 123 | } 124 | 125 | // TestPythonArgumentSet tests ArgumentSet method returns correct argument sets. 126 | func TestPythonArgumentSet(t *testing.T) { 127 | 128 | var ( 129 | travis *Travis 130 | res TestCaseSet 131 | err error 132 | ) 133 | 134 | travis, err = storeAndLoadTravis(&Travis{ 135 | Language: "python", 136 | }) 137 | if err != nil { 138 | t.Fatal(err.Error()) 139 | } 140 | res, err = travis.ArgumentSet(ioutil.Discard) 141 | if err != nil { 142 | t.Error(err.Error()) 143 | } 144 | t.Log("Arguments:", res) 145 | if len(res) != 1 { 146 | t.Error("Generated arguments are wrong:", res) 147 | } 148 | if set, ok := res["2.7"]; !ok { 149 | t.Error("Version is wrong:", res) 150 | } else if len(set) != 1 || len(set[0]) != 0 { 151 | t.Error("Env has wrong values:", res) 152 | } 153 | 154 | travis, err = NewTravis([]byte(`language: "python" 155 | env: 156 | - FOO=foo BAR=bar 157 | - FOO=bar BAR=foo 158 | `)) 159 | 160 | if err != nil { 161 | t.Fatal(err.Error()) 162 | } 163 | res, err = travis.ArgumentSet(ioutil.Discard) 164 | if err != nil { 165 | t.Error(err.Error()) 166 | } 167 | t.Log("Arguments:", res) 168 | if len(res) != 1 { 169 | t.Error("Generated arguments are wrong:", res) 170 | } 171 | if set, ok := res["2.7"]; !ok { 172 | t.Error("Version is wrong:", res) 173 | } else if len(set) != 2 { 174 | t.Error("Env has wrong values:", res) 175 | } else { 176 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 177 | t.Error("Env has wrong values:", res) 178 | } 179 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 180 | t.Error("Env has wrong values:", res) 181 | } 182 | } 183 | 184 | travis, err = storeAndLoadTravis(&Travis{ 185 | Language: "python", 186 | Python: []string{"2.7", "3.5"}, 187 | }) 188 | if err != nil { 189 | t.Fatal(err.Error()) 190 | } 191 | res, err = travis.ArgumentSet(ioutil.Discard) 192 | if err != nil { 193 | t.Error(err.Error()) 194 | } 195 | t.Log("Arguments:", res) 196 | if len(res) != 2 { 197 | t.Error("Generated arguments are wrong:", res) 198 | } 199 | if set, ok := res["2.7"]; !ok { 200 | t.Error("Version is wrong:", res) 201 | } else if len(set) != 1 || len(set[0]) != 0 { 202 | t.Error("Env has wrong values:", res) 203 | } 204 | if set, ok := res["3.5"]; !ok { 205 | t.Error("Version is wrong:", res) 206 | } else if len(set) != 1 || len(set[0]) != 0 { 207 | t.Error("Env has wrong values:", res) 208 | } 209 | 210 | travis, err = NewTravis([]byte(`language: "python" 211 | python: 212 | - 2.7 213 | - 3.5 214 | env: 215 | - FOO=foo BAR=bar 216 | - FOO=bar BAR=foo 217 | `)) 218 | if err != nil { 219 | t.Fatal(err.Error()) 220 | } 221 | res, err = travis.ArgumentSet(ioutil.Discard) 222 | if err != nil { 223 | t.Error(err.Error()) 224 | } 225 | t.Log("Arguments:", res) 226 | if len(res) != 2 { 227 | t.Error("Generated arguments are wrong:", res) 228 | } 229 | if set, ok := res["2.7"]; !ok { 230 | t.Error("Version is wrong:", res) 231 | } else if len(set) != 2 { 232 | t.Error("Env has wrong values:", res) 233 | } else { 234 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 235 | t.Error("Env has wrong values:", res) 236 | } 237 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 238 | t.Error("Env has wrong values:", res) 239 | } 240 | } 241 | if set, ok := res["3.5"]; !ok { 242 | t.Error("Version is wrong:", res) 243 | } else if len(set) != 2 { 244 | t.Error("Env has wrong values:", res) 245 | } else { 246 | if len(set[0]) != 2 || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 247 | t.Error("Env has wrong values:", res) 248 | } 249 | if len(set[1]) != 2 || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 250 | t.Error("Env has wrong values:", res) 251 | } 252 | } 253 | 254 | } 255 | 256 | func TestPythonArgumentSetWithFullDescriptions(t *testing.T) { 257 | 258 | travis, err := storeAndLoadTravis(&Travis{ 259 | Language: "python", 260 | Python: []string{"2.7", "3.5"}, 261 | Env: Env{ 262 | Global: []string{"GLOBAL=global"}, 263 | Matrix: []string{"FOO=foo BAR=bar", "FOO=bar BAR=foo"}, 264 | }, 265 | }) 266 | if err != nil { 267 | t.Fatal(err.Error()) 268 | } 269 | res, err := travis.ArgumentSet(ioutil.Discard) 270 | if err != nil { 271 | t.Error(err.Error()) 272 | } 273 | t.Log("Arguments:", res) 274 | if len(res) != 2 { 275 | t.Error("Generated arguments are wrong:", res) 276 | } 277 | if set, ok := res["2.7"]; !ok { 278 | t.Error("Version is wrong:", res) 279 | } else if len(set) != 2 { 280 | t.Error("Env has wrong values:", res) 281 | } else { 282 | if len(set[0]) != 3 || set[0]["GLOBAL"] != "global" || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 283 | t.Error("Env has wrong values:", res) 284 | } 285 | if len(set[1]) != 3 || set[1]["GLOBAL"] != "global" || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 286 | t.Error("Env has wrong values:", res) 287 | } 288 | } 289 | if set, ok := res["3.5"]; !ok { 290 | t.Error("Version is wrong:", res) 291 | } else if len(set) != 2 { 292 | t.Error("Env has wrong values:", res) 293 | } else { 294 | if len(set[0]) != 3 || set[0]["GLOBAL"] != "global" || set[0]["FOO"] != "foo" || set[0]["BAR"] != "bar" { 295 | t.Error("Env has wrong values:", res) 296 | } 297 | if len(set[1]) != 3 || set[1]["GLOBAL"] != "global" || set[1]["FOO"] != "bar" || set[1]["BAR"] != "foo" { 298 | t.Error("Env has wrong values:", res) 299 | } 300 | } 301 | 302 | } 303 | 304 | func TestPythonUnknownArgumentSet(t *testing.T) { 305 | 306 | var err error 307 | // The following configuration is copied from matplotlib. 308 | travis, err := NewTravis([]byte(`language: "python" 309 | matrix: 310 | include: 311 | - python: "nightly" 312 | env: PRE=--pre 313 | - os: osx 314 | osx_image: xcode7.3 315 | language: generic 316 | `)) 317 | if err != nil { 318 | t.Fatal(err.Error()) 319 | } 320 | if len(travis.Matrix.Include) != 2 { 321 | t.Error("Size of items in matrix.include is wrong:", travis.Matrix.Exclude) 322 | } 323 | 324 | res, err := travis.ArgumentSet(ioutil.Discard) 325 | if err != nil { 326 | t.Error(err.Error()) 327 | } 328 | 329 | // Python 2.7 is automatically added when no available runtimes are specified. 330 | if len(res) != 1 { 331 | t.Fatal("Generated arguments are wrong:", res) 332 | } 333 | 334 | } 335 | 336 | func TestPythonOverwriteEvnSet(t *testing.T) { 337 | 338 | var err error 339 | // The following configuration is copied from matplotlib. 340 | travis, err := NewTravis([]byte(`language: "python" 341 | env: 342 | global: 343 | - secure: some_encrypted_value 344 | - BUILD_DOCS=false 345 | matrix: 346 | include: 347 | - python: 2.7 348 | env: MOCK=mock NUMPY=numpy==1.7.1 PANDAS=pandas NOSE=nose 349 | - python: 3.5 350 | env: BUILD_DOCS=true 351 | `)) 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | 356 | res, err := travis.ArgumentSet(ioutil.Discard) 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | if len(res) != 2 { 361 | t.Error("The number of generated test cases is wrong", res) 362 | } 363 | 364 | if cases, exist := res["2.7"]; !exist { 365 | t.Error("Version 2.7 is not included in the generated test sets", res) 366 | } else if len(cases) != 1 { 367 | t.Error("Number of generated tast cases for 2.7 is wrong", res) 368 | } else { 369 | var mock, numpy, pandas, nose, docs bool 370 | for k, v := range cases[0] { 371 | switch { 372 | case k == "MOCK" && v == "mock": 373 | mock = true 374 | case k == "NUMPY" && v == "numpy==1.7.1": 375 | numpy = true 376 | case k == "PANDAS" && v == "pandas": 377 | pandas = true 378 | case k == "NOSE" && v == "nose": 379 | nose = true 380 | case k == "BUILD_DOCS" && v == "false": 381 | docs = true 382 | default: 383 | t.Error("A test case has wrong environment variables", v) 384 | } 385 | } 386 | if !mock || !numpy || !pandas || !nose || !docs { 387 | t.Errorf("Missing variables: MOCK=%v, NUMPY=%v, PANDAS=%v, NOSE=%v, BUILD_DOCS=%v", mock, numpy, pandas, nose, docs) 388 | } 389 | } 390 | 391 | if cases, exist := res["3.5"]; !exist { 392 | t.Error("Version 3.5 is not included in the generated test sets", res) 393 | } else if len(cases) != 1 { 394 | t.Error("Number of generated tast cases for 3.5 is wrong", res) 395 | } else { 396 | var docs bool 397 | for k, v := range cases[0] { 398 | switch { 399 | case k == "BUILD_DOCS" && v == "true": 400 | docs = true 401 | default: 402 | t.Error("A test case has wrong environment variables", v) 403 | } 404 | } 405 | if !docs { 406 | t.Errorf("Missing variables: BUILD_DOCS=%v", docs) 407 | } 408 | } 409 | 410 | } 411 | -------------------------------------------------------------------------------- /command/travis_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/travis_test.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import ( 14 | "io/ioutil" 15 | "os" 16 | "path" 17 | "testing" 18 | 19 | yaml "gopkg.in/yaml.v2" 20 | ) 21 | 22 | func TestTestCaseSlice(t *testing.T) { 23 | 24 | c := TestCase{ 25 | "FOO": "foo", 26 | "BAR": "bar", 27 | } 28 | 29 | var foo, bar bool 30 | for _, s := range c.Slice() { 31 | switch s { 32 | case "FOO=foo": 33 | foo = true 34 | case "BAR=bar": 35 | bar = true 36 | default: 37 | t.Error("The slice converted from a test case has a wrong string:", s) 38 | } 39 | } 40 | if !foo || !bar { 41 | t.Errorf("The slice converted from a test case has missing variables: FOO=%v, BAR=%v", foo, bar) 42 | } 43 | 44 | } 45 | 46 | func TestTestCaseCopy(t *testing.T) { 47 | 48 | a := TestCase{ 49 | "FOO": "foo", 50 | "BAR": "bar", 51 | } 52 | b := a.Copy() 53 | a["FOO"] = "piyo" 54 | 55 | if b["FOO"] != "foo" || b["BAR"] != "bar" { 56 | t.Error("Copied test case is not a hard copy", b) 57 | } 58 | 59 | } 60 | 61 | func TestTestCaseMerge(t *testing.T) { 62 | 63 | a := TestCase{ 64 | "FOO": "foo", 65 | "BAR": "bar", 66 | } 67 | b := TestCase{ 68 | "BAR": "piyo", 69 | "FUGA": "fuga", 70 | } 71 | a.Merge(b) 72 | 73 | var foo, bar, fuga bool 74 | for key, value := range a { 75 | switch { 76 | case key == "FOO" && value == "foo": 77 | foo = true 78 | case key == "BAR" && value == "piyo": 79 | bar = true 80 | case key == "FUGA" && value == "fuga": 81 | fuga = true 82 | default: 83 | t.Errorf("Merged test case has a wrong pair: %v=%v", key, value) 84 | } 85 | } 86 | if !foo || !bar || !fuga { 87 | t.Errorf("Merged test case has missing pairs: FOO=%v, BAR=%v, FUGA=%v", foo, bar, fuga) 88 | } 89 | 90 | } 91 | 92 | func TestTestCaseMatch(t *testing.T) { 93 | 94 | a := TestCase{ 95 | "FOO": "foo", 96 | "BAR": "bar", 97 | } 98 | if !a.Match(a.Copy()) { 99 | t.Error("Match returns false for a copied test case") 100 | } 101 | b := a.Copy() 102 | a["FOO"] = "piyo" 103 | if a.Match(b) { 104 | t.Error("Match returns true for a different test case") 105 | } 106 | 107 | } 108 | 109 | func storeAndLoadTravis(src *Travis) (res *Travis, err error) { 110 | temp := os.TempDir() 111 | target := path.Join(temp, "sample.yml") 112 | data, err := yaml.Marshal(src) 113 | if err != nil { 114 | return 115 | } 116 | if err = ioutil.WriteFile(target, data, 0644); err != nil { 117 | return 118 | } 119 | defer os.Remove(target) 120 | return NewTravisFromFile(target) 121 | } 122 | 123 | func TestParseBeforeInstallWithNoValues(t *testing.T) { 124 | travis, err := storeAndLoadTravis(&Travis{}) 125 | if err != nil { 126 | t.Fatal(err.Error()) 127 | } 128 | if len(travis.BeforeInstall) != 0 { 129 | t.Error("BeforeInstall is wrong:", travis.BeforeInstall) 130 | } 131 | } 132 | 133 | func TestParseBeforeInstallWithString(t *testing.T) { 134 | travis, err := NewTravis([]byte(`language: "" 135 | before_install: install 1`)) 136 | if err != nil { 137 | t.Fatal(err.Error()) 138 | } 139 | if len(travis.BeforeInstall) != 1 || travis.BeforeInstall[0] != "install 1" { 140 | t.Error("BeforeInstall is wrong:", travis.BeforeInstall) 141 | } 142 | } 143 | 144 | func TestParseBeforeInstallWithList(t *testing.T) { 145 | travis, err := NewTravis([]byte(`language: "" 146 | before_install: 147 | - install 1`)) 148 | if err != nil { 149 | t.Fatal(err.Error()) 150 | } 151 | if len(travis.BeforeInstall) != 1 || travis.BeforeInstall[0] != "install 1" { 152 | t.Error("BeforeInstall is wrong:", travis.BeforeInstall) 153 | } 154 | } 155 | 156 | func TestParseInstallWithNoValues(t *testing.T) { 157 | travis, err := storeAndLoadTravis(&Travis{}) 158 | if err != nil { 159 | t.Fatal(err.Error()) 160 | } 161 | if len(travis.Install) != 0 { 162 | t.Error("Install is wrong:", travis.Install) 163 | } 164 | } 165 | 166 | func TestParseInstallWithString(t *testing.T) { 167 | travis, err := NewTravis([]byte(`language: "" 168 | install: install 1`)) 169 | if err != nil { 170 | t.Fatal(err.Error()) 171 | } 172 | if len(travis.Install) != 1 || travis.Install[0] != "install 1" { 173 | t.Error("Install is wrong:", travis.Install) 174 | } 175 | } 176 | 177 | func TestParseInstallWithList(t *testing.T) { 178 | travis, err := NewTravis([]byte(`language: "" 179 | install: 180 | - install 1`)) 181 | 182 | if err != nil { 183 | t.Fatal(err.Error()) 184 | } 185 | if len(travis.Install) != 1 || travis.Install[0] != "install 1" { 186 | t.Error("Install is wrong:", travis.Install) 187 | } 188 | } 189 | 190 | func TestParseBeforeScriptWithNoValues(t *testing.T) { 191 | travis, err := storeAndLoadTravis(&Travis{}) 192 | if err != nil { 193 | t.Fatal(err.Error()) 194 | } 195 | if len(travis.BeforeScript) != 0 { 196 | t.Error("BeforeScript is wrong:", travis.BeforeScript) 197 | } 198 | } 199 | 200 | func TestParseBeforeScriptWithString(t *testing.T) { 201 | travis, err := NewTravis([]byte(`language: "" 202 | before_script: python setup.py test`)) 203 | if err != nil { 204 | t.Fatal(err.Error()) 205 | } 206 | if len(travis.BeforeScript) != 1 || travis.BeforeScript[0] != "python setup.py test" { 207 | t.Error("BeforeScript is wrong:", travis.BeforeScript) 208 | } 209 | } 210 | 211 | func TestParseBeforeScriptWithList(t *testing.T) { 212 | travis, err := NewTravis([]byte(`language: "" 213 | before_script: 214 | - python setup.py test`)) 215 | if err != nil { 216 | t.Fatal(err.Error()) 217 | } 218 | if len(travis.BeforeScript) != 1 || travis.BeforeScript[0] != "python setup.py test" { 219 | t.Error("BeforeScript is wrong:", travis.BeforeScript) 220 | } 221 | } 222 | 223 | func TestParseScriptWithNoValues(t *testing.T) { 224 | travis, err := storeAndLoadTravis(&Travis{}) 225 | if err != nil { 226 | t.Fatal(err.Error()) 227 | } 228 | if len(travis.Script) != 0 { 229 | t.Error("Script is wrong:", travis.Script) 230 | } 231 | } 232 | 233 | func TestParseScriptWithString(t *testing.T) { 234 | travis, err := NewTravis([]byte(`language: "" 235 | script: python setup.py test`)) 236 | if err != nil { 237 | t.Fatal(err.Error()) 238 | } 239 | if len(travis.Script) != 1 || travis.Script[0] != "python setup.py test" { 240 | t.Error("Script is wrong:", travis.Script) 241 | } 242 | } 243 | 244 | func TestParseScriptWithList(t *testing.T) { 245 | travis, err := NewTravis([]byte(`language: "" 246 | script: 247 | - python setup.py test`)) 248 | if err != nil { 249 | t.Fatal(err.Error()) 250 | } 251 | if len(travis.Script) != 1 || travis.Script[0] != "python setup.py test" { 252 | t.Error("Script is wrong:", travis.Script) 253 | } 254 | } 255 | 256 | func TestParseEnvWithNoValues(t *testing.T) { 257 | 258 | travis, err := storeAndLoadTravis(&Travis{}) 259 | if err != nil { 260 | t.Fatal(err.Error()) 261 | } 262 | if len(travis.Env.Global) != 0 { 263 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 264 | } 265 | if len(travis.Env.Matrix) != 0 { 266 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 267 | } 268 | 269 | } 270 | 271 | func TestParseEnvWithGlobalsList(t *testing.T) { 272 | 273 | globals := []string{ 274 | "DB=postgres", 275 | "SH=bash", 276 | "PACKAGE_VERSION=\"1.0.*\"", 277 | } 278 | travis, err := NewTravis([]byte(`language: "" 279 | env: 280 | - DB=postgres 281 | - SH=bash 282 | - PACKAGE_VERSION="1.0.*" 283 | `)) 284 | if err != nil { 285 | t.Fatal(err.Error()) 286 | } 287 | 288 | if len(travis.Env.Global) != 3 { 289 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 290 | } 291 | if len(travis.Env.Matrix) != 0 { 292 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 293 | } 294 | for i, v := range globals { 295 | if travis.Env.Global[i] != v { 296 | t.Error("A global variable is not match:", travis.Env.Global) 297 | } 298 | } 299 | 300 | } 301 | 302 | func TestParseEnvWithMultipleVariablesList(t *testing.T) { 303 | 304 | matrix := []string{ 305 | "FOO=foo BAR=bar", 306 | "FOO=bar BAR=foo", 307 | } 308 | travis, err := NewTravis([]byte(`language: "" 309 | env: 310 | - FOO=foo BAR=bar 311 | - FOO=bar BAR=foo 312 | `)) 313 | 314 | if err != nil { 315 | t.Fatal(err.Error()) 316 | } 317 | 318 | if len(travis.Env.Global) != 0 { 319 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 320 | } 321 | if len(travis.Env.Matrix) != 2 { 322 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 323 | } 324 | for i, v := range matrix { 325 | if travis.Env.Matrix[i] != v { 326 | t.Error("Matrix variables are not match:", travis.Env.Matrix) 327 | } 328 | } 329 | 330 | } 331 | 332 | func TestParseEnvWithSpecificGlobals(t *testing.T) { 333 | 334 | globals := []string{ 335 | "DB=postgres", 336 | "SH=bash", 337 | "PACKAGE_VERSION=\"1.0.*\"", 338 | } 339 | travis, err := NewTravis([]byte(`language: "go" 340 | env: 341 | global: 342 | - DB=postgres 343 | - SH=bash 344 | - PACKAGE_VERSION="1.0.*" 345 | `)) 346 | if err != nil { 347 | t.Fatal(err.Error()) 348 | } 349 | 350 | if len(travis.Env.Global) != 3 { 351 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 352 | } 353 | if len(travis.Env.Matrix) != 0 { 354 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 355 | } 356 | for i, v := range globals { 357 | if travis.Env.Global[i] != v { 358 | t.Error("A global variable is not match:", travis.Env.Global) 359 | } 360 | } 361 | 362 | } 363 | 364 | func TestParseEnvWithSpecificMatrixVariables(t *testing.T) { 365 | 366 | matrix := []string{ 367 | "FOO=foo BAR=bar", 368 | "FOO=bar BAR=foo", 369 | } 370 | travis, err := NewTravis([]byte(`language: go 371 | env: 372 | matrix: 373 | - FOO=foo BAR=bar 374 | - FOO=bar BAR=foo 375 | `)) 376 | if err != nil { 377 | t.Fatal(err.Error()) 378 | } 379 | 380 | if len(travis.Env.Global) != 0 { 381 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 382 | } 383 | if len(travis.Env.Matrix) != 2 { 384 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 385 | } 386 | for i, v := range matrix { 387 | if travis.Env.Matrix[i] != v { 388 | t.Error("Matrix variables are not match:", travis.Env.Matrix) 389 | } 390 | } 391 | 392 | } 393 | 394 | func TestParseEnv(t *testing.T) { 395 | 396 | globals := []string{ 397 | "DB=postgres", 398 | "SH=bash", 399 | "PACKAGE_VERSION=\"1.0.*\"", 400 | } 401 | matrix := []string{ 402 | "FOO=foo BAR=bar", 403 | "FOO=bar BAR=foo", 404 | } 405 | travis, err := NewTravis([]byte(`language: go 406 | env: 407 | global: 408 | - DB=postgres 409 | - SH=bash 410 | - PACKAGE_VERSION="1.0.*" 411 | matrix: 412 | - FOO=foo BAR=bar 413 | - FOO=bar BAR=foo 414 | `)) 415 | if err != nil { 416 | t.Fatal(err.Error()) 417 | } 418 | 419 | if len(travis.Env.Global) != 3 { 420 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 421 | } 422 | if len(travis.Env.Matrix) != 2 { 423 | t.Fatal("The number of matrix variables is wrong:", travis.Env.Matrix) 424 | } 425 | for i, v := range globals { 426 | if travis.Env.Global[i] != v { 427 | t.Error("A global variable is not match:", travis.Env.Global) 428 | } 429 | } 430 | for i, v := range matrix { 431 | if travis.Env.Matrix[i] != v { 432 | t.Error("Matrix variables are not match:", travis.Env.Matrix) 433 | } 434 | } 435 | 436 | } 437 | 438 | func TestParseSecretEnv(t *testing.T) { 439 | 440 | globals := []string{ 441 | "DB=postgres", 442 | "SH=bash", 443 | "PACKAGE_VERSION=\"1.0.*\"", 444 | } 445 | travis, err := NewTravis([]byte(`language: go 446 | env: 447 | global: 448 | - DB=postgres 449 | - SH=bash 450 | - PACKAGE_VERSION="1.0.*" 451 | - secret: xxxxxxxxxxxxxxx 452 | `)) 453 | if err != nil { 454 | t.Fatal(err.Error()) 455 | } 456 | 457 | if len(travis.Env.Global) != 3 { 458 | t.Fatal("The number of global variables is wrong:", travis.Env.Global) 459 | } 460 | for i, v := range globals { 461 | if travis.Env.Global[i] != v { 462 | t.Error("A global variable is not match:", travis.Env.Global) 463 | } 464 | } 465 | 466 | } 467 | 468 | func TestParseEnvStrings(t *testing.T) { 469 | 470 | var res TestCase 471 | res = parseEnv("FOO=bar") 472 | if len(res) != 1 || res["FOO"] != "bar" { 473 | t.Error("parseEnv returns wrong envs:", res) 474 | } 475 | 476 | res = parseEnv("FOO=bar BAR=fuga") 477 | if len(res) != 2 || res["FOO"] != "bar" || res["BAR"] != "fuga" { 478 | t.Error("parseEnv returns wrong envs:", res) 479 | } 480 | 481 | res = parseEnv(`FOO="bar fuga"`) 482 | if len(res) != 1 || res["FOO"] != "bar fuga" { 483 | t.Error("parseEnv returns wrong envs:", res) 484 | } 485 | 486 | res = parseEnv(`FOO="bar fuga" BAR="foo fuga"`) 487 | if len(res) != 2 || res["FOO"] != "bar fuga" || res["BAR"] != "foo fuga" { 488 | t.Error("parseEnv returns wrong envs:", res) 489 | } 490 | 491 | } 492 | -------------------------------------------------------------------------------- /command/yaml.go: -------------------------------------------------------------------------------- 1 | // 2 | // command/yaml.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package command 12 | 13 | import "fmt" 14 | 15 | // ListOrString defines an ambiguous type for YAML document, which can take 16 | // a list of strings by default but also a single string literal. 17 | type ListOrString []string 18 | 19 | // UnmarshalYAML defines a way to unmarshal variables of ListOrString. 20 | func (e *ListOrString) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { 21 | 22 | var aux interface{} 23 | if err = unmarshal(&aux); err != nil { 24 | return 25 | } 26 | 27 | switch raw := aux.(type) { 28 | case string: 29 | *e = []string{raw} 30 | 31 | case []interface{}: 32 | list := make([]string, len(raw)) 33 | for i, r := range raw { 34 | v, ok := r.(string) 35 | if !ok { 36 | return fmt.Errorf("An item in evn cannot be converted to a string: %v", aux) 37 | } 38 | list[i] = v 39 | } 40 | *e = list 41 | 42 | } 43 | return 44 | 45 | } 46 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | // 2 | // commands.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | "os" 16 | "runtime" 17 | 18 | "github.com/urfave/cli" 19 | ) 20 | 21 | // max returns the bigger value of the given two integers. 22 | func max(a, b int) int { 23 | if a > b { 24 | return a 25 | } 26 | return b 27 | } 28 | 29 | // GlobalFlags defines global flags. 30 | var GlobalFlags = []cli.Flag{ 31 | cli.StringFlag{ 32 | Name: "name, n", 33 | Usage: "base `NAME` of containers running tests. " + 34 | "If not given, containers will be deleted.", 35 | }, 36 | cli.StringFlag{ 37 | Name: "select, s", 38 | Usage: "select specific runtime `VERSION` where tests running on.", 39 | }, 40 | cli.StringFlag{ 41 | Name: "tag, t", 42 | Usage: "specify a `TAG` name of the docker image to be build.", 43 | }, 44 | cli.IntFlag{ 45 | Name: "max-processors, p", 46 | Usage: "max processors used to run tests.", 47 | Value: max(runtime.NumCPU()-2, 1), 48 | }, 49 | cli.BoolFlag{ 50 | Name: "log, l", 51 | Usage: "store logging information to files.", 52 | }, 53 | cli.StringFlag{ 54 | Name: "base, b", 55 | Usage: "use image `TAG` as the base image.", 56 | Value: "ubuntu:trusty", 57 | }, 58 | cli.StringFlag{ 59 | Name: "apt-proxy", 60 | Usage: "`URL` for a proxy server of apt repository.", 61 | EnvVar: "APT_PROXY", 62 | }, 63 | cli.StringFlag{ 64 | Name: "pypi-proxy", 65 | Usage: "`URL` for a proxy server of pypi repository.", 66 | EnvVar: "PYPI_PROXY", 67 | }, 68 | cli.StringFlag{ 69 | Name: "http-proxy", 70 | Usage: "`URL` for a http proxy server.", 71 | EnvVar: "HTTP_PROXY", 72 | }, 73 | cli.StringFlag{ 74 | Name: "https-proxy", 75 | Usage: "`URL` for a https proxy server.", 76 | EnvVar: "HTTPS_PROXY", 77 | }, 78 | cli.StringFlag{ 79 | Name: "no-proxy", 80 | Usage: "Comma separated URL `LIST` for which proxies won't be used.", 81 | EnvVar: "NO_PROXY", 82 | }, 83 | cli.BoolFlag{ 84 | Name: "no-build-cache", 85 | Usage: "Do not use cache when building the image.", 86 | }, 87 | cli.BoolFlag{ 88 | Name: "no-color", 89 | Usage: "Omit to print color codes.", 90 | }, 91 | } 92 | 93 | // Commands defines sub-commands. 94 | var Commands = []cli.Command{} 95 | 96 | // CommandNotFound prints an error message when a given command is not supported. 97 | func CommandNotFound(c *cli.Context, command string) { 98 | fmt.Fprintf( 99 | os.Stderr, "%s: '%s' is not a %s command. See '%s --help'.", 100 | c.App.Name, command, c.App.Name, c.App.Name) 101 | os.Exit(2) 102 | } 103 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | baseurl = "https://jkawamoto.github.io/loci/" 2 | languageCode = "en-us" 3 | title = "Loci" 4 | theme = "github-project-landing-page" 5 | 6 | [params] 7 | SubTitle = "Testing remote CI scripts locally" 8 | Description = """\ 9 | Loci runs CI tests locally to make sure your commits will pass such tests\ 10 | before pushing to a remote repository.\ 11 | """ 12 | AuthorName = "Junpei Kawamoto" 13 | AuthorURL = "https://www.jkawamoto.info/" 14 | GithubURL = "https://github.com/jkawamoto/loci" 15 | ProfilePicture = "img/dna.png" 16 | 17 | GithubRepo = "loci" 18 | GithubOrg = "jkawamoto" 19 | 20 | GoogleAnalytics = "UA-82315630-1" 21 | GoogleAdSenseClient = "ca-pub-4734862314145555" 22 | GoogleAdSenseSlot = "4875187824" 23 | 24 | HighlightStyle = "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/agate.min.css" 25 | HighlightLanguages = [] 26 | AddThis = "ra-57f529147128b1fb" 27 | 28 | Favicon = "img/favicon.ico" 29 | Icon16 = "img/icon_16.png" 30 | Icon32 = "img/icon_32.png" 31 | Icon96 = "img/icon_96.png" 32 | 33 | first_color="#f8f8f8" 34 | first_border_color="#e7e7e7" 35 | first_text_color="#333" 36 | 37 | second_color="white" 38 | second_text_color="#333" 39 | 40 | header_color="#f8f8f8" 41 | header_text_color="rgb(51, 51, 51)" 42 | 43 | header_link_color="#777" 44 | header_link_hover_color="rgb(51, 51, 51)" 45 | -------------------------------------------------------------------------------- /docs/content/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Loci: Testing remote CI scripts locally" 3 | description: >- 4 | Loci runs CI tests locally to make sure your commits will pass such tests 5 | before pushing to a remote repository. 6 | date: 2016-12-14 7 | lastmod: 2017-02-01 8 | slug: readme 9 | --- 10 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./info/licenses/) 11 | [![Build Status](https://travis-ci.org/jkawamoto/loci.svg?branch=master)](https://travis-ci.org/jkawamoto/loci) 12 | [![wercker status](https://app.wercker.com/status/25b462a013ed96bf51254862938e7659/s/master "wercker status")](https://app.wercker.com/project/byKey/25b462a013ed96bf51254862938e7659) 13 | [![Release](https://img.shields.io/badge/release-0.5.3-brightgreen.svg)](https://github.com/jkawamoto/loci/releases/tag/v0.5.3) 14 | [![Japanese](https://img.shields.io/badge/qiita-%E6%97%A5%E6%9C%AC%E8%AA%9E-brightgreen.svg)](http://qiita.com/jkawamoto/items/a409dd9cd6e63034aa28) 15 | 16 | Loci runs CI tests locally to make sure your commits will pass such tests 17 | *before* pushing to a remote repository. 18 | 19 | Loci currently supports [Travis CI](https://travis-ci.org/)'s scripts 20 | for [Python](https://www.python.org/) and [Go](https://golang.org/) projects. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Loci also requires [Docker](https://www.docker.com/) to run tests in a sandbox. 33 | 34 | [![Docker logo](img/small_h-trans.png)](https://www.docker.com/) 35 | 36 | ### Demo 37 | 38 | 39 | ### Usage 40 | If your current directory has `.travis.yml`, run just `loci` like 41 | 42 | ```shell 43 | $ loci 44 | ``` 45 | 46 | If your `.travis.yml` specifies more than two runtime versions, Loci will run 47 | those tests palatally. If you want to run tests on a selected one runtime 48 | version, use `--select`/`-s` flag. For example, the following command runs tests 49 | on only Python 3.6: 50 | 51 | ```shell 52 | $ loci -s 3.6 53 | ``` 54 | 55 | Here is the help text of the `loci` command: 56 | 57 | ~~~shell 58 | loci [global options] [script file] 59 | 60 | If script file isn't given, .travis.yml will be used. 61 | 62 | GLOBAL OPTIONS: 63 | --name NAME, -n NAME base NAME of containers running tests. If not given, containers will be 64 | deleted. 65 | --select VERSION, -s VERSION select specific runtime VERSION where tests running on. 66 | --tag TAG, -t TAG specify a TAG name of the docker image to be build. 67 | --max-processors value, -p value max processors used to run tests. 68 | --log, -l store logging information to files. 69 | --base TAG, -b TAG use image TAG as the base image. (default: "ubuntu:latest") 70 | --apt-proxy URL URL for a proxy server of apt repository. If environment variable 71 | APT_PROXY exists, that value will be used by default. 72 | --pypi-proxy URL URL for a proxy server of PyPI repository. If environment variable 73 | PYPI_PROXY exists, that value will be used by default. 74 | --http-proxy URL URL for a http proxy server. If environment variable HTTP_PROXY exists, 75 | that value will be used by default. 76 | --https-proxy URL URL for a https proxy server. If environment variable HTTPS_PROXY exists, 77 | that value will be used by default. 78 | --no-proxy LIST Comma separated URL LIST for which proxies won't be used. If environment 79 | variable NO_PROXY exists, that value will be used by default. 80 | --no-build-cache Do not use cache when building the image. 81 | --no-color Omit to print color codes. 82 | --help, -h show help 83 | --version, -v print the version 84 | ~~~ 85 | 86 | Loci builds docker images every time to run tests in a sandbox. 87 | The default image name will be the repository name of the project with 88 | prefix `loci/`, you can specify another name with `--tag` or `-t` flag. 89 | 90 | Loci creates a container to run a set of tests, 91 | and then deletes it after the test set ends. 92 | If you want to keep the container, 93 | give a container name with `--name` or `-n` flag. 94 | 95 | 96 | ### Installation 97 | Loci works with [docker](https://www.docker.com/). 98 | If your environment doesn't have docker, install it first. 99 | The minimum required docker version is 1.12.0 (API version: 1.24). 100 | 101 | If you're a [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/) 102 | user, you can install Loci by the following commands: 103 | 104 | ```shell 105 | $ brew tap jkawamoto/loci 106 | $ brew install loci 107 | ``` 108 | 109 | To build the newest version of Loci, use `go get` command: 110 | 111 | ```shell 112 | $ go get github.com/jkawamoto/loci 113 | ``` 114 | 115 | Otherwise, compiled binaries are also available in 116 | [Github](https://github.com/jkawamoto/loci/releases). 117 | 118 | 119 | ### License 120 | This software is released under the MIT License, see [LICENSE](./info/licenses/). 121 | -------------------------------------------------------------------------------- /docs/content/info/LICENSES.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Licenses 3 | description: License information of loci. 4 | date: 2016-12-14 5 | lastmod: 2017-06-23 6 | slug: licenses 7 | --- 8 | This software is released under the MIT License. 9 | 10 | > The MIT License (MIT) 11 | > 12 | > Copyright (c) 2016-2017 Junpei Kawamoto 13 | > 14 | > Permission is hereby granted, free of charge, to any person obtaining a copy 15 | > of this software and associated documentation files (the "Software"), to deal 16 | > in the Software without restriction, including without limitation the rights 17 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | > copies of the Software, and to permit persons to whom the Software is 19 | > furnished to do so, subject to the following conditions: 20 | > 21 | > The above copyright notice and this permission notice shall be included in all 22 | > copies or substantial portions of the Software. 23 | > 24 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | > SOFTWARE. 31 | 32 | 33 | ## Notices for libraries 34 | This software uses the following open source libraries: 35 | 36 | ### [chalk](https://github.com/ttacon/chalk) 37 | 38 | > Copyright (c) 2014 Trey Tacon 39 | > 40 | > Licensed under the MIT License. 41 | > 42 | > Permission is hereby granted, free of charge, to any person obtaining a copy 43 | > of this software and associated documentation files (the "Software"), to deal 44 | > in the Software without restriction, including without limitation the rights 45 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | > copies of the Software, and to permit persons to whom the Software is 47 | > furnished to do so, subject to the following conditions: 48 | > 49 | > The above copyright notice and this permission notice shall be included in all 50 | > copies or substantial portions of the Software. 51 | > 52 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | > SOFTWARE. 59 | 60 | ### [cli](https://github.com/urfave/cli) 61 | 62 | > Copyright (c) 2016 Jeremy Saenz & Contributors 63 | > 64 | > Licensed under the MIT License. 65 | > 66 | > Permission is hereby granted, free of charge, to any person obtaining a copy 67 | > of this software and associated documentation files (the "Software"), to deal 68 | > in the Software without restriction, including without limitation the rights 69 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 70 | > copies of the Software, and to permit persons to whom the Software is 71 | > furnished to do so, subject to the following conditions: 72 | > 73 | > The above copyright notice and this permission notice shall be included in all 74 | > copies or substantial portions of the Software. 75 | > 76 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 77 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 78 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 79 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 80 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 81 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 82 | > SOFTWARE. 83 | 84 | ### [docker](https://github.com/docker/docker) 85 | 86 | > Copyright 2013-2016 Docker, Inc. 87 | > 88 | > Licensed under the Apache License, Version 2.0 (the "License"); 89 | > you may not use this file except in compliance with the License. 90 | > You may obtain a copy of the License at 91 | > 92 | > https://www.apache.org/licenses/LICENSE-2.0 93 | > 94 | > Unless required by applicable law or agreed to in writing, software 95 | > distributed under the License is distributed on an "AS IS" BASIS, 96 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | > See the License for the specific language governing permissions and 98 | > limitations under the License. 99 | 100 | ### [go-colorable](https://github.com/mattn/go-colorable) 101 | 102 | >The MIT License (MIT) 103 | > 104 | > Copyright (c) 2016 Yasuhiro Matsumoto 105 | > 106 | > Permission is hereby granted, free of charge, to any person obtaining a copy 107 | > of this software and associated documentation files (the "Software"), to deal 108 | > in the Software without restriction, including without limitation the rights 109 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 110 | > copies of the Software, and to permit persons to whom the Software is 111 | > furnished to do so, subject to the following conditions: 112 | > 113 | > The above copyright notice and this permission notice shall be included in all 114 | > copies or substantial portions of the Software. 115 | > 116 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 117 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 118 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 119 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 120 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 121 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 122 | > SOFTWARE. 123 | 124 | ### [go-cui](https://github.com/jroimartin/gocui) 125 | 126 | > Copyright (c) 2014 The gocui Authors. All rights reserved. 127 | > 128 | > Redistribution and use in source and binary forms, with or without 129 | > modification, are permitted provided that the following conditions are met: 130 | > * Redistributions of source code must retain the above copyright 131 | > notice, this list of conditions and the following disclaimer. 132 | > * Redistributions in binary form must reproduce the above copyright 133 | > notice, this list of conditions and the following disclaimer in the 134 | > documentation and/or other materials provided with the distribution. 135 | > * Neither the name of the gocui Authors nor the names of its contributors 136 | > may be used to endorse or promote products derived from this software 137 | > without specific prior written permission. 138 | > 139 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 140 | > ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 141 | > WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 142 | > DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 143 | > ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 144 | > (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 145 | > LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 146 | > ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 147 | > (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 148 | > SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 149 | 150 | ### [go-gitconfig](https://github.com/tcnksm/go-gitconfig) 151 | 152 | > Copyright (c) 2014 tcnksm 153 | > 154 | > Licensed under the MIT License. 155 | > 156 | > Permission is hereby granted, free of charge, to any person obtaining 157 | > a copy of this software and associated documentation files (the 158 | > "Software"), to deal in the Software without restriction, including 159 | > without limitation the rights to use, copy, modify, merge, publish, 160 | > distribute, sublicense, and/or sell copies of the Software, and to 161 | > permit persons to whom the Software is furnished to do so, subject to 162 | > the following conditions: 163 | > 164 | > The above copyright notice and this permission notice shall be 165 | > included in all copies or substantial portions of the Software. 166 | > 167 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 168 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 169 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 170 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 171 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 172 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 173 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 174 | 175 | ### [YAML support for the Go language](https://github.com/go-yaml/yaml) 176 | 177 | > Copyright 2011-2016 Canonical Ltd. 178 | > 179 | > Licensed under the Apache License, Version 2.0 (the "License"); 180 | > you may not use this file except in compliance with the License. 181 | > You may obtain a copy of the License at 182 | > 183 | > http://www.apache.org/licenses/LICENSE-2.0 184 | > 185 | > Unless required by applicable law or agreed to in writing, software 186 | > distributed under the License is distributed on an "AS IS" BASIS, 187 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 188 | > See the License for the specific language governing permissions and 189 | > limitations under the License. 190 | -------------------------------------------------------------------------------- /docs/layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | {{ "" | safeHTML }} 2 | 26 | -------------------------------------------------------------------------------- /docs/layouts/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 | {{ "" | safeHTML }} 3 |
4 | 5 |
6 | 7 |
8 |
10 |
11 |

{{ .Site.Title }}

12 |

{{ .Site.Params.SubTitle }}

13 | 14 | {{ if (isset .Site.Params "githuborg") }} 15 | {{ $org := .Site.Params.GithubOrg }} 16 | {{ $repo := .Site.Params.GithubRepo }} 17 | 20 | 23 | {{ end }} 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /docs/static/img/dna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/dna.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/gopher.png -------------------------------------------------------------------------------- /docs/static/img/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/icon_16.png -------------------------------------------------------------------------------- /docs/static/img/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/icon_32.png -------------------------------------------------------------------------------- /docs/static/img/icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/icon_96.png -------------------------------------------------------------------------------- /docs/static/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/image.png -------------------------------------------------------------------------------- /docs/static/img/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/python.png -------------------------------------------------------------------------------- /docs/static/img/small-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/small-logo.png -------------------------------------------------------------------------------- /docs/static/img/small_h-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/small_h-trans.png -------------------------------------------------------------------------------- /docs/static/img/travis-ci-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/travis-ci-small.png -------------------------------------------------------------------------------- /docs/static/img/travis-ci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkawamoto/loci/35cb7feeecee113d06acbb2c6923f7cc090565eb/docs/static/img/travis-ci.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 2 | // main.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package main 12 | 13 | import ( 14 | "os" 15 | 16 | "github.com/jkawamoto/loci/command" 17 | "github.com/urfave/cli" 18 | ) 19 | 20 | func main() { 21 | 22 | app := cli.NewApp() 23 | app.Name = Name 24 | app.Version = Version 25 | app.Author = "Junpei Kawamoto" 26 | app.Email = "junpei.kawamoto@acm.org" 27 | app.Usage = "Run a cloud CI script locally." 28 | app.UsageText = `loci [global options] [script file] 29 | 30 | If script file is omitted, .travis.yml will be used.` 31 | 32 | app.Flags = GlobalFlags 33 | app.Commands = Commands 34 | app.CommandNotFound = CommandNotFound 35 | app.Copyright = `Copyright (c) 2016-2017 Junpei Kawamoto 36 | 37 | This software is released under the MIT License. 38 | See https://jkawamoto.github.io/loci/info/licenses/ for more information.` 39 | app.Action = command.Run 40 | 41 | cli.AppHelpTemplate = `NAME: 42 | {{.Name}} - {{.Usage}} 43 | 44 | USAGE: 45 | {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} 46 | {{if .Version}}{{if not .HideVersion}} 47 | VERSION: 48 | {{.Version}} 49 | {{end}}{{end}}{{if len .Authors}} 50 | AUTHOR(S): 51 | {{range .Authors}}{{.}}{{end}} 52 | {{end}}{{if .VisibleCommands}} 53 | GLOBAL OPTIONS: 54 | {{range .VisibleFlags}}{{.}} 55 | {{end}}{{end}}{{if .Copyright}} 56 | COPYRIGHT: 57 | {{.Copyright}} 58 | {{end}} 59 | ` 60 | 61 | app.Run(os.Args) 62 | } 63 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // 2 | // version.go 3 | // 4 | // Copyright (c) 2016-2017 Junpei Kawamoto 5 | // 6 | // This software is released under the MIT License. 7 | // 8 | // http://opensource.org/licenses/mit-license.php 9 | // 10 | 11 | package main 12 | 13 | const ( 14 | // Name defines the name of this command. 15 | Name string = "Loci" 16 | // Version defines version number. 17 | Version string = "0.5.3" 18 | ) 19 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: jkawamoto/ghp-box 2 | build: 3 | steps: 4 | - script: 5 | name: Prepare submodules 6 | code: |- 7 | git submodule update --init 8 | - arjen/hugo-build: 9 | version: "0.18.1" 10 | basedir: docs 11 | - samueldebruyn/minify: 12 | base_dir: docs/public 13 | js: false 14 | deploy: 15 | steps: 16 | - jkawamoto/ghp-import: 17 | token: $GIT_TOKEN 18 | basedir: docs/public 19 | --------------------------------------------------------------------------------