@ -1,3 +1,8 @@ |
|||||
module github.com/jaedle/mirror-to-gitea |
module github.com/jaedle/mirror-to-gitea |
||||
|
|
||||
go 1.16 |
go 1.16 |
||||
|
|
||||
|
require ( |
||||
|
github.com/onsi/ginkgo v1.16.2 |
||||
|
github.com/onsi/gomega v1.12.0 |
||||
|
) |
@ -0,0 +1,93 @@ |
|||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= |
||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= |
||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= |
||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= |
||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= |
||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= |
||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= |
||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= |
||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= |
||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= |
||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= |
||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= |
||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= |
||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= |
||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= |
||||
|
github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134= |
||||
|
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= |
||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= |
||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= |
||||
|
github.com/onsi/gomega v1.12.0 h1:p4oGGk2M2UJc0wWN4lHFvIB71lxsh0T/UiKCCgFADY8= |
||||
|
github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= |
||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= |
||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= |
||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= |
||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= |
||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
|
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= |
||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= |
||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |
||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= |
||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= |
||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= |
||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= |
||||
|
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= |
||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= |
||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= |
||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
@ -0,0 +1,13 @@ |
|||||
|
package config_test |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
. "github.com/onsi/ginkgo" |
||||
|
. "github.com/onsi/gomega" |
||||
|
) |
||||
|
|
||||
|
func TestConfig(t *testing.T) { |
||||
|
RegisterFailHandler(Fail) |
||||
|
RunSpecs(t, "Config Suite") |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
root = true |
||||
|
|
||||
|
[*.go] |
||||
|
indent_style = tab |
||||
|
indent_size = 4 |
||||
|
insert_final_newline = true |
||||
|
|
||||
|
[*.{yml,yaml}] |
||||
|
indent_style = space |
||||
|
indent_size = 2 |
||||
|
insert_final_newline = true |
||||
|
trim_trailing_whitespace = true |
@ -0,0 +1 @@ |
|||||
|
go.sum linguist-generated |
@ -0,0 +1,6 @@ |
|||||
|
# Setup a Global .gitignore for OS and editor generated files: |
||||
|
# https://help.github.com/articles/ignoring-files |
||||
|
# git config --global core.excludesfile ~/.gitignore_global |
||||
|
|
||||
|
.vagrant |
||||
|
*.sublime-project |
@ -0,0 +1,36 @@ |
|||||
|
sudo: false |
||||
|
language: go |
||||
|
|
||||
|
go: |
||||
|
- "stable" |
||||
|
- "1.11.x" |
||||
|
- "1.10.x" |
||||
|
- "1.9.x" |
||||
|
|
||||
|
matrix: |
||||
|
include: |
||||
|
- go: "stable" |
||||
|
env: GOLINT=true |
||||
|
allow_failures: |
||||
|
- go: tip |
||||
|
fast_finish: true |
||||
|
|
||||
|
|
||||
|
before_install: |
||||
|
- if [ ! -z "${GOLINT}" ]; then go get -u golang.org/x/lint/golint; fi |
||||
|
|
||||
|
script: |
||||
|
- go test --race ./... |
||||
|
|
||||
|
after_script: |
||||
|
- test -z "$(gofmt -s -l -w . | tee /dev/stderr)" |
||||
|
- if [ ! -z "${GOLINT}" ]; then echo running golint; golint --set_exit_status ./...; else echo skipping golint; fi |
||||
|
- go vet ./... |
||||
|
|
||||
|
os: |
||||
|
- linux |
||||
|
- osx |
||||
|
- windows |
||||
|
|
||||
|
notifications: |
||||
|
email: false |
@ -0,0 +1,52 @@ |
|||||
|
# Names should be added to this file as |
||||
|
# Name or Organization <email address> |
||||
|
# The email address is not required for organizations. |
||||
|
|
||||
|
# You can update this list using the following command: |
||||
|
# |
||||
|
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}' |
||||
|
|
||||
|
# Please keep the list sorted. |
||||
|
|
||||
|
Aaron L <aaron@bettercoder.net> |
||||
|
Adrien Bustany <adrien@bustany.org> |
||||
|
Amit Krishnan <amit.krishnan@oracle.com> |
||||
|
Anmol Sethi <me@anmol.io> |
||||
|
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> |
||||
|
Bruno Bigras <bigras.bruno@gmail.com> |
||||
|
Caleb Spare <cespare@gmail.com> |
||||
|
Case Nelson <case@teammating.com> |
||||
|
Chris Howey <chris@howey.me> <howeyc@gmail.com> |
||||
|
Christoffer Buchholz <christoffer.buchholz@gmail.com> |
||||
|
Daniel Wagner-Hall <dawagner@gmail.com> |
||||
|
Dave Cheney <dave@cheney.net> |
||||
|
Evan Phoenix <evan@fallingsnow.net> |
||||
|
Francisco Souza <f@souza.cc> |
||||
|
Hari haran <hariharan.uno@gmail.com> |
||||
|
John C Barstow |
||||
|
Kelvin Fo <vmirage@gmail.com> |
||||
|
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp> |
||||
|
Matt Layher <mdlayher@gmail.com> |
||||
|
Nathan Youngman <git@nathany.com> |
||||
|
Nickolai Zeldovich <nickolai@csail.mit.edu> |
||||
|
Patrick <patrick@dropbox.com> |
||||
|
Paul Hammond <paul@paulhammond.org> |
||||
|
Pawel Knap <pawelknap88@gmail.com> |
||||
|
Pieter Droogendijk <pieter@binky.org.uk> |
||||
|
Pursuit92 <JoshChase@techpursuit.net> |
||||
|
Riku Voipio <riku.voipio@linaro.org> |
||||
|
Rob Figueiredo <robfig@gmail.com> |
||||
|
Rodrigo Chiossi <rodrigochiossi@gmail.com> |
||||
|
Slawek Ligus <root@ooz.ie> |
||||
|
Soge Zhang <zhssoge@gmail.com> |
||||
|
Tiffany Jernigan <tiffany.jernigan@intel.com> |
||||
|
Tilak Sharma <tilaks@google.com> |
||||
|
Tom Payne <twpayne@gmail.com> |
||||
|
Travis Cline <travis.cline@gmail.com> |
||||
|
Tudor Golubenco <tudor.g@gmail.com> |
||||
|
Vahe Khachikyan <vahe@live.ca> |
||||
|
Yukang <moorekang@gmail.com> |
||||
|
bronze1man <bronze1man@gmail.com> |
||||
|
debrando <denis.brandolini@gmail.com> |
||||
|
henrikedwards <henrik.edwards@gmail.com> |
||||
|
铁哥 <guotie.9@gmail.com> |
@ -0,0 +1,317 @@ |
|||||
|
# Changelog |
||||
|
|
||||
|
## v1.4.7 / 2018-01-09 |
||||
|
|
||||
|
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) |
||||
|
* Tests: Fix missing verb on format string (thanks @rchiossi) |
||||
|
* Linux: Fix deadlock in Remove (thanks @aarondl) |
||||
|
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) |
||||
|
* Docs: Moved FAQ into the README (thanks @vahe) |
||||
|
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) |
||||
|
* Docs: replace references to OS X with macOS |
||||
|
|
||||
|
## v1.4.2 / 2016-10-10 |
||||
|
|
||||
|
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack) |
||||
|
|
||||
|
## v1.4.1 / 2016-10-04 |
||||
|
|
||||
|
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) |
||||
|
|
||||
|
## v1.4.0 / 2016-10-01 |
||||
|
|
||||
|
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) |
||||
|
|
||||
|
## v1.3.1 / 2016-06-28 |
||||
|
|
||||
|
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) |
||||
|
|
||||
|
## v1.3.0 / 2016-04-19 |
||||
|
|
||||
|
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135) |
||||
|
|
||||
|
## v1.2.10 / 2016-03-02 |
||||
|
|
||||
|
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) |
||||
|
|
||||
|
## v1.2.9 / 2016-01-13 |
||||
|
|
||||
|
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) |
||||
|
|
||||
|
## v1.2.8 / 2015-12-17 |
||||
|
|
||||
|
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test) |
||||
|
* inotify: fix race in test |
||||
|
* enable race detection for continuous integration (Linux, Mac, Windows) |
||||
|
|
||||
|
## v1.2.5 / 2015-10-17 |
||||
|
|
||||
|
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) |
||||
|
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) |
||||
|
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) |
||||
|
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) |
||||
|
|
||||
|
## v1.2.1 / 2015-10-14 |
||||
|
|
||||
|
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) |
||||
|
|
||||
|
## v1.2.0 / 2015-02-08 |
||||
|
|
||||
|
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) |
||||
|
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) |
||||
|
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) |
||||
|
|
||||
|
## v1.1.1 / 2015-02-05 |
||||
|
|
||||
|
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) |
||||
|
|
||||
|
## v1.1.0 / 2014-12-12 |
||||
|
|
||||
|
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) |
||||
|
* add low-level functions |
||||
|
* only need to store flags on directories |
||||
|
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) |
||||
|
* done can be an unbuffered channel |
||||
|
* remove calls to os.NewSyscallError |
||||
|
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) |
||||
|
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) |
||||
|
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) |
||||
|
|
||||
|
## v1.0.4 / 2014-09-07 |
||||
|
|
||||
|
* kqueue: add dragonfly to the build tags. |
||||
|
* Rename source code files, rearrange code so exported APIs are at the top. |
||||
|
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) |
||||
|
|
||||
|
## v1.0.3 / 2014-08-19 |
||||
|
|
||||
|
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) |
||||
|
|
||||
|
## v1.0.2 / 2014-08-17 |
||||
|
|
||||
|
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) |
||||
|
* [Fix] Make ./path and path equivalent. (thanks @zhsso) |
||||
|
|
||||
|
## v1.0.0 / 2014-08-15 |
||||
|
|
||||
|
* [API] Remove AddWatch on Windows, use Add. |
||||
|
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) |
||||
|
* Minor updates based on feedback from golint. |
||||
|
|
||||
|
## dev / 2014-07-09 |
||||
|
|
||||
|
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). |
||||
|
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) |
||||
|
|
||||
|
## dev / 2014-07-04 |
||||
|
|
||||
|
* kqueue: fix incorrect mutex used in Close() |
||||
|
* Update example to demonstrate usage of Op. |
||||
|
|
||||
|
## dev / 2014-06-28 |
||||
|
|
||||
|
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) |
||||
|
* Fix for String() method on Event (thanks Alex Brainman) |
||||
|
* Don't build on Plan 9 or Solaris (thanks @4ad) |
||||
|
|
||||
|
## dev / 2014-06-21 |
||||
|
|
||||
|
* Events channel of type Event rather than *Event. |
||||
|
* [internal] use syscall constants directly for inotify and kqueue. |
||||
|
* [internal] kqueue: rename events to kevents and fileEvent to event. |
||||
|
|
||||
|
## dev / 2014-06-19 |
||||
|
|
||||
|
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). |
||||
|
* [internal] remove cookie from Event struct (unused). |
||||
|
* [internal] Event struct has the same definition across every OS. |
||||
|
* [internal] remove internal watch and removeWatch methods. |
||||
|
|
||||
|
## dev / 2014-06-12 |
||||
|
|
||||
|
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). |
||||
|
* [API] Pluralized channel names: Events and Errors. |
||||
|
* [API] Renamed FileEvent struct to Event. |
||||
|
* [API] Op constants replace methods like IsCreate(). |
||||
|
|
||||
|
## dev / 2014-06-12 |
||||
|
|
||||
|
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) |
||||
|
|
||||
|
## dev / 2014-05-23 |
||||
|
|
||||
|
* [API] Remove current implementation of WatchFlags. |
||||
|
* current implementation doesn't take advantage of OS for efficiency |
||||
|
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes |
||||
|
* no tests for the current implementation |
||||
|
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) |
||||
|
|
||||
|
## v0.9.3 / 2014-12-31 |
||||
|
|
||||
|
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) |
||||
|
|
||||
|
## v0.9.2 / 2014-08-17 |
||||
|
|
||||
|
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) |
||||
|
|
||||
|
## v0.9.1 / 2014-06-12 |
||||
|
|
||||
|
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) |
||||
|
|
||||
|
## v0.9.0 / 2014-01-17 |
||||
|
|
||||
|
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) |
||||
|
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) |
||||
|
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. |
||||
|
|
||||
|
## v0.8.12 / 2013-11-13 |
||||
|
|
||||
|
* [API] Remove FD_SET and friends from Linux adapter |
||||
|
|
||||
|
## v0.8.11 / 2013-11-02 |
||||
|
|
||||
|
* [Doc] Add Changelog [#72][] (thanks @nathany) |
||||
|
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) |
||||
|
|
||||
|
## v0.8.10 / 2013-10-19 |
||||
|
|
||||
|
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) |
||||
|
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) |
||||
|
* [Doc] specify OS-specific limits in README (thanks @debrando) |
||||
|
|
||||
|
## v0.8.9 / 2013-09-08 |
||||
|
|
||||
|
* [Doc] Contributing (thanks @nathany) |
||||
|
* [Doc] update package path in example code [#63][] (thanks @paulhammond) |
||||
|
* [Doc] GoCI badge in README (Linux only) [#60][] |
||||
|
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) |
||||
|
|
||||
|
## v0.8.8 / 2013-06-17 |
||||
|
|
||||
|
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) |
||||
|
|
||||
|
## v0.8.7 / 2013-06-03 |
||||
|
|
||||
|
* [API] Make syscall flags internal |
||||
|
* [Fix] inotify: ignore event changes |
||||
|
* [Fix] race in symlink test [#45][] (reported by @srid) |
||||
|
* [Fix] tests on Windows |
||||
|
* lower case error messages |
||||
|
|
||||
|
## v0.8.6 / 2013-05-23 |
||||
|
|
||||
|
* kqueue: Use EVT_ONLY flag on Darwin |
||||
|
* [Doc] Update README with full example |
||||
|
|
||||
|
## v0.8.5 / 2013-05-09 |
||||
|
|
||||
|
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) |
||||
|
|
||||
|
## v0.8.4 / 2013-04-07 |
||||
|
|
||||
|
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) |
||||
|
|
||||
|
## v0.8.3 / 2013-03-13 |
||||
|
|
||||
|
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) |
||||
|
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) |
||||
|
|
||||
|
## v0.8.2 / 2013-02-07 |
||||
|
|
||||
|
* [Doc] add Authors |
||||
|
* [Fix] fix data races for map access [#29][] (thanks @fsouza) |
||||
|
|
||||
|
## v0.8.1 / 2013-01-09 |
||||
|
|
||||
|
* [Fix] Windows path separators |
||||
|
* [Doc] BSD License |
||||
|
|
||||
|
## v0.8.0 / 2012-11-09 |
||||
|
|
||||
|
* kqueue: directory watching improvements (thanks @vmirage) |
||||
|
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) |
||||
|
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) |
||||
|
|
||||
|
## v0.7.4 / 2012-10-09 |
||||
|
|
||||
|
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) |
||||
|
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) |
||||
|
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) |
||||
|
* [Fix] kqueue: modify after recreation of file |
||||
|
|
||||
|
## v0.7.3 / 2012-09-27 |
||||
|
|
||||
|
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) |
||||
|
* [Fix] kqueue: no longer get duplicate CREATE events |
||||
|
|
||||
|
## v0.7.2 / 2012-09-01 |
||||
|
|
||||
|
* kqueue: events for created directories |
||||
|
|
||||
|
## v0.7.1 / 2012-07-14 |
||||
|
|
||||
|
* [Fix] for renaming files |
||||
|
|
||||
|
## v0.7.0 / 2012-07-02 |
||||
|
|
||||
|
* [Feature] FSNotify flags |
||||
|
* [Fix] inotify: Added file name back to event path |
||||
|
|
||||
|
## v0.6.0 / 2012-06-06 |
||||
|
|
||||
|
* kqueue: watch files after directory created (thanks @tmc) |
||||
|
|
||||
|
## v0.5.1 / 2012-05-22 |
||||
|
|
||||
|
* [Fix] inotify: remove all watches before Close() |
||||
|
|
||||
|
## v0.5.0 / 2012-05-03 |
||||
|
|
||||
|
* [API] kqueue: return errors during watch instead of sending over channel |
||||
|
* kqueue: match symlink behavior on Linux |
||||
|
* inotify: add `DELETE_SELF` (requested by @taralx) |
||||
|
* [Fix] kqueue: handle EINTR (reported by @robfig) |
||||
|
* [Doc] Godoc example [#1][] (thanks @davecheney) |
||||
|
|
||||
|
## v0.4.0 / 2012-03-30 |
||||
|
|
||||
|
* Go 1 released: build with go tool |
||||
|
* [Feature] Windows support using winfsnotify |
||||
|
* Windows does not have attribute change notifications |
||||
|
* Roll attribute notifications into IsModify |
||||
|
|
||||
|
## v0.3.0 / 2012-02-19 |
||||
|
|
||||
|
* kqueue: add files when watch directory |
||||
|
|
||||
|
## v0.2.0 / 2011-12-30 |
||||
|
|
||||
|
* update to latest Go weekly code |
||||
|
|
||||
|
## v0.1.0 / 2011-10-19 |
||||
|
|
||||
|
* kqueue: add watch on file creation to match inotify |
||||
|
* kqueue: create file event |
||||
|
* inotify: ignore `IN_IGNORED` events |
||||
|
* event String() |
||||
|
* linux: common FileEvent functions |
||||
|
* initial commit |
||||
|
|
||||
|
[#79]: https://github.com/howeyc/fsnotify/pull/79 |
||||
|
[#77]: https://github.com/howeyc/fsnotify/pull/77 |
||||
|
[#72]: https://github.com/howeyc/fsnotify/issues/72 |
||||
|
[#71]: https://github.com/howeyc/fsnotify/issues/71 |
||||
|
[#70]: https://github.com/howeyc/fsnotify/issues/70 |
||||
|
[#63]: https://github.com/howeyc/fsnotify/issues/63 |
||||
|
[#62]: https://github.com/howeyc/fsnotify/issues/62 |
||||
|
[#60]: https://github.com/howeyc/fsnotify/issues/60 |
||||
|
[#59]: https://github.com/howeyc/fsnotify/issues/59 |
||||
|
[#49]: https://github.com/howeyc/fsnotify/issues/49 |
||||
|
[#45]: https://github.com/howeyc/fsnotify/issues/45 |
||||
|
[#40]: https://github.com/howeyc/fsnotify/issues/40 |
||||
|
[#36]: https://github.com/howeyc/fsnotify/issues/36 |
||||
|
[#33]: https://github.com/howeyc/fsnotify/issues/33 |
||||
|
[#29]: https://github.com/howeyc/fsnotify/issues/29 |
||||
|
[#25]: https://github.com/howeyc/fsnotify/issues/25 |
||||
|
[#24]: https://github.com/howeyc/fsnotify/issues/24 |
||||
|
[#21]: https://github.com/howeyc/fsnotify/issues/21 |
@ -0,0 +1,77 @@ |
|||||
|
# Contributing |
||||
|
|
||||
|
## Issues |
||||
|
|
||||
|
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues). |
||||
|
* Please indicate the platform you are using fsnotify on. |
||||
|
* A code example to reproduce the problem is appreciated. |
||||
|
|
||||
|
## Pull Requests |
||||
|
|
||||
|
### Contributor License Agreement |
||||
|
|
||||
|
fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). |
||||
|
|
||||
|
Please indicate that you have signed the CLA in your pull request. |
||||
|
|
||||
|
### How fsnotify is Developed |
||||
|
|
||||
|
* Development is done on feature branches. |
||||
|
* Tests are run on BSD, Linux, macOS and Windows. |
||||
|
* Pull requests are reviewed and [applied to master][am] using [hub][]. |
||||
|
* Maintainers may modify or squash commits rather than asking contributors to. |
||||
|
* To issue a new release, the maintainers will: |
||||
|
* Update the CHANGELOG |
||||
|
* Tag a version, which will become available through gopkg.in. |
||||
|
|
||||
|
### How to Fork |
||||
|
|
||||
|
For smooth sailing, always use the original import path. Installing with `go get` makes this easy. |
||||
|
|
||||
|
1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`) |
||||
|
2. Create your feature branch (`git checkout -b my-new-feature`) |
||||
|
3. Ensure everything works and the tests pass (see below) |
||||
|
4. Commit your changes (`git commit -am 'Add some feature'`) |
||||
|
|
||||
|
Contribute upstream: |
||||
|
|
||||
|
1. Fork fsnotify on GitHub |
||||
|
2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) |
||||
|
3. Push to the branch (`git push fork my-new-feature`) |
||||
|
4. Create a new Pull Request on GitHub |
||||
|
|
||||
|
This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/). |
||||
|
|
||||
|
### Testing |
||||
|
|
||||
|
fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows. |
||||
|
|
||||
|
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. |
||||
|
|
||||
|
To aid in cross-platform testing there is a Vagrantfile for Linux and BSD. |
||||
|
|
||||
|
* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) |
||||
|
* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. |
||||
|
* Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password) |
||||
|
* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd fsnotify/fsnotify; go test'`. |
||||
|
* When you're done, you will want to halt or destroy the Vagrant boxes. |
||||
|
|
||||
|
Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory. |
||||
|
|
||||
|
Right now there is no equivalent solution for Windows and macOS, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads). |
||||
|
|
||||
|
### Maintainers |
||||
|
|
||||
|
Help maintaining fsnotify is welcome. To be a maintainer: |
||||
|
|
||||
|
* Submit a pull request and sign the CLA as above. |
||||
|
* You must be able to run the test suite on Mac, Windows, Linux and BSD. |
||||
|
|
||||
|
To keep master clean, the fsnotify project uses the "apply mail" workflow outlined in Nathaniel Talbott's post ["Merge pull request" Considered Harmful][am]. This requires installing [hub][]. |
||||
|
|
||||
|
All code changes should be internal pull requests. |
||||
|
|
||||
|
Releases are tagged using [Semantic Versioning](http://semver.org/). |
||||
|
|
||||
|
[hub]: https://github.com/github/hub |
||||
|
[am]: http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.VGa5yZPF_Zs |
@ -0,0 +1,28 @@ |
|||||
|
Copyright (c) 2012 The Go Authors. All rights reserved. |
||||
|
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. |
||||
|
|
||||
|
Redistribution and use in source and binary forms, with or without |
||||
|
modification, are permitted provided that the following conditions are |
||||
|
met: |
||||
|
|
||||
|
* Redistributions of source code must retain the above copyright |
||||
|
notice, this list of conditions and the following disclaimer. |
||||
|
* Redistributions in binary form must reproduce the above |
||||
|
copyright notice, this list of conditions and the following disclaimer |
||||
|
in the documentation and/or other materials provided with the |
||||
|
distribution. |
||||
|
* Neither the name of Google Inc. nor the names of its |
||||
|
contributors may be used to endorse or promote products derived from |
||||
|
this software without specific prior written permission. |
||||
|
|
||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,130 @@ |
|||||
|
# File system notifications for Go |
||||
|
|
||||
|
[![GoDoc](https://godoc.org/github.com/fsnotify/fsnotify?status.svg)](https://godoc.org/github.com/fsnotify/fsnotify) [![Go Report Card](https://goreportcard.com/badge/github.com/fsnotify/fsnotify)](https://goreportcard.com/report/github.com/fsnotify/fsnotify) |
||||
|
|
||||
|
fsnotify utilizes [golang.org/x/sys](https://godoc.org/golang.org/x/sys) rather than `syscall` from the standard library. Ensure you have the latest version installed by running: |
||||
|
|
||||
|
```console |
||||
|
go get -u golang.org/x/sys/... |
||||
|
``` |
||||
|
|
||||
|
Cross platform: Windows, Linux, BSD and macOS. |
||||
|
|
||||
|
| Adapter | OS | Status | |
||||
|
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | |
||||
|
| inotify | Linux 2.6.27 or later, Android\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | |
||||
|
| kqueue | BSD, macOS, iOS\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | |
||||
|
| ReadDirectoryChangesW | Windows | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | |
||||
|
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) | |
||||
|
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/issues/12) | |
||||
|
| fanotify | Linux 2.6.37+ | [Planned](https://github.com/fsnotify/fsnotify/issues/114) | |
||||
|
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) | |
||||
|
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) | |
||||
|
|
||||
|
\* Android and iOS are untested. |
||||
|
|
||||
|
Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information. |
||||
|
|
||||
|
## API stability |
||||
|
|
||||
|
fsnotify is a fork of [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify) with a new API as of v1.0. The API is based on [this design document](http://goo.gl/MrYxyA). |
||||
|
|
||||
|
All [releases](https://github.com/fsnotify/fsnotify/releases) are tagged based on [Semantic Versioning](http://semver.org/). Further API changes are [planned](https://github.com/fsnotify/fsnotify/milestones), and will be tagged with a new major revision number. |
||||
|
|
||||
|
Go 1.6 supports dependencies located in the `vendor/` folder. Unless you are creating a library, it is recommended that you copy fsnotify into `vendor/github.com/fsnotify/fsnotify` within your project, and likewise for `golang.org/x/sys`. |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```go |
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"log" |
||||
|
|
||||
|
"github.com/fsnotify/fsnotify" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
watcher, err := fsnotify.NewWatcher() |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
defer watcher.Close() |
||||
|
|
||||
|
done := make(chan bool) |
||||
|
go func() { |
||||
|
for { |
||||
|
select { |
||||
|
case event, ok := <-watcher.Events: |
||||
|
if !ok { |
||||
|
return |
||||
|
} |
||||
|
log.Println("event:", event) |
||||
|
if event.Op&fsnotify.Write == fsnotify.Write { |
||||
|
log.Println("modified file:", event.Name) |
||||
|
} |
||||
|
case err, ok := <-watcher.Errors: |
||||
|
if !ok { |
||||
|
return |
||||
|
} |
||||
|
log.Println("error:", err) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
err = watcher.Add("/tmp/foo") |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
<-done |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Contributing |
||||
|
|
||||
|
Please refer to [CONTRIBUTING][] before opening an issue or pull request. |
||||
|
|
||||
|
## Example |
||||
|
|
||||
|
See [example_test.go](https://github.com/fsnotify/fsnotify/blob/master/example_test.go). |
||||
|
|
||||
|
## FAQ |
||||
|
|
||||
|
**When a file is moved to another directory is it still being watched?** |
||||
|
|
||||
|
No (it shouldn't be, unless you are watching where it was moved to). |
||||
|
|
||||
|
**When I watch a directory, are all subdirectories watched as well?** |
||||
|
|
||||
|
No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]). |
||||
|
|
||||
|
**Do I have to watch the Error and Event channels in a separate goroutine?** |
||||
|
|
||||
|
As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7]) |
||||
|
|
||||
|
**Why am I receiving multiple events for the same file on OS X?** |
||||
|
|
||||
|
Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]). |
||||
|
|
||||
|
**How many files can be watched at once?** |
||||
|
|
||||
|
There are OS-specific limits as to how many watches can be created: |
||||
|
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error. |
||||
|
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error. |
||||
|
|
||||
|
**Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?** |
||||
|
|
||||
|
fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications. |
||||
|
|
||||
|
[#62]: https://github.com/howeyc/fsnotify/issues/62 |
||||
|
[#18]: https://github.com/fsnotify/fsnotify/issues/18 |
||||
|
[#11]: https://github.com/fsnotify/fsnotify/issues/11 |
||||
|
[#7]: https://github.com/howeyc/fsnotify/issues/7 |
||||
|
|
||||
|
[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md |
||||
|
|
||||
|
## Related Projects |
||||
|
|
||||
|
* [notify](https://github.com/rjeczalik/notify) |
||||
|
* [fsevents](https://github.com/fsnotify/fsevents) |
||||
|
|
@ -0,0 +1,37 @@ |
|||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build solaris
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// Watcher watches a set of files, delivering events to a channel.
|
||||
|
type Watcher struct { |
||||
|
Events chan Event |
||||
|
Errors chan error |
||||
|
} |
||||
|
|
||||
|
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
|
func NewWatcher() (*Watcher, error) { |
||||
|
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") |
||||
|
} |
||||
|
|
||||
|
// Close removes all watches and closes the events channel.
|
||||
|
func (w *Watcher) Close() error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Add starts watching the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Add(name string) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Remove stops watching the the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Remove(name string) error { |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,68 @@ |
|||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build !plan9
|
||||
|
|
||||
|
// Package fsnotify provides a platform-independent interface for file system notifications.
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
// Event represents a single file system notification.
|
||||
|
type Event struct { |
||||
|
Name string // Relative path to the file or directory.
|
||||
|
Op Op // File operation that triggered the event.
|
||||
|
} |
||||
|
|
||||
|
// Op describes a set of file operations.
|
||||
|
type Op uint32 |
||||
|
|
||||
|
// These are the generalized file operations that can trigger a notification.
|
||||
|
const ( |
||||
|
Create Op = 1 << iota |
||||
|
Write |
||||
|
Remove |
||||
|
Rename |
||||
|
Chmod |
||||
|
) |
||||
|
|
||||
|
func (op Op) String() string { |
||||
|
// Use a buffer for efficient string concatenation
|
||||
|
var buffer bytes.Buffer |
||||
|
|
||||
|
if op&Create == Create { |
||||
|
buffer.WriteString("|CREATE") |
||||
|
} |
||||
|
if op&Remove == Remove { |
||||
|
buffer.WriteString("|REMOVE") |
||||
|
} |
||||
|
if op&Write == Write { |
||||
|
buffer.WriteString("|WRITE") |
||||
|
} |
||||
|
if op&Rename == Rename { |
||||
|
buffer.WriteString("|RENAME") |
||||
|
} |
||||
|
if op&Chmod == Chmod { |
||||
|
buffer.WriteString("|CHMOD") |
||||
|
} |
||||
|
if buffer.Len() == 0 { |
||||
|
return "" |
||||
|
} |
||||
|
return buffer.String()[1:] // Strip leading pipe
|
||||
|
} |
||||
|
|
||||
|
// String returns a string representation of the event in the form
|
||||
|
// "file: REMOVE|WRITE|..."
|
||||
|
func (e Event) String() string { |
||||
|
return fmt.Sprintf("%q: %s", e.Name, e.Op.String()) |
||||
|
} |
||||
|
|
||||
|
// Common errors that can be reported by a watcher
|
||||
|
var ( |
||||
|
ErrEventOverflow = errors.New("fsnotify queue overflow") |
||||
|
) |
@ -0,0 +1,5 @@ |
|||||
|
module github.com/fsnotify/fsnotify |
||||
|
|
||||
|
go 1.13 |
||||
|
|
||||
|
require golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 |
@ -0,0 +1,2 @@ |
|||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= |
||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
@ -0,0 +1,337 @@ |
|||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build linux
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"unsafe" |
||||
|
|
||||
|
"golang.org/x/sys/unix" |
||||
|
) |
||||
|
|
||||
|
// Watcher watches a set of files, delivering events to a channel.
|
||||
|
type Watcher struct { |
||||
|
Events chan Event |
||||
|
Errors chan error |
||||
|
mu sync.Mutex // Map access
|
||||
|
fd int |
||||
|
poller *fdPoller |
||||
|
watches map[string]*watch // Map of inotify watches (key: path)
|
||||
|
paths map[int]string // Map of watched paths (key: watch descriptor)
|
||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||
|
doneResp chan struct{} // Channel to respond to Close
|
||||
|
} |
||||
|
|
||||
|
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
|
func NewWatcher() (*Watcher, error) { |
||||
|
// Create inotify fd
|
||||
|
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC) |
||||
|
if fd == -1 { |
||||
|
return nil, errno |
||||
|
} |
||||
|
// Create epoll
|
||||
|
poller, err := newFdPoller(fd) |
||||
|
if err != nil { |
||||
|
unix.Close(fd) |
||||
|
return nil, err |
||||
|
} |
||||
|
w := &Watcher{ |
||||
|
fd: fd, |
||||
|
poller: poller, |
||||
|
watches: make(map[string]*watch), |
||||
|
paths: make(map[int]string), |
||||
|
Events: make(chan Event), |
||||
|
Errors: make(chan error), |
||||
|
done: make(chan struct{}), |
||||
|
doneResp: make(chan struct{}), |
||||
|
} |
||||
|
|
||||
|
go w.readEvents() |
||||
|
return w, nil |
||||
|
} |
||||
|
|
||||
|
func (w *Watcher) isClosed() bool { |
||||
|
select { |
||||
|
case <-w.done: |
||||
|
return true |
||||
|
default: |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Close removes all watches and closes the events channel.
|
||||
|
func (w *Watcher) Close() error { |
||||
|
if w.isClosed() { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Send 'close' signal to goroutine, and set the Watcher to closed.
|
||||
|
close(w.done) |
||||
|
|
||||
|
// Wake up goroutine
|
||||
|
w.poller.wake() |
||||
|
|
||||
|
// Wait for goroutine to close
|
||||
|
<-w.doneResp |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Add starts watching the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Add(name string) error { |
||||
|
name = filepath.Clean(name) |
||||
|
if w.isClosed() { |
||||
|
return errors.New("inotify instance already closed") |
||||
|
} |
||||
|
|
||||
|
const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | |
||||
|
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | |
||||
|
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF |
||||
|
|
||||
|
var flags uint32 = agnosticEvents |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
defer w.mu.Unlock() |
||||
|
watchEntry := w.watches[name] |
||||
|
if watchEntry != nil { |
||||
|
flags |= watchEntry.flags | unix.IN_MASK_ADD |
||||
|
} |
||||
|
wd, errno := unix.InotifyAddWatch(w.fd, name, flags) |
||||
|
if wd == -1 { |
||||
|
return errno |
||||
|
} |
||||
|
|
||||
|
if watchEntry == nil { |
||||
|
w.watches[name] = &watch{wd: uint32(wd), flags: flags} |
||||
|
w.paths[wd] = name |
||||
|
} else { |
||||
|
watchEntry.wd = uint32(wd) |
||||
|
watchEntry.flags = flags |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Remove stops watching the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Remove(name string) error { |
||||
|
name = filepath.Clean(name) |
||||
|
|
||||
|
// Fetch the watch.
|
||||
|
w.mu.Lock() |
||||
|
defer w.mu.Unlock() |
||||
|
watch, ok := w.watches[name] |
||||
|
|
||||
|
// Remove it from inotify.
|
||||
|
if !ok { |
||||
|
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) |
||||
|
} |
||||
|
|
||||
|
// We successfully removed the watch if InotifyRmWatch doesn't return an
|
||||
|
// error, we need to clean up our internal state to ensure it matches
|
||||
|
// inotify's kernel state.
|
||||
|
delete(w.paths, int(watch.wd)) |
||||
|
delete(w.watches, name) |
||||
|
|
||||
|
// inotify_rm_watch will return EINVAL if the file has been deleted;
|
||||
|
// the inotify will already have been removed.
|
||||
|
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
|
||||
|
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
|
||||
|
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
|
||||
|
// by another thread and we have not received IN_IGNORE event.
|
||||
|
success, errno := unix.InotifyRmWatch(w.fd, watch.wd) |
||||
|
if success == -1 { |
||||
|
// TODO: Perhaps it's not helpful to return an error here in every case.
|
||||
|
// the only two possible errors are:
|
||||
|
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
|
||||
|
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
|
||||
|
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
|
||||
|
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
|
||||
|
return errno |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type watch struct { |
||||
|
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||
|
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||
|
} |
||||
|
|
||||
|
// readEvents reads from the inotify file descriptor, converts the
|
||||
|
// received events into Event objects and sends them via the Events channel
|
||||
|
func (w *Watcher) readEvents() { |
||||
|
var ( |
||||
|
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
||||
|
n int // Number of bytes read with read()
|
||||
|
errno error // Syscall errno
|
||||
|
ok bool // For poller.wait
|
||||
|
) |
||||
|
|
||||
|
defer close(w.doneResp) |
||||
|
defer close(w.Errors) |
||||
|
defer close(w.Events) |
||||
|
defer unix.Close(w.fd) |
||||
|
defer w.poller.close() |
||||
|
|
||||
|
for { |
||||
|
// See if we have been closed.
|
||||
|
if w.isClosed() { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
ok, errno = w.poller.wait() |
||||
|
if errno != nil { |
||||
|
select { |
||||
|
case w.Errors <- errno: |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if !ok { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
n, errno = unix.Read(w.fd, buf[:]) |
||||
|
// If a signal interrupted execution, see if we've been asked to close, and try again.
|
||||
|
// http://man7.org/linux/man-pages/man7/signal.7.html :
|
||||
|
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
|
||||
|
if errno == unix.EINTR { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// unix.Read might have been woken up by Close. If so, we're done.
|
||||
|
if w.isClosed() { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if n < unix.SizeofInotifyEvent { |
||||
|
var err error |
||||
|
if n == 0 { |
||||
|
// If EOF is received. This should really never happen.
|
||||
|
err = io.EOF |
||||
|
} else if n < 0 { |
||||
|
// If an error occurred while reading.
|
||||
|
err = errno |
||||
|
} else { |
||||
|
// Read was too short.
|
||||
|
err = errors.New("notify: short read in readEvents()") |
||||
|
} |
||||
|
select { |
||||
|
case w.Errors <- err: |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
var offset uint32 |
||||
|
// We don't know how many events we just read into the buffer
|
||||
|
// While the offset points to at least one whole event...
|
||||
|
for offset <= uint32(n-unix.SizeofInotifyEvent) { |
||||
|
// Point "raw" to the event in the buffer
|
||||
|
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) |
||||
|
|
||||
|
mask := uint32(raw.Mask) |
||||
|
nameLen := uint32(raw.Len) |
||||
|
|
||||
|
if mask&unix.IN_Q_OVERFLOW != 0 { |
||||
|
select { |
||||
|
case w.Errors <- ErrEventOverflow: |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If the event happened to the watched directory or the watched file, the kernel
|
||||
|
// doesn't append the filename to the event, but we would like to always fill the
|
||||
|
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||
|
// the "paths" map.
|
||||
|
w.mu.Lock() |
||||
|
name, ok := w.paths[int(raw.Wd)] |
||||
|
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
|
||||
|
// This is a sign to clean up the maps, otherwise we are no longer in sync
|
||||
|
// with the inotify kernel state which has already deleted the watch
|
||||
|
// automatically.
|
||||
|
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { |
||||
|
delete(w.paths, int(raw.Wd)) |
||||
|
delete(w.watches, name) |
||||
|
} |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
if nameLen > 0 { |
||||
|
// Point "bytes" at the first byte of the filename
|
||||
|
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent])) |
||||
|
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
|
||||
|
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") |
||||
|
} |
||||
|
|
||||
|
event := newEvent(name, mask) |
||||
|
|
||||
|
// Send the events that are not ignored on the events channel
|
||||
|
if !event.ignoreLinux(mask) { |
||||
|
select { |
||||
|
case w.Events <- event: |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Move to the next event in the buffer
|
||||
|
offset += unix.SizeofInotifyEvent + nameLen |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Certain types of events can be "ignored" and not sent over the Events
|
||||
|
// channel. Such as events marked ignore by the kernel, or MODIFY events
|
||||
|
// against files that do not exist.
|
||||
|
func (e *Event) ignoreLinux(mask uint32) bool { |
||||
|
// Ignore anything the inotify API says to ignore
|
||||
|
if mask&unix.IN_IGNORED == unix.IN_IGNORED { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// If the event is not a DELETE or RENAME, the file must exist.
|
||||
|
// Otherwise the event is ignored.
|
||||
|
// *Note*: this was put in place because it was seen that a MODIFY
|
||||
|
// event was sent after the DELETE. This ignores that MODIFY and
|
||||
|
// assumes a DELETE will come or has come if the file doesn't exist.
|
||||
|
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { |
||||
|
_, statErr := os.Lstat(e.Name) |
||||
|
return os.IsNotExist(statErr) |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// newEvent returns an platform-independent Event based on an inotify mask.
|
||||
|
func newEvent(name string, mask uint32) Event { |
||||
|
e := Event{Name: name} |
||||
|
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { |
||||
|
e.Op |= Create |
||||
|
} |
||||
|
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { |
||||
|
e.Op |= Remove |
||||
|
} |
||||
|
if mask&unix.IN_MODIFY == unix.IN_MODIFY { |
||||
|
e.Op |= Write |
||||
|
} |
||||
|
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { |
||||
|
e.Op |= Rename |
||||
|
} |
||||
|
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { |
||||
|
e.Op |= Chmod |
||||
|
} |
||||
|
return e |
||||
|
} |
@ -0,0 +1,187 @@ |
|||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build linux
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
|
||||
|
"golang.org/x/sys/unix" |
||||
|
) |
||||
|
|
||||
|
type fdPoller struct { |
||||
|
fd int // File descriptor (as returned by the inotify_init() syscall)
|
||||
|
epfd int // Epoll file descriptor
|
||||
|
pipe [2]int // Pipe for waking up
|
||||
|
} |
||||
|
|
||||
|
func emptyPoller(fd int) *fdPoller { |
||||
|
poller := new(fdPoller) |
||||
|
poller.fd = fd |
||||
|
poller.epfd = -1 |
||||
|
poller.pipe[0] = -1 |
||||
|
poller.pipe[1] = -1 |
||||
|
return poller |
||||
|
} |
||||
|
|
||||
|
// Create a new inotify poller.
|
||||
|
// This creates an inotify handler, and an epoll handler.
|
||||
|
func newFdPoller(fd int) (*fdPoller, error) { |
||||
|
var errno error |
||||
|
poller := emptyPoller(fd) |
||||
|
defer func() { |
||||
|
if errno != nil { |
||||
|
poller.close() |
||||
|
} |
||||
|
}() |
||||
|
poller.fd = fd |
||||
|
|
||||
|
// Create epoll fd
|
||||
|
poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC) |
||||
|
if poller.epfd == -1 { |
||||
|
return nil, errno |
||||
|
} |
||||
|
// Create pipe; pipe[0] is the read end, pipe[1] the write end.
|
||||
|
errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC) |
||||
|
if errno != nil { |
||||
|
return nil, errno |
||||
|
} |
||||
|
|
||||
|
// Register inotify fd with epoll
|
||||
|
event := unix.EpollEvent{ |
||||
|
Fd: int32(poller.fd), |
||||
|
Events: unix.EPOLLIN, |
||||
|
} |
||||
|
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event) |
||||
|
if errno != nil { |
||||
|
return nil, errno |
||||
|
} |
||||
|
|
||||
|
// Register pipe fd with epoll
|
||||
|
event = unix.EpollEvent{ |
||||
|
Fd: int32(poller.pipe[0]), |
||||
|
Events: unix.EPOLLIN, |
||||
|
} |
||||
|
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event) |
||||
|
if errno != nil { |
||||
|
return nil, errno |
||||
|
} |
||||
|
|
||||
|
return poller, nil |
||||
|
} |
||||
|
|
||||
|
// Wait using epoll.
|
||||
|
// Returns true if something is ready to be read,
|
||||
|
// false if there is not.
|
||||
|
func (poller *fdPoller) wait() (bool, error) { |
||||
|
// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
|
||||
|
// I don't know whether epoll_wait returns the number of events returned,
|
||||
|
// or the total number of events ready.
|
||||
|
// I decided to catch both by making the buffer one larger than the maximum.
|
||||
|
events := make([]unix.EpollEvent, 7) |
||||
|
for { |
||||
|
n, errno := unix.EpollWait(poller.epfd, events, -1) |
||||
|
if n == -1 { |
||||
|
if errno == unix.EINTR { |
||||
|
continue |
||||
|
} |
||||
|
return false, errno |
||||
|
} |
||||
|
if n == 0 { |
||||
|
// If there are no events, try again.
|
||||
|
continue |
||||
|
} |
||||
|
if n > 6 { |
||||
|
// This should never happen. More events were returned than should be possible.
|
||||
|
return false, errors.New("epoll_wait returned more events than I know what to do with") |
||||
|
} |
||||
|
ready := events[:n] |
||||
|
epollhup := false |
||||
|
epollerr := false |
||||
|
epollin := false |
||||
|
for _, event := range ready { |
||||
|
if event.Fd == int32(poller.fd) { |
||||
|
if event.Events&unix.EPOLLHUP != 0 { |
||||
|
// This should not happen, but if it does, treat it as a wakeup.
|
||||
|
epollhup = true |
||||
|
} |
||||
|
if event.Events&unix.EPOLLERR != 0 { |
||||
|
// If an error is waiting on the file descriptor, we should pretend
|
||||
|
// something is ready to read, and let unix.Read pick up the error.
|
||||
|
epollerr = true |
||||
|
} |
||||
|
if event.Events&unix.EPOLLIN != 0 { |
||||
|
// There is data to read.
|
||||
|
epollin = true |
||||
|
} |
||||
|
} |
||||
|
if event.Fd == int32(poller.pipe[0]) { |
||||
|
if event.Events&unix.EPOLLHUP != 0 { |
||||
|
// Write pipe descriptor was closed, by us. This means we're closing down the
|
||||
|
// watcher, and we should wake up.
|
||||
|
} |
||||
|
if event.Events&unix.EPOLLERR != 0 { |
||||
|
// If an error is waiting on the pipe file descriptor.
|
||||
|
// This is an absolute mystery, and should never ever happen.
|
||||
|
return false, errors.New("Error on the pipe descriptor.") |
||||
|
} |
||||
|
if event.Events&unix.EPOLLIN != 0 { |
||||
|
// This is a regular wakeup, so we have to clear the buffer.
|
||||
|
err := poller.clearWake() |
||||
|
if err != nil { |
||||
|
return false, err |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if epollhup || epollerr || epollin { |
||||
|
return true, nil |
||||
|
} |
||||
|
return false, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Close the write end of the poller.
|
||||
|
func (poller *fdPoller) wake() error { |
||||
|
buf := make([]byte, 1) |
||||
|
n, errno := unix.Write(poller.pipe[1], buf) |
||||
|
if n == -1 { |
||||
|
if errno == unix.EAGAIN { |
||||
|
// Buffer is full, poller will wake.
|
||||
|
return nil |
||||
|
} |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (poller *fdPoller) clearWake() error { |
||||
|
// You have to be woken up a LOT in order to get to 100!
|
||||
|
buf := make([]byte, 100) |
||||
|
n, errno := unix.Read(poller.pipe[0], buf) |
||||
|
if n == -1 { |
||||
|
if errno == unix.EAGAIN { |
||||
|
// Buffer is empty, someone else cleared our wake.
|
||||
|
return nil |
||||
|
} |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Close all poller file descriptors, but not the one passed to it.
|
||||
|
func (poller *fdPoller) close() { |
||||
|
if poller.pipe[1] != -1 { |
||||
|
unix.Close(poller.pipe[1]) |
||||
|
} |
||||
|
if poller.pipe[0] != -1 { |
||||
|
unix.Close(poller.pipe[0]) |
||||
|
} |
||||
|
if poller.epfd != -1 { |
||||
|
unix.Close(poller.epfd) |
||||
|
} |
||||
|
} |
@ -0,0 +1,521 @@ |
|||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build freebsd openbsd netbsd dragonfly darwin
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"golang.org/x/sys/unix" |
||||
|
) |
||||
|
|
||||
|
// Watcher watches a set of files, delivering events to a channel.
|
||||
|
type Watcher struct { |
||||
|
Events chan Event |
||||
|
Errors chan error |
||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||
|
|
||||
|
kq int // File descriptor (as returned by the kqueue() syscall).
|
||||
|
|
||||
|
mu sync.Mutex // Protects access to watcher data
|
||||
|
watches map[string]int // Map of watched file descriptors (key: path).
|
||||
|
externalWatches map[string]bool // Map of watches added by user of the library.
|
||||
|
dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue.
|
||||
|
paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events.
|
||||
|
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events).
|
||||
|
isClosed bool // Set to true when Close() is first called
|
||||
|
} |
||||
|
|
||||
|
type pathInfo struct { |
||||
|
name string |
||||
|
isDir bool |
||||
|
} |
||||
|
|
||||
|
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
|
func NewWatcher() (*Watcher, error) { |
||||
|
kq, err := kqueue() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
w := &Watcher{ |
||||
|
kq: kq, |
||||
|
watches: make(map[string]int), |
||||
|
dirFlags: make(map[string]uint32), |
||||
|
paths: make(map[int]pathInfo), |
||||
|
fileExists: make(map[string]bool), |
||||
|
externalWatches: make(map[string]bool), |
||||
|
Events: make(chan Event), |
||||
|
Errors: make(chan error), |
||||
|
done: make(chan struct{}), |
||||
|
} |
||||
|
|
||||
|
go w.readEvents() |
||||
|
return w, nil |
||||
|
} |
||||
|
|
||||
|
// Close removes all watches and closes the events channel.
|
||||
|
func (w *Watcher) Close() error { |
||||
|
w.mu.Lock() |
||||
|
if w.isClosed { |
||||
|
w.mu.Unlock() |
||||
|
return nil |
||||
|
} |
||||
|
w.isClosed = true |
||||
|
|
||||
|
// copy paths to remove while locked
|
||||
|
var pathsToRemove = make([]string, 0, len(w.watches)) |
||||
|
for name := range w.watches { |
||||
|
pathsToRemove = append(pathsToRemove, name) |
||||
|
} |
||||
|
w.mu.Unlock() |
||||
|
// unlock before calling Remove, which also locks
|
||||
|
|
||||
|
for _, name := range pathsToRemove { |
||||
|
w.Remove(name) |
||||
|
} |
||||
|
|
||||
|
// send a "quit" message to the reader goroutine
|
||||
|
close(w.done) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Add starts watching the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Add(name string) error { |
||||
|
w.mu.Lock() |
||||
|
w.externalWatches[name] = true |
||||
|
w.mu.Unlock() |
||||
|
_, err := w.addWatch(name, noteAllEvents) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// Remove stops watching the the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Remove(name string) error { |
||||
|
name = filepath.Clean(name) |
||||
|
w.mu.Lock() |
||||
|
watchfd, ok := w.watches[name] |
||||
|
w.mu.Unlock() |
||||
|
if !ok { |
||||
|
return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) |
||||
|
} |
||||
|
|
||||
|
const registerRemove = unix.EV_DELETE |
||||
|
if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
unix.Close(watchfd) |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
isDir := w.paths[watchfd].isDir |
||||
|
delete(w.watches, name) |
||||
|
delete(w.paths, watchfd) |
||||
|
delete(w.dirFlags, name) |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
// Find all watched paths that are in this directory that are not external.
|
||||
|
if isDir { |
||||
|
var pathsToRemove []string |
||||
|
w.mu.Lock() |
||||
|
for _, path := range w.paths { |
||||
|
wdir, _ := filepath.Split(path.name) |
||||
|
if filepath.Clean(wdir) == name { |
||||
|
if !w.externalWatches[path.name] { |
||||
|
pathsToRemove = append(pathsToRemove, path.name) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
w.mu.Unlock() |
||||
|
for _, name := range pathsToRemove { |
||||
|
// Since these are internal, not much sense in propagating error
|
||||
|
// to the user, as that will just confuse them with an error about
|
||||
|
// a path they did not explicitly watch themselves.
|
||||
|
w.Remove(name) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
||||
|
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME |
||||
|
|
||||
|
// keventWaitTime to block on each read from kevent
|
||||
|
var keventWaitTime = durationToTimespec(100 * time.Millisecond) |
||||
|
|
||||
|
// addWatch adds name to the watched file set.
|
||||
|
// The flags are interpreted as described in kevent(2).
|
||||
|
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
|
||||
|
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { |
||||
|
var isDir bool |
||||
|
// Make ./name and name equivalent
|
||||
|
name = filepath.Clean(name) |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
if w.isClosed { |
||||
|
w.mu.Unlock() |
||||
|
return "", errors.New("kevent instance already closed") |
||||
|
} |
||||
|
watchfd, alreadyWatching := w.watches[name] |
||||
|
// We already have a watch, but we can still override flags.
|
||||
|
if alreadyWatching { |
||||
|
isDir = w.paths[watchfd].isDir |
||||
|
} |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
if !alreadyWatching { |
||||
|
fi, err := os.Lstat(name) |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
// Don't watch sockets.
|
||||
|
if fi.Mode()&os.ModeSocket == os.ModeSocket { |
||||
|
return "", nil |
||||
|
} |
||||
|
|
||||
|
// Don't watch named pipes.
|
||||
|
if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe { |
||||
|
return "", nil |
||||
|
} |
||||
|
|
||||
|
// Follow Symlinks
|
||||
|
// Unfortunately, Linux can add bogus symlinks to watch list without
|
||||
|
// issue, and Windows can't do symlinks period (AFAIK). To maintain
|
||||
|
// consistency, we will act like everything is fine. There will simply
|
||||
|
// be no file events for broken symlinks.
|
||||
|
// Hence the returns of nil on errors.
|
||||
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
||||
|
name, err = filepath.EvalSymlinks(name) |
||||
|
if err != nil { |
||||
|
return "", nil |
||||
|
} |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
_, alreadyWatching = w.watches[name] |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
if alreadyWatching { |
||||
|
return name, nil |
||||
|
} |
||||
|
|
||||
|
fi, err = os.Lstat(name) |
||||
|
if err != nil { |
||||
|
return "", nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
watchfd, err = unix.Open(name, openMode, 0700) |
||||
|
if watchfd == -1 { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
isDir = fi.IsDir() |
||||
|
} |
||||
|
|
||||
|
const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE |
||||
|
if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil { |
||||
|
unix.Close(watchfd) |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
if !alreadyWatching { |
||||
|
w.mu.Lock() |
||||
|
w.watches[name] = watchfd |
||||
|
w.paths[watchfd] = pathInfo{name: name, isDir: isDir} |
||||
|
w.mu.Unlock() |
||||
|
} |
||||
|
|
||||
|
if isDir { |
||||
|
// Watch the directory if it has not been watched before,
|
||||
|
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
||||
|
w.mu.Lock() |
||||
|
|
||||
|
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && |
||||
|
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) |
||||
|
// Store flags so this watch can be updated later
|
||||
|
w.dirFlags[name] = flags |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
if watchDir { |
||||
|
if err := w.watchDirectoryFiles(name); err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return name, nil |
||||
|
} |
||||
|
|
||||
|
// readEvents reads from kqueue and converts the received kevents into
|
||||
|
// Event values that it sends down the Events channel.
|
||||
|
func (w *Watcher) readEvents() { |
||||
|
eventBuffer := make([]unix.Kevent_t, 10) |
||||
|
|
||||
|
loop: |
||||
|
for { |
||||
|
// See if there is a message on the "done" channel
|
||||
|
select { |
||||
|
case <-w.done: |
||||
|
break loop |
||||
|
default: |
||||
|
} |
||||
|
|
||||
|
// Get new events
|
||||
|
kevents, err := read(w.kq, eventBuffer, &keventWaitTime) |
||||
|
// EINTR is okay, the syscall was interrupted before timeout expired.
|
||||
|
if err != nil && err != unix.EINTR { |
||||
|
select { |
||||
|
case w.Errors <- err: |
||||
|
case <-w.done: |
||||
|
break loop |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Flush the events we received to the Events channel
|
||||
|
for len(kevents) > 0 { |
||||
|
kevent := &kevents[0] |
||||
|
watchfd := int(kevent.Ident) |
||||
|
mask := uint32(kevent.Fflags) |
||||
|
w.mu.Lock() |
||||
|
path := w.paths[watchfd] |
||||
|
w.mu.Unlock() |
||||
|
event := newEvent(path.name, mask) |
||||
|
|
||||
|
if path.isDir && !(event.Op&Remove == Remove) { |
||||
|
// Double check to make sure the directory exists. This can happen when
|
||||
|
// we do a rm -fr on a recursively watched folders and we receive a
|
||||
|
// modification event first but the folder has been deleted and later
|
||||
|
// receive the delete event
|
||||
|
if _, err := os.Lstat(event.Name); os.IsNotExist(err) { |
||||
|
// mark is as delete event
|
||||
|
event.Op |= Remove |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if event.Op&Rename == Rename || event.Op&Remove == Remove { |
||||
|
w.Remove(event.Name) |
||||
|
w.mu.Lock() |
||||
|
delete(w.fileExists, event.Name) |
||||
|
w.mu.Unlock() |
||||
|
} |
||||
|
|
||||
|
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { |
||||
|
w.sendDirectoryChangeEvents(event.Name) |
||||
|
} else { |
||||
|
// Send the event on the Events channel.
|
||||
|
select { |
||||
|
case w.Events <- event: |
||||
|
case <-w.done: |
||||
|
break loop |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if event.Op&Remove == Remove { |
||||
|
// Look for a file that may have overwritten this.
|
||||
|
// For example, mv f1 f2 will delete f2, then create f2.
|
||||
|
if path.isDir { |
||||
|
fileDir := filepath.Clean(event.Name) |
||||
|
w.mu.Lock() |
||||
|
_, found := w.watches[fileDir] |
||||
|
w.mu.Unlock() |
||||
|
if found { |
||||
|
// make sure the directory exists before we watch for changes. When we
|
||||
|
// do a recursive watch and perform rm -fr, the parent directory might
|
||||
|
// have gone missing, ignore the missing directory and let the
|
||||
|
// upcoming delete event remove the watch from the parent directory.
|
||||
|
if _, err := os.Lstat(fileDir); err == nil { |
||||
|
w.sendDirectoryChangeEvents(fileDir) |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
filePath := filepath.Clean(event.Name) |
||||
|
if fileInfo, err := os.Lstat(filePath); err == nil { |
||||
|
w.sendFileCreatedEventIfNew(filePath, fileInfo) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Move to next event
|
||||
|
kevents = kevents[1:] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// cleanup
|
||||
|
err := unix.Close(w.kq) |
||||
|
if err != nil { |
||||
|
// only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors.
|
||||
|
select { |
||||
|
case w.Errors <- err: |
||||
|
default: |
||||
|
} |
||||
|
} |
||||
|
close(w.Events) |
||||
|
close(w.Errors) |
||||
|
} |
||||
|
|
||||
|
// newEvent returns an platform-independent Event based on kqueue Fflags.
|
||||
|
func newEvent(name string, mask uint32) Event { |
||||
|
e := Event{Name: name} |
||||
|
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { |
||||
|
e.Op |= Remove |
||||
|
} |
||||
|
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { |
||||
|
e.Op |= Write |
||||
|
} |
||||
|
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { |
||||
|
e.Op |= Rename |
||||
|
} |
||||
|
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { |
||||
|
e.Op |= Chmod |
||||
|
} |
||||
|
return e |
||||
|
} |
||||
|
|
||||
|
func newCreateEvent(name string) Event { |
||||
|
return Event{Name: name, Op: Create} |
||||
|
} |
||||
|
|
||||
|
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
||||
|
func (w *Watcher) watchDirectoryFiles(dirPath string) error { |
||||
|
// Get all files
|
||||
|
files, err := ioutil.ReadDir(dirPath) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
for _, fileInfo := range files { |
||||
|
filePath := filepath.Join(dirPath, fileInfo.Name()) |
||||
|
filePath, err = w.internalWatch(filePath, fileInfo) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
w.fileExists[filePath] = true |
||||
|
w.mu.Unlock() |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// sendDirectoryEvents searches the directory for newly created files
|
||||
|
// and sends them over the event channel. This functionality is to have
|
||||
|
// the BSD version of fsnotify match Linux inotify which provides a
|
||||
|
// create event for files created in a watched directory.
|
||||
|
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { |
||||
|
// Get all files
|
||||
|
files, err := ioutil.ReadDir(dirPath) |
||||
|
if err != nil { |
||||
|
select { |
||||
|
case w.Errors <- err: |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Search for new files
|
||||
|
for _, fileInfo := range files { |
||||
|
filePath := filepath.Join(dirPath, fileInfo.Name()) |
||||
|
err := w.sendFileCreatedEventIfNew(filePath, fileInfo) |
||||
|
|
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
||||
|
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { |
||||
|
w.mu.Lock() |
||||
|
_, doesExist := w.fileExists[filePath] |
||||
|
w.mu.Unlock() |
||||
|
if !doesExist { |
||||
|
// Send create event
|
||||
|
select { |
||||
|
case w.Events <- newCreateEvent(filePath): |
||||
|
case <-w.done: |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// like watchDirectoryFiles (but without doing another ReadDir)
|
||||
|
filePath, err = w.internalWatch(filePath, fileInfo) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
w.mu.Lock() |
||||
|
w.fileExists[filePath] = true |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { |
||||
|
if fileInfo.IsDir() { |
||||
|
// mimic Linux providing delete events for subdirectories
|
||||
|
// but preserve the flags used if currently watching subdirectory
|
||||
|
w.mu.Lock() |
||||
|
flags := w.dirFlags[name] |
||||
|
w.mu.Unlock() |
||||
|
|
||||
|
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME |
||||
|
return w.addWatch(name, flags) |
||||
|
} |
||||
|
|
||||
|
// watch file to mimic Linux inotify
|
||||
|
return w.addWatch(name, noteAllEvents) |
||||
|
} |
||||
|
|
||||
|
// kqueue creates a new kernel event queue and returns a descriptor.
|
||||
|
func kqueue() (kq int, err error) { |
||||
|
kq, err = unix.Kqueue() |
||||
|
if kq == -1 { |
||||
|
return kq, err |
||||
|
} |
||||
|
return kq, nil |
||||
|
} |
||||
|
|
||||
|
// register events with the queue
|
||||
|
func register(kq int, fds []int, flags int, fflags uint32) error { |
||||
|
changes := make([]unix.Kevent_t, len(fds)) |
||||
|
|
||||
|
for i, fd := range fds { |
||||
|
// SetKevent converts int to the platform-specific types:
|
||||
|
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) |
||||
|
changes[i].Fflags = fflags |
||||
|
} |
||||
|
|
||||
|
// register the events
|
||||
|
success, err := unix.Kevent(kq, changes, nil, nil) |
||||
|
if success == -1 { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// read retrieves pending events, or waits until an event occurs.
|
||||
|
// A timeout of nil blocks indefinitely, while 0 polls the queue.
|
||||
|
func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) { |
||||
|
n, err := unix.Kevent(kq, nil, events, timeout) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
return events[0:n], nil |
||||
|
} |
||||
|
|
||||
|
// durationToTimespec prepares a timeout value
|
||||
|
func durationToTimespec(d time.Duration) unix.Timespec { |
||||
|
return unix.NsecToTimespec(d.Nanoseconds()) |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build freebsd openbsd netbsd dragonfly
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import "golang.org/x/sys/unix" |
||||
|
|
||||
|
const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC |
@ -0,0 +1,12 @@ |
|||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build darwin
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import "golang.org/x/sys/unix" |
||||
|
|
||||
|
// note: this constant is not defined on BSD
|
||||
|
const openMode = unix.O_EVTONLY | unix.O_CLOEXEC |
@ -0,0 +1,561 @@ |
|||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
|
||||
|
// +build windows
|
||||
|
|
||||
|
package fsnotify |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"runtime" |
||||
|
"sync" |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Watcher watches a set of files, delivering events to a channel.
|
||||
|
type Watcher struct { |
||||
|
Events chan Event |
||||
|
Errors chan error |
||||
|
isClosed bool // Set to true when Close() is first called
|
||||
|
mu sync.Mutex // Map access
|
||||
|
port syscall.Handle // Handle to completion port
|
||||
|
watches watchMap // Map of watches (key: i-number)
|
||||
|
input chan *input // Inputs to the reader are sent on this channel
|
||||
|
quit chan chan<- error |
||||
|
} |
||||
|
|
||||
|
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
|
func NewWatcher() (*Watcher, error) { |
||||
|
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
||||
|
if e != nil { |
||||
|
return nil, os.NewSyscallError("CreateIoCompletionPort", e) |
||||
|
} |
||||
|
w := &Watcher{ |
||||
|
port: port, |
||||
|
watches: make(watchMap), |
||||
|
input: make(chan *input, 1), |
||||
|
Events: make(chan Event, 50), |
||||
|
Errors: make(chan error), |
||||
|
quit: make(chan chan<- error, 1), |
||||
|
} |
||||
|
go w.readEvents() |
||||
|
return w, nil |
||||
|
} |
||||
|
|
||||
|
// Close removes all watches and closes the events channel.
|
||||
|
func (w *Watcher) Close() error { |
||||
|
if w.isClosed { |
||||
|
return nil |
||||
|
} |
||||
|
w.isClosed = true |
||||
|
|
||||
|
// Send "quit" message to the reader goroutine
|
||||
|
ch := make(chan error) |
||||
|
w.quit <- ch |
||||
|
if err := w.wakeupReader(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return <-ch |
||||
|
} |
||||
|
|
||||
|
// Add starts watching the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Add(name string) error { |
||||
|
if w.isClosed { |
||||
|
return errors.New("watcher already closed") |
||||
|
} |
||||
|
in := &input{ |
||||
|
op: opAddWatch, |
||||
|
path: filepath.Clean(name), |
||||
|
flags: sysFSALLEVENTS, |
||||
|
reply: make(chan error), |
||||
|
} |
||||
|
w.input <- in |
||||
|
if err := w.wakeupReader(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return <-in.reply |
||||
|
} |
||||
|
|
||||
|
// Remove stops watching the the named file or directory (non-recursively).
|
||||
|
func (w *Watcher) Remove(name string) error { |
||||
|
in := &input{ |
||||
|
op: opRemoveWatch, |
||||
|
path: filepath.Clean(name), |
||||
|
reply: make(chan error), |
||||
|
} |
||||
|
w.input <- in |
||||
|
if err := w.wakeupReader(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return <-in.reply |
||||
|
} |
||||
|
|
||||
|
const ( |
||||
|
// Options for AddWatch
|
||||
|
sysFSONESHOT = 0x80000000 |
||||
|
sysFSONLYDIR = 0x1000000 |
||||
|
|
||||
|
// Events
|
||||
|
sysFSACCESS = 0x1 |
||||
|
sysFSALLEVENTS = 0xfff |
||||
|
sysFSATTRIB = 0x4 |
||||
|
sysFSCLOSE = 0x18 |
||||
|
sysFSCREATE = 0x100 |
||||
|
sysFSDELETE = 0x200 |
||||
|
sysFSDELETESELF = 0x400 |
||||
|
sysFSMODIFY = 0x2 |
||||
|
sysFSMOVE = 0xc0 |
||||
|
sysFSMOVEDFROM = 0x40 |
||||
|
sysFSMOVEDTO = 0x80 |
||||
|
sysFSMOVESELF = 0x800 |
||||
|
|
||||
|
// Special events
|
||||
|
sysFSIGNORED = 0x8000 |
||||
|
sysFSQOVERFLOW = 0x4000 |
||||
|
) |
||||
|
|
||||
|
func newEvent(name string, mask uint32) Event { |
||||
|
e := Event{Name: name} |
||||
|
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { |
||||
|
e.Op |= Create |
||||
|
} |
||||
|
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { |
||||
|
e.Op |= Remove |
||||
|
} |
||||
|
if mask&sysFSMODIFY == sysFSMODIFY { |
||||
|
e.Op |= Write |
||||
|
} |
||||
|
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { |
||||
|
e.Op |= Rename |
||||
|
} |
||||
|
if mask&sysFSATTRIB == sysFSATTRIB { |
||||
|
e.Op |= Chmod |
||||
|
} |
||||
|
return e |
||||
|
} |
||||
|
|
||||
|
const ( |
||||
|
opAddWatch = iota |
||||
|
opRemoveWatch |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
provisional uint64 = 1 << (32 + iota) |
||||
|
) |
||||
|
|
||||
|
type input struct { |
||||
|
op int |
||||
|
path string |
||||
|
flags uint32 |
||||
|
reply chan error |
||||
|
} |
||||
|
|
||||
|
type inode struct { |
||||
|
handle syscall.Handle |
||||
|
volume uint32 |
||||
|
index uint64 |
||||
|
} |
||||
|
|
||||
|
type watch struct { |
||||
|
ov syscall.Overlapped |
||||
|
ino *inode // i-number
|
||||
|
path string // Directory path
|
||||
|
mask uint64 // Directory itself is being watched with these notify flags
|
||||
|
names map[string]uint64 // Map of names being watched and their notify flags
|
||||
|
rename string // Remembers the old name while renaming a file
|
||||
|
buf [4096]byte |
||||
|
} |
||||
|
|
||||
|
type indexMap map[uint64]*watch |
||||
|
type watchMap map[uint32]indexMap |
||||
|
|
||||
|
func (w *Watcher) wakeupReader() error { |
||||
|
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
||||
|
if e != nil { |
||||
|
return os.NewSyscallError("PostQueuedCompletionStatus", e) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func getDir(pathname string) (dir string, err error) { |
||||
|
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) |
||||
|
if e != nil { |
||||
|
return "", os.NewSyscallError("GetFileAttributes", e) |
||||
|
} |
||||
|
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { |
||||
|
dir = pathname |
||||
|
} else { |
||||
|
dir, _ = filepath.Split(pathname) |
||||
|
dir = filepath.Clean(dir) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func getIno(path string) (ino *inode, err error) { |
||||
|
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), |
||||
|
syscall.FILE_LIST_DIRECTORY, |
||||
|
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
||||
|
nil, syscall.OPEN_EXISTING, |
||||
|
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) |
||||
|
if e != nil { |
||||
|
return nil, os.NewSyscallError("CreateFile", e) |
||||
|
} |
||||
|
var fi syscall.ByHandleFileInformation |
||||
|
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { |
||||
|
syscall.CloseHandle(h) |
||||
|
return nil, os.NewSyscallError("GetFileInformationByHandle", e) |
||||
|
} |
||||
|
ino = &inode{ |
||||
|
handle: h, |
||||
|
volume: fi.VolumeSerialNumber, |
||||
|
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
||||
|
} |
||||
|
return ino, nil |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (m watchMap) get(ino *inode) *watch { |
||||
|
if i := m[ino.volume]; i != nil { |
||||
|
return i[ino.index] |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (m watchMap) set(ino *inode, watch *watch) { |
||||
|
i := m[ino.volume] |
||||
|
if i == nil { |
||||
|
i = make(indexMap) |
||||
|
m[ino.volume] = i |
||||
|
} |
||||
|
i[ino.index] = watch |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (w *Watcher) addWatch(pathname string, flags uint64) error { |
||||
|
dir, err := getDir(pathname) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if flags&sysFSONLYDIR != 0 && pathname != dir { |
||||
|
return nil |
||||
|
} |
||||
|
ino, err := getIno(dir) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
w.mu.Lock() |
||||
|
watchEntry := w.watches.get(ino) |
||||
|
w.mu.Unlock() |
||||
|
if watchEntry == nil { |
||||
|
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { |
||||
|
syscall.CloseHandle(ino.handle) |
||||
|
return os.NewSyscallError("CreateIoCompletionPort", e) |
||||
|
} |
||||
|
watchEntry = &watch{ |
||||
|
ino: ino, |
||||
|
path: dir, |
||||
|
names: make(map[string]uint64), |
||||
|
} |
||||
|
w.mu.Lock() |
||||
|
w.watches.set(ino, watchEntry) |
||||
|
w.mu.Unlock() |
||||
|
flags |= provisional |
||||
|
} else { |
||||
|
syscall.CloseHandle(ino.handle) |
||||
|
} |
||||
|
if pathname == dir { |
||||
|
watchEntry.mask |= flags |
||||
|
} else { |
||||
|
watchEntry.names[filepath.Base(pathname)] |= flags |
||||
|
} |
||||
|
if err = w.startRead(watchEntry); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if pathname == dir { |
||||
|
watchEntry.mask &= ^provisional |
||||
|
} else { |
||||
|
watchEntry.names[filepath.Base(pathname)] &= ^provisional |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (w *Watcher) remWatch(pathname string) error { |
||||
|
dir, err := getDir(pathname) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
ino, err := getIno(dir) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
w.mu.Lock() |
||||
|
watch := w.watches.get(ino) |
||||
|
w.mu.Unlock() |
||||
|
if watch == nil { |
||||
|
return fmt.Errorf("can't remove non-existent watch for: %s", pathname) |
||||
|
} |
||||
|
if pathname == dir { |
||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
||||
|
watch.mask = 0 |
||||
|
} else { |
||||
|
name := filepath.Base(pathname) |
||||
|
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) |
||||
|
delete(watch.names, name) |
||||
|
} |
||||
|
return w.startRead(watch) |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (w *Watcher) deleteWatch(watch *watch) { |
||||
|
for name, mask := range watch.names { |
||||
|
if mask&provisional == 0 { |
||||
|
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) |
||||
|
} |
||||
|
delete(watch.names, name) |
||||
|
} |
||||
|
if watch.mask != 0 { |
||||
|
if watch.mask&provisional == 0 { |
||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
||||
|
} |
||||
|
watch.mask = 0 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Must run within the I/O thread.
|
||||
|
func (w *Watcher) startRead(watch *watch) error { |
||||
|
if e := syscall.CancelIo(watch.ino.handle); e != nil { |
||||
|
w.Errors <- os.NewSyscallError("CancelIo", e) |
||||
|
w.deleteWatch(watch) |
||||
|
} |
||||
|
mask := toWindowsFlags(watch.mask) |
||||
|
for _, m := range watch.names { |
||||
|
mask |= toWindowsFlags(m) |
||||
|
} |
||||
|
if mask == 0 { |
||||
|
if e := syscall.CloseHandle(watch.ino.handle); e != nil { |
||||
|
w.Errors <- os.NewSyscallError("CloseHandle", e) |
||||
|
} |
||||
|
w.mu.Lock() |
||||
|
delete(w.watches[watch.ino.volume], watch.ino.index) |
||||
|
w.mu.Unlock() |
||||
|
return nil |
||||
|
} |
||||
|
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
||||
|
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
||||
|
if e != nil { |
||||
|
err := os.NewSyscallError("ReadDirectoryChanges", e) |
||||
|
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { |
||||
|
// Watched directory was probably removed
|
||||
|
if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) { |
||||
|
if watch.mask&sysFSONESHOT != 0 { |
||||
|
watch.mask = 0 |
||||
|
} |
||||
|
} |
||||
|
err = nil |
||||
|
} |
||||
|
w.deleteWatch(watch) |
||||
|
w.startRead(watch) |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// readEvents reads from the I/O completion port, converts the
|
||||
|
// received events into Event objects and sends them via the Events channel.
|
||||
|
// Entry point to the I/O thread.
|
||||
|
func (w *Watcher) readEvents() { |
||||
|
var ( |
||||
|
n, key uint32 |
||||
|
ov *syscall.Overlapped |
||||
|
) |
||||
|
runtime.LockOSThread() |
||||
|
|
||||
|
for { |
||||
|
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) |
||||
|
watch := (*watch)(unsafe.Pointer(ov)) |
||||
|
|
||||
|
if watch == nil { |
||||
|
select { |
||||
|
case ch := <-w.quit: |
||||
|
w.mu.Lock() |
||||
|
var indexes []indexMap |
||||
|
for _, index := range w.watches { |
||||
|
indexes = append(indexes, index) |
||||
|
} |
||||
|
w.mu.Unlock() |
||||
|
for _, index := range indexes { |
||||
|
for _, watch := range index { |
||||
|
w.deleteWatch(watch) |
||||
|
w.startRead(watch) |
||||
|
} |
||||
|
} |
||||
|
var err error |
||||
|
if e := syscall.CloseHandle(w.port); e != nil { |
||||
|
err = os.NewSyscallError("CloseHandle", e) |
||||
|
} |
||||
|
close(w.Events) |
||||
|
close(w.Errors) |
||||
|
ch <- err |
||||
|
return |
||||
|
case in := <-w.input: |
||||
|
switch in.op { |
||||
|
case opAddWatch: |
||||
|
in.reply <- w.addWatch(in.path, uint64(in.flags)) |
||||
|
case opRemoveWatch: |
||||
|
in.reply <- w.remWatch(in.path) |
||||
|
} |
||||
|
default: |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
switch e { |
||||
|
case syscall.ERROR_MORE_DATA: |
||||
|
if watch == nil { |
||||
|
w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") |
||||
|
} else { |
||||
|
// The i/o succeeded but the buffer is full.
|
||||
|
// In theory we should be building up a full packet.
|
||||
|
// In practice we can get away with just carrying on.
|
||||
|
n = uint32(unsafe.Sizeof(watch.buf)) |
||||
|
} |
||||
|
case syscall.ERROR_ACCESS_DENIED: |
||||
|
// Watched directory was probably removed
|
||||
|
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) |
||||
|
w.deleteWatch(watch) |
||||
|
w.startRead(watch) |
||||
|
continue |
||||
|
case syscall.ERROR_OPERATION_ABORTED: |
||||
|
// CancelIo was called on this handle
|
||||
|
continue |
||||
|
default: |
||||
|
w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) |
||||
|
continue |
||||
|
case nil: |
||||
|
} |
||||
|
|
||||
|
var offset uint32 |
||||
|
for { |
||||
|
if n == 0 { |
||||
|
w.Events <- newEvent("", sysFSQOVERFLOW) |
||||
|
w.Errors <- errors.New("short read in readEvents()") |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
// Point "raw" to the event in the buffer
|
||||
|
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
||||
|
buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) |
||||
|
name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) |
||||
|
fullname := filepath.Join(watch.path, name) |
||||
|
|
||||
|
var mask uint64 |
||||
|
switch raw.Action { |
||||
|
case syscall.FILE_ACTION_REMOVED: |
||||
|
mask = sysFSDELETESELF |
||||
|
case syscall.FILE_ACTION_MODIFIED: |
||||
|
mask = sysFSMODIFY |
||||
|
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
||||
|
watch.rename = name |
||||
|
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
||||
|
if watch.names[watch.rename] != 0 { |
||||
|
watch.names[name] |= watch.names[watch.rename] |
||||
|
delete(watch.names, watch.rename) |
||||
|
mask = sysFSMOVESELF |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
sendNameEvent := func() { |
||||
|
if w.sendEvent(fullname, watch.names[name]&mask) { |
||||
|
if watch.names[name]&sysFSONESHOT != 0 { |
||||
|
delete(watch.names, name) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { |
||||
|
sendNameEvent() |
||||
|
} |
||||
|
if raw.Action == syscall.FILE_ACTION_REMOVED { |
||||
|
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) |
||||
|
delete(watch.names, name) |
||||
|
} |
||||
|
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { |
||||
|
if watch.mask&sysFSONESHOT != 0 { |
||||
|
watch.mask = 0 |
||||
|
} |
||||
|
} |
||||
|
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { |
||||
|
fullname = filepath.Join(watch.path, watch.rename) |
||||
|
sendNameEvent() |
||||
|
} |
||||
|
|
||||
|
// Move to the next event in the buffer
|
||||
|
if raw.NextEntryOffset == 0 { |
||||
|
break |
||||
|
} |
||||
|
offset += raw.NextEntryOffset |
||||
|
|
||||
|
// Error!
|
||||
|
if offset >= n { |
||||
|
w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if err := w.startRead(watch); err != nil { |
||||
|
w.Errors <- err |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (w *Watcher) sendEvent(name string, mask uint64) bool { |
||||
|
if mask == 0 { |
||||
|
return false |
||||
|
} |
||||
|
event := newEvent(name, uint32(mask)) |
||||
|
select { |
||||
|
case ch := <-w.quit: |
||||
|
w.quit <- ch |
||||
|
case w.Events <- event: |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func toWindowsFlags(mask uint64) uint32 { |
||||
|
var m uint32 |
||||
|
if mask&sysFSACCESS != 0 { |
||||
|
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS |
||||
|
} |
||||
|
if mask&sysFSMODIFY != 0 { |
||||
|
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE |
||||
|
} |
||||
|
if mask&sysFSATTRIB != 0 { |
||||
|
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES |
||||
|
} |
||||
|
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { |
||||
|
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME |
||||
|
} |
||||
|
return m |
||||
|
} |
||||
|
|
||||
|
func toFSnotifyFlags(action uint32) uint64 { |
||||
|
switch action { |
||||
|
case syscall.FILE_ACTION_ADDED: |
||||
|
return sysFSCREATE |
||||
|
case syscall.FILE_ACTION_REMOVED: |
||||
|
return sysFSDELETE |
||||
|
case syscall.FILE_ACTION_MODIFIED: |
||||
|
return sysFSMODIFY |
||||
|
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
||||
|
return sysFSMOVEDFROM |
||||
|
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
||||
|
return sysFSMOVEDTO |
||||
|
} |
||||
|
return 0 |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
.idea/ |
||||
|
.test/ |
||||
|
examples/_* |
@ -0,0 +1,56 @@ |
|||||
|
# Version v1.4.7-v1.4.8 |
||||
|
* Documentation updates. |
||||
|
* Small linter cleanups. |
||||
|
* Added example in test. |
||||
|
|
||||
|
# Version v1.4.6 |
||||
|
|
||||
|
* Document the usage of Cleanup when re-reading a file (thanks to @lesovsky) for issue #18. |
||||
|
* Add example directories with example and tests for issues. |
||||
|
|
||||
|
# Version v1.4.4-v1.4.5 |
||||
|
|
||||
|
* Fix of checksum problem because of forced tag. No changes to the code. |
||||
|
|
||||
|
# Version v1.4.1 |
||||
|
|
||||
|
* Incorporated PR 162 by by Mohammed902: "Simplify non-Windows build tag". |
||||
|
|
||||
|
# Version v1.4.0 |
||||
|
|
||||
|
* Incorporated PR 9 by mschneider82: "Added seekinfo to Tail". |
||||
|
|
||||
|
# Version v1.3.1 |
||||
|
|
||||
|
* Incorporated PR 7: "Fix deadlock when stopping on non-empty file/buffer", |
||||
|
fixes upstream issue 93. |
||||
|
|
||||
|
|
||||
|
# Version v1.3.0 |
||||
|
|
||||
|
* Incorporated changes of unmerged upstream PR 149 by mezzi: "added line num |
||||
|
to Line struct". |
||||
|
|
||||
|
# Version v1.2.1 |
||||
|
|
||||
|
* Incorporated changes of unmerged upstream PR 128 by jadekler: "Compile-able |
||||
|
code in readme". |
||||
|
* Incorporated changes of unmerged upstream PR 130 by fgeller: "small change |
||||
|
to comment wording". |
||||
|
* Incorporated changes of unmerged upstream PR 133 by sm3142: "removed |
||||
|
spurious newlines from log messages". |
||||
|
|
||||
|
# Version v1.2.0 |
||||
|
|
||||
|
* Incorporated changes of unmerged upstream PR 126 by Code-Hex: "Solved the |
||||
|
problem for never return the last line if it's not followed by a newline". |
||||
|
* Incorporated changes of unmerged upstream PR 131 by StoicPerlman: "Remove |
||||
|
deprecated os.SEEK consts". The changes bumped the minimal supported Go |
||||
|
release to 1.9. |
||||
|
|
||||
|
# Version v1.1.0 |
||||
|
|
||||
|
* migration to go modules. |
||||
|
* release of master branch of the dormant upstream, because it contains |
||||
|
fixes and improvement no present in the tagged release. |
||||
|
|
@ -0,0 +1,19 @@ |
|||||
|
FROM golang |
||||
|
|
||||
|
RUN mkdir -p $GOPATH/src/github.com/nxadm/tail/ |
||||
|
ADD . $GOPATH/src/github.com/nxadm/tail/ |
||||
|
|
||||
|
# expecting to fetch dependencies successfully. |
||||
|
RUN go get -v github.com/nxadm/tail |
||||
|
|
||||
|
# expecting to run the test successfully. |
||||
|
RUN go test -v github.com/nxadm/tail |
||||
|
|
||||
|
# expecting to install successfully |
||||
|
RUN go install -v github.com/nxadm/tail |
||||
|
RUN go install -v github.com/nxadm/tail/cmd/gotail |
||||
|
|
||||
|
RUN $GOPATH/bin/gotail -h || true |
||||
|
|
||||
|
ENV PATH $GOPATH/bin:$PATH |
||||
|
CMD ["gotail"] |
@ -0,0 +1,21 @@ |
|||||
|
# The MIT License (MIT) |
||||
|
|
||||
|
# © Copyright 2015 Hewlett Packard Enterprise Development LP |
||||
|
Copyright (c) 2014 ActiveState |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
@ -0,0 +1,44 @@ |
|||||
|
![ci](https://github.com/nxadm/tail/workflows/ci/badge.svg)[![Go Reference](https://pkg.go.dev/badge/github.com/nxadm/tail.svg)](https://pkg.go.dev/github.com/nxadm/tail) |
||||
|
|
||||
|
# tail functionality in Go |
||||
|
|
||||
|
nxadm/tail provides a Go library that emulates the features of the BSD `tail` |
||||
|
program. The library comes with full support for truncation/move detection as |
||||
|
it is designed to work with log rotation tools. The library works on all |
||||
|
operating systems supported by Go, including POSIX systems like Linux and |
||||
|
*BSD, and MS Windows. Go 1.9 is the oldest compiler release supported. |
||||
|
|
||||
|
A simple example: |
||||
|
|
||||
|
```Go |
||||
|
// Create a tail |
||||
|
t, err := tail.TailFile( |
||||
|
"/var/log/nginx.log", tail.Config{Follow: true, ReOpen: true}) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
|
||||
|
// Print the text of each received line |
||||
|
for line := range t.Lines { |
||||
|
fmt.Println(line.Text) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
See [API documentation](https://pkg.go.dev/github.com/nxadm/tail). |
||||
|
|
||||
|
## Installing |
||||
|
|
||||
|
go get github.com/nxadm/tail/... |
||||
|
|
||||
|
## History |
||||
|
|
||||
|
This project is an active, drop-in replacement for the |
||||
|
[abandoned](https://en.wikipedia.org/wiki/HPE_Helion) Go tail library at |
||||
|
[hpcloud](https://github.com/hpcloud/tail). Next to |
||||
|
[addressing open issues/PRs of the original project](https://github.com/nxadm/tail/issues/6), |
||||
|
nxadm/tail continues the development by keeping up to date with the Go toolchain |
||||
|
(e.g. go modules) and dependencies, completing the documentation, adding features |
||||
|
and fixing bugs. |
||||
|
|
||||
|
## Examples |
||||
|
Examples, e.g. used to debug an issue, are kept in the [examples directory](/examples). |
@ -0,0 +1,8 @@ |
|||||
|
module github.com/nxadm/tail |
||||
|
|
||||
|
go 1.13 |
||||
|
|
||||
|
require ( |
||||
|
github.com/fsnotify/fsnotify v1.4.9 |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 |
||||
|
) |
@ -0,0 +1,6 @@ |
|||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= |
||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= |
||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
@ -0,0 +1,7 @@ |
|||||
|
Copyright (C) 2013 99designs |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,97 @@ |
|||||
|
// Package ratelimiter implements the Leaky Bucket ratelimiting algorithm with memcached and in-memory backends.
|
||||
|
package ratelimiter |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type LeakyBucket struct { |
||||
|
Size uint16 |
||||
|
Fill float64 |
||||
|
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
|
||||
|
Lastupdate time.Time |
||||
|
Now func() time.Time |
||||
|
} |
||||
|
|
||||
|
func NewLeakyBucket(size uint16, leakInterval time.Duration) *LeakyBucket { |
||||
|
bucket := LeakyBucket{ |
||||
|
Size: size, |
||||
|
Fill: 0, |
||||
|
LeakInterval: leakInterval, |
||||
|
Now: time.Now, |
||||
|
Lastupdate: time.Now(), |
||||
|
} |
||||
|
|
||||
|
return &bucket |
||||
|
} |
||||
|
|
||||
|
func (b *LeakyBucket) updateFill() { |
||||
|
now := b.Now() |
||||
|
if b.Fill > 0 { |
||||
|
elapsed := now.Sub(b.Lastupdate) |
||||
|
|
||||
|
b.Fill -= float64(elapsed) / float64(b.LeakInterval) |
||||
|
if b.Fill < 0 { |
||||
|
b.Fill = 0 |
||||
|
} |
||||
|
} |
||||
|
b.Lastupdate = now |
||||
|
} |
||||
|
|
||||
|
func (b *LeakyBucket) Pour(amount uint16) bool { |
||||
|
b.updateFill() |
||||
|
|
||||
|
var newfill float64 = b.Fill + float64(amount) |
||||
|
|
||||
|
if newfill > float64(b.Size) { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
b.Fill = newfill |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// The time at which this bucket will be completely drained
|
||||
|
func (b *LeakyBucket) DrainedAt() time.Time { |
||||
|
return b.Lastupdate.Add(time.Duration(b.Fill * float64(b.LeakInterval))) |
||||
|
} |
||||
|
|
||||
|
// The duration until this bucket is completely drained
|
||||
|
func (b *LeakyBucket) TimeToDrain() time.Duration { |
||||
|
return b.DrainedAt().Sub(b.Now()) |
||||
|
} |
||||
|
|
||||
|
func (b *LeakyBucket) TimeSinceLastUpdate() time.Duration { |
||||
|
return b.Now().Sub(b.Lastupdate) |
||||
|
} |
||||
|
|
||||
|
type LeakyBucketSer struct { |
||||
|
Size uint16 |
||||
|
Fill float64 |
||||
|
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
|
||||
|
Lastupdate time.Time |
||||
|
} |
||||
|
|
||||
|
func (b *LeakyBucket) Serialise() *LeakyBucketSer { |
||||
|
bucket := LeakyBucketSer{ |
||||
|
Size: b.Size, |
||||
|
Fill: b.Fill, |
||||
|
LeakInterval: b.LeakInterval, |
||||
|
Lastupdate: b.Lastupdate, |
||||
|
} |
||||
|
|
||||
|
return &bucket |
||||
|
} |
||||
|
|
||||
|
func (b *LeakyBucketSer) DeSerialise() *LeakyBucket { |
||||
|
bucket := LeakyBucket{ |
||||
|
Size: b.Size, |
||||
|
Fill: b.Fill, |
||||
|
LeakInterval: b.LeakInterval, |
||||
|
Lastupdate: b.Lastupdate, |
||||
|
Now: time.Now, |
||||
|
} |
||||
|
|
||||
|
return &bucket |
||||
|
} |
@ -0,0 +1,60 @@ |
|||||
|
package ratelimiter |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
GC_SIZE int = 100 |
||||
|
GC_PERIOD time.Duration = 60 * time.Second |
||||
|
) |
||||
|
|
||||
|
type Memory struct { |
||||
|
store map[string]LeakyBucket |
||||
|
lastGCCollected time.Time |
||||
|
} |
||||
|
|
||||
|
func NewMemory() *Memory { |
||||
|
m := new(Memory) |
||||
|
m.store = make(map[string]LeakyBucket) |
||||
|
m.lastGCCollected = time.Now() |
||||
|
return m |
||||
|
} |
||||
|
|
||||
|
func (m *Memory) GetBucketFor(key string) (*LeakyBucket, error) { |
||||
|
|
||||
|
bucket, ok := m.store[key] |
||||
|
if !ok { |
||||
|
return nil, errors.New("miss") |
||||
|
} |
||||
|
|
||||
|
return &bucket, nil |
||||
|
} |
||||
|
|
||||
|
func (m *Memory) SetBucketFor(key string, bucket LeakyBucket) error { |
||||
|
|
||||
|
if len(m.store) > GC_SIZE { |
||||
|
m.GarbageCollect() |
||||
|
} |
||||
|
|
||||
|
m.store[key] = bucket |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (m *Memory) GarbageCollect() { |
||||
|
now := time.Now() |
||||
|
|
||||
|
// rate limit GC to once per minute
|
||||
|
if now.Unix() >= m.lastGCCollected.Add(GC_PERIOD).Unix() { |
||||
|
for key, bucket := range m.store { |
||||
|
// if the bucket is drained, then GC
|
||||
|
if bucket.DrainedAt().Unix() < now.Unix() { |
||||
|
delete(m.store, key) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
m.lastGCCollected = now |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
package ratelimiter |
||||
|
|
||||
|
type Storage interface { |
||||
|
GetBucketFor(string) (*LeakyBucket, error) |
||||
|
SetBucketFor(string, LeakyBucket) error |
||||
|
} |
@ -0,0 +1,455 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
//nxadm/tail provides a Go library that emulates the features of the BSD `tail`
|
||||
|
//program. The library comes with full support for truncation/move detection as
|
||||
|
//it is designed to work with log rotation tools. The library works on all
|
||||
|
//operating systems supported by Go, including POSIX systems like Linux and
|
||||
|
//*BSD, and MS Windows. Go 1.9 is the oldest compiler release supported.
|
||||
|
package tail |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"os" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/nxadm/tail/ratelimiter" |
||||
|
"github.com/nxadm/tail/util" |
||||
|
"github.com/nxadm/tail/watch" |
||||
|
"gopkg.in/tomb.v1" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
// ErrStop is returned when the tail of a file has been marked to be stopped.
|
||||
|
ErrStop = errors.New("tail should now stop") |
||||
|
) |
||||
|
|
||||
|
type Line struct { |
||||
|
Text string // The contents of the file
|
||||
|
Num int // The line number
|
||||
|
SeekInfo SeekInfo // SeekInfo
|
||||
|
Time time.Time // Present time
|
||||
|
Err error // Error from tail
|
||||
|
} |
||||
|
|
||||
|
// Deprecated: this function is no longer used internally and it has little of no
|
||||
|
// use in the API. As such, it will be removed from the API in a future major
|
||||
|
// release.
|
||||
|
//
|
||||
|
// NewLine returns a * pointer to a Line struct.
|
||||
|
func NewLine(text string, lineNum int) *Line { |
||||
|
return &Line{text, lineNum, SeekInfo{}, time.Now(), nil} |
||||
|
} |
||||
|
|
||||
|
// SeekInfo represents arguments to io.Seek. See: https://golang.org/pkg/io/#SectionReader.Seek
|
||||
|
type SeekInfo struct { |
||||
|
Offset int64 |
||||
|
Whence int |
||||
|
} |
||||
|
|
||||
|
type logger interface { |
||||
|
Fatal(v ...interface{}) |
||||
|
Fatalf(format string, v ...interface{}) |
||||
|
Fatalln(v ...interface{}) |
||||
|
Panic(v ...interface{}) |
||||
|
Panicf(format string, v ...interface{}) |
||||
|
Panicln(v ...interface{}) |
||||
|
Print(v ...interface{}) |
||||
|
Printf(format string, v ...interface{}) |
||||
|
Println(v ...interface{}) |
||||
|
} |
||||
|
|
||||
|
// Config is used to specify how a file must be tailed.
|
||||
|
type Config struct { |
||||
|
// File-specifc
|
||||
|
Location *SeekInfo // Tail from this location. If nil, start at the beginning of the file
|
||||
|
ReOpen bool // Reopen recreated files (tail -F)
|
||||
|
MustExist bool // Fail early if the file does not exist
|
||||
|
Poll bool // Poll for file changes instead of using the default inotify
|
||||
|
Pipe bool // The file is a named pipe (mkfifo)
|
||||
|
|
||||
|
// Generic IO
|
||||
|
Follow bool // Continue looking for new lines (tail -f)
|
||||
|
MaxLineSize int // If non-zero, split longer lines into multiple lines
|
||||
|
|
||||
|
// Optionally, use a ratelimiter (e.g. created by the ratelimiter/NewLeakyBucket function)
|
||||
|
RateLimiter *ratelimiter.LeakyBucket |
||||
|
|
||||
|
// Optionally use a Logger. When nil, the Logger is set to tail.DefaultLogger.
|
||||
|
// To disable logging, set it to tail.DiscardingLogger
|
||||
|
Logger logger |
||||
|
} |
||||
|
|
||||
|
type Tail struct { |
||||
|
Filename string // The filename
|
||||
|
Lines chan *Line // A consumable channel of *Line
|
||||
|
Config // Tail.Configuration
|
||||
|
|
||||
|
file *os.File |
||||
|
reader *bufio.Reader |
||||
|
lineNum int |
||||
|
|
||||
|
watcher watch.FileWatcher |
||||
|
changes *watch.FileChanges |
||||
|
|
||||
|
tomb.Tomb // provides: Done, Kill, Dying
|
||||
|
|
||||
|
lk sync.Mutex |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
// DefaultLogger logs to os.Stderr and it is used when Config.Logger == nil
|
||||
|
DefaultLogger = log.New(os.Stderr, "", log.LstdFlags) |
||||
|
// DiscardingLogger can be used to disable logging output
|
||||
|
DiscardingLogger = log.New(ioutil.Discard, "", 0) |
||||
|
) |
||||
|
|
||||
|
// TailFile begins tailing the file. And returns a pointer to a Tail struct
|
||||
|
// and an error. An output stream is made available via the Tail.Lines
|
||||
|
// channel (e.g. to be looped and printed). To handle errors during tailing,
|
||||
|
// after finishing reading from the Lines channel, invoke the `Wait` or `Err`
|
||||
|
// method on the returned *Tail.
|
||||
|
func TailFile(filename string, config Config) (*Tail, error) { |
||||
|
if config.ReOpen && !config.Follow { |
||||
|
util.Fatal("cannot set ReOpen without Follow.") |
||||
|
} |
||||
|
|
||||
|
t := &Tail{ |
||||
|
Filename: filename, |
||||
|
Lines: make(chan *Line), |
||||
|
Config: config, |
||||
|
} |
||||
|
|
||||
|
// when Logger was not specified in config, use default logger
|
||||
|
if t.Logger == nil { |
||||
|
t.Logger = DefaultLogger |
||||
|
} |
||||
|
|
||||
|
if t.Poll { |
||||
|
t.watcher = watch.NewPollingFileWatcher(filename) |
||||
|
} else { |
||||
|
t.watcher = watch.NewInotifyFileWatcher(filename) |
||||
|
} |
||||
|
|
||||
|
if t.MustExist { |
||||
|
var err error |
||||
|
t.file, err = OpenFile(t.Filename) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
go t.tailFileSync() |
||||
|
|
||||
|
return t, nil |
||||
|
} |
||||
|
|
||||
|
// Tell returns the file's current position, like stdio's ftell() and an error.
|
||||
|
// Beware that this value may not be completely accurate because one line from
|
||||
|
// the chan(tail.Lines) may have been read already.
|
||||
|
func (tail *Tail) Tell() (offset int64, err error) { |
||||
|
if tail.file == nil { |
||||
|
return |
||||
|
} |
||||
|
offset, err = tail.file.Seek(0, io.SeekCurrent) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
tail.lk.Lock() |
||||
|
defer tail.lk.Unlock() |
||||
|
if tail.reader == nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
offset -= int64(tail.reader.Buffered()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Stop stops the tailing activity.
|
||||
|
func (tail *Tail) Stop() error { |
||||
|
tail.Kill(nil) |
||||
|
return tail.Wait() |
||||
|
} |
||||
|
|
||||
|
// StopAtEOF stops tailing as soon as the end of the file is reached. The function
|
||||
|
// returns an error,
|
||||
|
func (tail *Tail) StopAtEOF() error { |
||||
|
tail.Kill(errStopAtEOF) |
||||
|
return tail.Wait() |
||||
|
} |
||||
|
|
||||
|
var errStopAtEOF = errors.New("tail: stop at eof") |
||||
|
|
||||
|
func (tail *Tail) close() { |
||||
|
close(tail.Lines) |
||||
|
tail.closeFile() |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) closeFile() { |
||||
|
if tail.file != nil { |
||||
|
tail.file.Close() |
||||
|
tail.file = nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) reopen() error { |
||||
|
tail.closeFile() |
||||
|
tail.lineNum = 0 |
||||
|
for { |
||||
|
var err error |
||||
|
tail.file, err = OpenFile(tail.Filename) |
||||
|
if err != nil { |
||||
|
if os.IsNotExist(err) { |
||||
|
tail.Logger.Printf("Waiting for %s to appear...", tail.Filename) |
||||
|
if err := tail.watcher.BlockUntilExists(&tail.Tomb); err != nil { |
||||
|
if err == tomb.ErrDying { |
||||
|
return err |
||||
|
} |
||||
|
return fmt.Errorf("Failed to detect creation of %s: %s", tail.Filename, err) |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
return fmt.Errorf("Unable to open file %s: %s", tail.Filename, err) |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) readLine() (string, error) { |
||||
|
tail.lk.Lock() |
||||
|
line, err := tail.reader.ReadString('\n') |
||||
|
tail.lk.Unlock() |
||||
|
if err != nil { |
||||
|
// Note ReadString "returns the data read before the error" in
|
||||
|
// case of an error, including EOF, so we return it as is. The
|
||||
|
// caller is expected to process it if err is EOF.
|
||||
|
return line, err |
||||
|
} |
||||
|
|
||||
|
line = strings.TrimRight(line, "\n") |
||||
|
|
||||
|
return line, err |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) tailFileSync() { |
||||
|
defer tail.Done() |
||||
|
defer tail.close() |
||||
|
|
||||
|
if !tail.MustExist { |
||||
|
// deferred first open.
|
||||
|
err := tail.reopen() |
||||
|
if err != nil { |
||||
|
if err != tomb.ErrDying { |
||||
|
tail.Kill(err) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Seek to requested location on first open of the file.
|
||||
|
if tail.Location != nil { |
||||
|
_, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence) |
||||
|
if err != nil { |
||||
|
tail.Killf("Seek error on %s: %s", tail.Filename, err) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
tail.openReader() |
||||
|
|
||||
|
// Read line by line.
|
||||
|
for { |
||||
|
// do not seek in named pipes
|
||||
|
if !tail.Pipe { |
||||
|
// grab the position in case we need to back up in the event of a half-line
|
||||
|
if _, err := tail.Tell(); err != nil { |
||||
|
tail.Kill(err) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
line, err := tail.readLine() |
||||
|
|
||||
|
// Process `line` even if err is EOF.
|
||||
|
if err == nil { |
||||
|
cooloff := !tail.sendLine(line) |
||||
|
if cooloff { |
||||
|
// Wait a second before seeking till the end of
|
||||
|
// file when rate limit is reached.
|
||||
|
msg := ("Too much log activity; waiting a second before resuming tailing") |
||||
|
offset, _ := tail.Tell() |
||||
|
tail.Lines <- &Line{msg, tail.lineNum, SeekInfo{Offset: offset}, time.Now(), errors.New(msg)} |
||||
|
select { |
||||
|
case <-time.After(time.Second): |
||||
|
case <-tail.Dying(): |
||||
|
return |
||||
|
} |
||||
|
if err := tail.seekEnd(); err != nil { |
||||
|
tail.Kill(err) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} else if err == io.EOF { |
||||
|
if !tail.Follow { |
||||
|
if line != "" { |
||||
|
tail.sendLine(line) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if tail.Follow && line != "" { |
||||
|
tail.sendLine(line) |
||||
|
if err := tail.seekEnd(); err != nil { |
||||
|
tail.Kill(err) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// When EOF is reached, wait for more data to become
|
||||
|
// available. Wait strategy is based on the `tail.watcher`
|
||||
|
// implementation (inotify or polling).
|
||||
|
err := tail.waitForChanges() |
||||
|
if err != nil { |
||||
|
if err != ErrStop { |
||||
|
tail.Kill(err) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
} else { |
||||
|
// non-EOF error
|
||||
|
tail.Killf("Error reading %s: %s", tail.Filename, err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
select { |
||||
|
case <-tail.Dying(): |
||||
|
if tail.Err() == errStopAtEOF { |
||||
|
continue |
||||
|
} |
||||
|
return |
||||
|
default: |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// waitForChanges waits until the file has been appended, deleted,
|
||||
|
// moved or truncated. When moved or deleted - the file will be
|
||||
|
// reopened if ReOpen is true. Truncated files are always reopened.
|
||||
|
func (tail *Tail) waitForChanges() error { |
||||
|
if tail.changes == nil { |
||||
|
pos, err := tail.file.Seek(0, io.SeekCurrent) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
select { |
||||
|
case <-tail.changes.Modified: |
||||
|
return nil |
||||
|
case <-tail.changes.Deleted: |
||||
|
tail.changes = nil |
||||
|
if tail.ReOpen { |
||||
|
// XXX: we must not log from a library.
|
||||
|
tail.Logger.Printf("Re-opening moved/deleted file %s ...", tail.Filename) |
||||
|
if err := tail.reopen(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
tail.Logger.Printf("Successfully reopened %s", tail.Filename) |
||||
|
tail.openReader() |
||||
|
return nil |
||||
|
} |
||||
|
tail.Logger.Printf("Stopping tail as file no longer exists: %s", tail.Filename) |
||||
|
return ErrStop |
||||
|
case <-tail.changes.Truncated: |
||||
|
// Always reopen truncated files (Follow is true)
|
||||
|
tail.Logger.Printf("Re-opening truncated file %s ...", tail.Filename) |
||||
|
if err := tail.reopen(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
tail.Logger.Printf("Successfully reopened truncated %s", tail.Filename) |
||||
|
tail.openReader() |
||||
|
return nil |
||||
|
case <-tail.Dying(): |
||||
|
return ErrStop |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) openReader() { |
||||
|
tail.lk.Lock() |
||||
|
if tail.MaxLineSize > 0 { |
||||
|
// add 2 to account for newline characters
|
||||
|
tail.reader = bufio.NewReaderSize(tail.file, tail.MaxLineSize+2) |
||||
|
} else { |
||||
|
tail.reader = bufio.NewReader(tail.file) |
||||
|
} |
||||
|
tail.lk.Unlock() |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) seekEnd() error { |
||||
|
return tail.seekTo(SeekInfo{Offset: 0, Whence: io.SeekEnd}) |
||||
|
} |
||||
|
|
||||
|
func (tail *Tail) seekTo(pos SeekInfo) error { |
||||
|
_, err := tail.file.Seek(pos.Offset, pos.Whence) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("Seek error on %s: %s", tail.Filename, err) |
||||
|
} |
||||
|
// Reset the read buffer whenever the file is re-seek'ed
|
||||
|
tail.reader.Reset(tail.file) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// sendLine sends the line(s) to Lines channel, splitting longer lines
|
||||
|
// if necessary. Return false if rate limit is reached.
|
||||
|
func (tail *Tail) sendLine(line string) bool { |
||||
|
now := time.Now() |
||||
|
lines := []string{line} |
||||
|
|
||||
|
// Split longer lines
|
||||
|
if tail.MaxLineSize > 0 && len(line) > tail.MaxLineSize { |
||||
|
lines = util.PartitionString(line, tail.MaxLineSize) |
||||
|
} |
||||
|
|
||||
|
for _, line := range lines { |
||||
|
tail.lineNum++ |
||||
|
offset, _ := tail.Tell() |
||||
|
select { |
||||
|
case tail.Lines <- &Line{line, tail.lineNum, SeekInfo{Offset: offset}, now, nil}: |
||||
|
case <-tail.Dying(): |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if tail.Config.RateLimiter != nil { |
||||
|
ok := tail.Config.RateLimiter.Pour(uint16(len(lines))) |
||||
|
if !ok { |
||||
|
tail.Logger.Printf("Leaky bucket full (%v); entering 1s cooloff period.", |
||||
|
tail.Filename) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Cleanup removes inotify watches added by the tail package. This function is
|
||||
|
// meant to be invoked from a process's exit handler. Linux kernel may not
|
||||
|
// automatically remove inotify watches after the process exits.
|
||||
|
// If you plan to re-read a file, don't call Cleanup in between.
|
||||
|
func (tail *Tail) Cleanup() { |
||||
|
watch.Cleanup(tail.Filename) |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// +build !windows
|
||||
|
|
||||
|
package tail |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
// Deprecated: this function is only useful internally and, as such,
|
||||
|
// it will be removed from the API in a future major release.
|
||||
|
//
|
||||
|
// OpenFile proxies a os.Open call for a file so it can be correctly tailed
|
||||
|
// on POSIX and non-POSIX OSes like MS Windows.
|
||||
|
func OpenFile(name string) (file *os.File, err error) { |
||||
|
return os.Open(name) |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// +build windows
|
||||
|
|
||||
|
package tail |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
|
||||
|
"github.com/nxadm/tail/winfile" |
||||
|
) |
||||
|
|
||||
|
// Deprecated: this function is only useful internally and, as such,
|
||||
|
// it will be removed from the API in a future major release.
|
||||
|
//
|
||||
|
// OpenFile proxies a os.Open call for a file so it can be correctly tailed
|
||||
|
// on POSIX and non-POSIX OSes like MS Windows.
|
||||
|
func OpenFile(name string) (file *os.File, err error) { |
||||
|
return winfile.OpenFile(name, os.O_RDONLY, 0) |
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
package util |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"log" |
||||
|
"os" |
||||
|
"runtime/debug" |
||||
|
) |
||||
|
|
||||
|
type Logger struct { |
||||
|
*log.Logger |
||||
|
} |
||||
|
|
||||
|
var LOGGER = &Logger{log.New(os.Stderr, "", log.LstdFlags)} |
||||
|
|
||||
|
// fatal is like panic except it displays only the current goroutine's stack.
|
||||
|
func Fatal(format string, v ...interface{}) { |
||||
|
// https://github.com/nxadm/log/blob/master/log.go#L45
|
||||
|
LOGGER.Output(2, fmt.Sprintf("FATAL -- "+format, v...)+"\n"+string(debug.Stack())) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
|
||||
|
// partitionString partitions the string into chunks of given size,
|
||||
|
// with the last chunk of variable size.
|
||||
|
func PartitionString(s string, chunkSize int) []string { |
||||
|
if chunkSize <= 0 { |
||||
|
panic("invalid chunkSize") |
||||
|
} |
||||
|
length := len(s) |
||||
|
chunks := 1 + length/chunkSize |
||||
|
start := 0 |
||||
|
end := chunkSize |
||||
|
parts := make([]string, 0, chunks) |
||||
|
for { |
||||
|
if end > length { |
||||
|
end = length |
||||
|
} |
||||
|
parts = append(parts, s[start:end]) |
||||
|
if end == length { |
||||
|
break |
||||
|
} |
||||
|
start, end = end, end+chunkSize |
||||
|
} |
||||
|
return parts |
||||
|
} |
@ -0,0 +1,37 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
package watch |
||||
|
|
||||
|
type FileChanges struct { |
||||
|
Modified chan bool // Channel to get notified of modifications
|
||||
|
Truncated chan bool // Channel to get notified of truncations
|
||||
|
Deleted chan bool // Channel to get notified of deletions/renames
|
||||
|
} |
||||
|
|
||||
|
func NewFileChanges() *FileChanges { |
||||
|
return &FileChanges{ |
||||
|
make(chan bool, 1), make(chan bool, 1), make(chan bool, 1)} |
||||
|
} |
||||
|
|
||||
|
func (fc *FileChanges) NotifyModified() { |
||||
|
sendOnlyIfEmpty(fc.Modified) |
||||
|
} |
||||
|
|
||||
|
func (fc *FileChanges) NotifyTruncated() { |
||||
|
sendOnlyIfEmpty(fc.Truncated) |
||||
|
} |
||||
|
|
||||
|
func (fc *FileChanges) NotifyDeleted() { |
||||
|
sendOnlyIfEmpty(fc.Deleted) |
||||
|
} |
||||
|
|
||||
|
// sendOnlyIfEmpty sends on a bool channel only if the channel has no
|
||||
|
// backlog to be read by other goroutines. This concurrency pattern
|
||||
|
// can be used to notify other goroutines if and only if they are
|
||||
|
// looking for it (i.e., subsequent notifications can be compressed
|
||||
|
// into one).
|
||||
|
func sendOnlyIfEmpty(ch chan bool) { |
||||
|
select { |
||||
|
case ch <- true: |
||||
|
default: |
||||
|
} |
||||
|
} |
@ -0,0 +1,136 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
package watch |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
|
||||
|
"github.com/nxadm/tail/util" |
||||
|
|
||||
|
"github.com/fsnotify/fsnotify" |
||||
|
"gopkg.in/tomb.v1" |
||||
|
) |
||||
|
|
||||
|
// InotifyFileWatcher uses inotify to monitor file changes.
|
||||
|
type InotifyFileWatcher struct { |
||||
|
Filename string |
||||
|
Size int64 |
||||
|
} |
||||
|
|
||||
|
func NewInotifyFileWatcher(filename string) *InotifyFileWatcher { |
||||
|
fw := &InotifyFileWatcher{filepath.Clean(filename), 0} |
||||
|
return fw |
||||
|
} |
||||
|
|
||||
|
func (fw *InotifyFileWatcher) BlockUntilExists(t *tomb.Tomb) error { |
||||
|
err := WatchCreate(fw.Filename) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer RemoveWatchCreate(fw.Filename) |
||||
|
|
||||
|
// Do a real check now as the file might have been created before
|
||||
|
// calling `WatchFlags` above.
|
||||
|
if _, err = os.Stat(fw.Filename); !os.IsNotExist(err) { |
||||
|
// file exists, or stat returned an error.
|
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
events := Events(fw.Filename) |
||||
|
|
||||
|
for { |
||||
|
select { |
||||
|
case evt, ok := <-events: |
||||
|
if !ok { |
||||
|
return fmt.Errorf("inotify watcher has been closed") |
||||
|
} |
||||
|
evtName, err := filepath.Abs(evt.Name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
fwFilename, err := filepath.Abs(fw.Filename) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if evtName == fwFilename { |
||||
|
return nil |
||||
|
} |
||||
|
case <-t.Dying(): |
||||
|
return tomb.ErrDying |
||||
|
} |
||||
|
} |
||||
|
panic("unreachable") |
||||
|
} |
||||
|
|
||||
|
func (fw *InotifyFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) { |
||||
|
err := Watch(fw.Filename) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
changes := NewFileChanges() |
||||
|
fw.Size = pos |
||||
|
|
||||
|
go func() { |
||||
|
|
||||
|
events := Events(fw.Filename) |
||||
|
|
||||
|
for { |
||||
|
prevSize := fw.Size |
||||
|
|
||||
|
var evt fsnotify.Event |
||||
|
var ok bool |
||||
|
|
||||
|
select { |
||||
|
case evt, ok = <-events: |
||||
|
if !ok { |
||||
|
RemoveWatch(fw.Filename) |
||||
|
return |
||||
|
} |
||||
|
case <-t.Dying(): |
||||
|
RemoveWatch(fw.Filename) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch { |
||||
|
case evt.Op&fsnotify.Remove == fsnotify.Remove: |
||||
|
fallthrough |
||||
|
|
||||
|
case evt.Op&fsnotify.Rename == fsnotify.Rename: |
||||
|
RemoveWatch(fw.Filename) |
||||
|
changes.NotifyDeleted() |
||||
|
return |
||||
|
|
||||
|
//With an open fd, unlink(fd) - inotify returns IN_ATTRIB (==fsnotify.Chmod)
|
||||
|
case evt.Op&fsnotify.Chmod == fsnotify.Chmod: |
||||
|
fallthrough |
||||
|
|
||||
|
case evt.Op&fsnotify.Write == fsnotify.Write: |
||||
|
fi, err := os.Stat(fw.Filename) |
||||
|
if err != nil { |
||||
|
if os.IsNotExist(err) { |
||||
|
RemoveWatch(fw.Filename) |
||||
|
changes.NotifyDeleted() |
||||
|
return |
||||
|
} |
||||
|
// XXX: report this error back to the user
|
||||
|
util.Fatal("Failed to stat file %v: %v", fw.Filename, err) |
||||
|
} |
||||
|
fw.Size = fi.Size() |
||||
|
|
||||
|
if prevSize > 0 && prevSize > fw.Size { |
||||
|
changes.NotifyTruncated() |
||||
|
} else { |
||||
|
changes.NotifyModified() |
||||
|
} |
||||
|
prevSize = fw.Size |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
return changes, nil |
||||
|
} |
@ -0,0 +1,249 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
package watch |
||||
|
|
||||
|
import ( |
||||
|
"log" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"sync" |
||||
|
"syscall" |
||||
|
|
||||
|
"github.com/nxadm/tail/util" |
||||
|
|
||||
|
"github.com/fsnotify/fsnotify" |
||||
|
) |
||||
|
|
||||
|
type InotifyTracker struct { |
||||
|
mux sync.Mutex |
||||
|
watcher *fsnotify.Watcher |
||||
|
chans map[string]chan fsnotify.Event |
||||
|
done map[string]chan bool |
||||
|
watchNums map[string]int |
||||
|
watch chan *watchInfo |
||||
|
remove chan *watchInfo |
||||
|
error chan error |
||||
|
} |
||||
|
|
||||
|
type watchInfo struct { |
||||
|
op fsnotify.Op |
||||
|
fname string |
||||
|
} |
||||
|
|
||||
|
func (this *watchInfo) isCreate() bool { |
||||
|
return this.op == fsnotify.Create |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
// globally shared InotifyTracker; ensures only one fsnotify.Watcher is used
|
||||
|
shared *InotifyTracker |
||||
|
|
||||
|
// these are used to ensure the shared InotifyTracker is run exactly once
|
||||
|
once = sync.Once{} |
||||
|
goRun = func() { |
||||
|
shared = &InotifyTracker{ |
||||
|
mux: sync.Mutex{}, |
||||
|
chans: make(map[string]chan fsnotify.Event), |
||||
|
done: make(map[string]chan bool), |
||||
|
watchNums: make(map[string]int), |
||||
|
watch: make(chan *watchInfo), |
||||
|
remove: make(chan *watchInfo), |
||||
|
error: make(chan error), |
||||
|
} |
||||
|
go shared.run() |
||||
|
} |
||||
|
|
||||
|
logger = log.New(os.Stderr, "", log.LstdFlags) |
||||
|
) |
||||
|
|
||||
|
// Watch signals the run goroutine to begin watching the input filename
|
||||
|
func Watch(fname string) error { |
||||
|
return watch(&watchInfo{ |
||||
|
fname: fname, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Watch create signals the run goroutine to begin watching the input filename
|
||||
|
// if call the WatchCreate function, don't call the Cleanup, call the RemoveWatchCreate
|
||||
|
func WatchCreate(fname string) error { |
||||
|
return watch(&watchInfo{ |
||||
|
op: fsnotify.Create, |
||||
|
fname: fname, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func watch(winfo *watchInfo) error { |
||||
|
// start running the shared InotifyTracker if not already running
|
||||
|
once.Do(goRun) |
||||
|
|
||||
|
winfo.fname = filepath.Clean(winfo.fname) |
||||
|
shared.watch <- winfo |
||||
|
return <-shared.error |
||||
|
} |
||||
|
|
||||
|
// RemoveWatch signals the run goroutine to remove the watch for the input filename
|
||||
|
func RemoveWatch(fname string) error { |
||||
|
return remove(&watchInfo{ |
||||
|
fname: fname, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// RemoveWatch create signals the run goroutine to remove the watch for the input filename
|
||||
|
func RemoveWatchCreate(fname string) error { |
||||
|
return remove(&watchInfo{ |
||||
|
op: fsnotify.Create, |
||||
|
fname: fname, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func remove(winfo *watchInfo) error { |
||||
|
// start running the shared InotifyTracker if not already running
|
||||
|
once.Do(goRun) |
||||
|
|
||||
|
winfo.fname = filepath.Clean(winfo.fname) |
||||
|
shared.mux.Lock() |
||||
|
done := shared.done[winfo.fname] |
||||
|
if done != nil { |
||||
|
delete(shared.done, winfo.fname) |
||||
|
close(done) |
||||
|
} |
||||
|
shared.mux.Unlock() |
||||
|
|
||||
|
shared.remove <- winfo |
||||
|
return <-shared.error |
||||
|
} |
||||
|
|
||||
|
// Events returns a channel to which FileEvents corresponding to the input filename
|
||||
|
// will be sent. This channel will be closed when removeWatch is called on this
|
||||
|
// filename.
|
||||
|
func Events(fname string) <-chan fsnotify.Event { |
||||
|
shared.mux.Lock() |
||||
|
defer shared.mux.Unlock() |
||||
|
|
||||
|
return shared.chans[fname] |
||||
|
} |
||||
|
|
||||
|
// Cleanup removes the watch for the input filename if necessary.
|
||||
|
func Cleanup(fname string) error { |
||||
|
return RemoveWatch(fname) |
||||
|
} |
||||
|
|
||||
|
// watchFlags calls fsnotify.WatchFlags for the input filename and flags, creating
|
||||
|
// a new Watcher if the previous Watcher was closed.
|
||||
|
func (shared *InotifyTracker) addWatch(winfo *watchInfo) error { |
||||
|
shared.mux.Lock() |
||||
|
defer shared.mux.Unlock() |
||||
|
|
||||
|
if shared.chans[winfo.fname] == nil { |
||||
|
shared.chans[winfo.fname] = make(chan fsnotify.Event) |
||||
|
} |
||||
|
if shared.done[winfo.fname] == nil { |
||||
|
shared.done[winfo.fname] = make(chan bool) |
||||
|
} |
||||
|
|
||||
|
fname := winfo.fname |
||||
|
if winfo.isCreate() { |
||||
|
// Watch for new files to be created in the parent directory.
|
||||
|
fname = filepath.Dir(fname) |
||||
|
} |
||||
|
|
||||
|
var err error |
||||
|
// already in inotify watch
|
||||
|
if shared.watchNums[fname] == 0 { |
||||
|
err = shared.watcher.Add(fname) |
||||
|
} |
||||
|
if err == nil { |
||||
|
shared.watchNums[fname]++ |
||||
|
} |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// removeWatch calls fsnotify.RemoveWatch for the input filename and closes the
|
||||
|
// corresponding events channel.
|
||||
|
func (shared *InotifyTracker) removeWatch(winfo *watchInfo) error { |
||||
|
shared.mux.Lock() |
||||
|
|
||||
|
ch := shared.chans[winfo.fname] |
||||
|
if ch != nil { |
||||
|
delete(shared.chans, winfo.fname) |
||||
|
close(ch) |
||||
|
} |
||||
|
|
||||
|
fname := winfo.fname |
||||
|
if winfo.isCreate() { |
||||
|
// Watch for new files to be created in the parent directory.
|
||||
|
fname = filepath.Dir(fname) |
||||
|
} |
||||
|
shared.watchNums[fname]-- |
||||
|
watchNum := shared.watchNums[fname] |
||||
|
if watchNum == 0 { |
||||
|
delete(shared.watchNums, fname) |
||||
|
} |
||||
|
shared.mux.Unlock() |
||||
|
|
||||
|
var err error |
||||
|
// If we were the last ones to watch this file, unsubscribe from inotify.
|
||||
|
// This needs to happen after releasing the lock because fsnotify waits
|
||||
|
// synchronously for the kernel to acknowledge the removal of the watch
|
||||
|
// for this file, which causes us to deadlock if we still held the lock.
|
||||
|
if watchNum == 0 { |
||||
|
err = shared.watcher.Remove(fname) |
||||
|
} |
||||
|
|
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// sendEvent sends the input event to the appropriate Tail.
|
||||
|
func (shared *InotifyTracker) sendEvent(event fsnotify.Event) { |
||||
|
name := filepath.Clean(event.Name) |
||||
|
|
||||
|
shared.mux.Lock() |
||||
|
ch := shared.chans[name] |
||||
|
done := shared.done[name] |
||||
|
shared.mux.Unlock() |
||||
|
|
||||
|
if ch != nil && done != nil { |
||||
|
select { |
||||
|
case ch <- event: |
||||
|
case <-done: |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// run starts the goroutine in which the shared struct reads events from its
|
||||
|
// Watcher's Event channel and sends the events to the appropriate Tail.
|
||||
|
func (shared *InotifyTracker) run() { |
||||
|
watcher, err := fsnotify.NewWatcher() |
||||
|
if err != nil { |
||||
|
util.Fatal("failed to create Watcher") |
||||
|
} |
||||
|
shared.watcher = watcher |
||||
|
|
||||
|
for { |
||||
|
select { |
||||
|
case winfo := <-shared.watch: |
||||
|
shared.error <- shared.addWatch(winfo) |
||||
|
|
||||
|
case winfo := <-shared.remove: |
||||
|
shared.error <- shared.removeWatch(winfo) |
||||
|
|
||||
|
case event, open := <-shared.watcher.Events: |
||||
|
if !open { |
||||
|
return |
||||
|
} |
||||
|
shared.sendEvent(event) |
||||
|
|
||||
|
case err, open := <-shared.watcher.Errors: |
||||
|
if !open { |
||||
|
return |
||||
|
} else if err != nil { |
||||
|
sysErr, ok := err.(*os.SyscallError) |
||||
|
if !ok || sysErr.Err != syscall.EINTR { |
||||
|
logger.Printf("Error in Watcher Error channel: %s", err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,119 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
package watch |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"runtime" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/nxadm/tail/util" |
||||
|
"gopkg.in/tomb.v1" |
||||
|
) |
||||
|
|
||||
|
// PollingFileWatcher polls the file for changes.
|
||||
|
type PollingFileWatcher struct { |
||||
|
Filename string |
||||
|
Size int64 |
||||
|
} |
||||
|
|
||||
|
func NewPollingFileWatcher(filename string) *PollingFileWatcher { |
||||
|
fw := &PollingFileWatcher{filename, 0} |
||||
|
return fw |
||||
|
} |
||||
|
|
||||
|
var POLL_DURATION time.Duration |
||||
|
|
||||
|
func (fw *PollingFileWatcher) BlockUntilExists(t *tomb.Tomb) error { |
||||
|
for { |
||||
|
if _, err := os.Stat(fw.Filename); err == nil { |
||||
|
return nil |
||||
|
} else if !os.IsNotExist(err) { |
||||
|
return err |
||||
|
} |
||||
|
select { |
||||
|
case <-time.After(POLL_DURATION): |
||||
|
continue |
||||
|
case <-t.Dying(): |
||||
|
return tomb.ErrDying |
||||
|
} |
||||
|
} |
||||
|
panic("unreachable") |
||||
|
} |
||||
|
|
||||
|
func (fw *PollingFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) { |
||||
|
origFi, err := os.Stat(fw.Filename) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
changes := NewFileChanges() |
||||
|
var prevModTime time.Time |
||||
|
|
||||
|
// XXX: use tomb.Tomb to cleanly manage these goroutines. replace
|
||||
|
// the fatal (below) with tomb's Kill.
|
||||
|
|
||||
|
fw.Size = pos |
||||
|
|
||||
|
go func() { |
||||
|
prevSize := fw.Size |
||||
|
for { |
||||
|
select { |
||||
|
case <-t.Dying(): |
||||
|
return |
||||
|
default: |
||||
|
} |
||||
|
|
||||
|
time.Sleep(POLL_DURATION) |
||||
|
fi, err := os.Stat(fw.Filename) |
||||
|
if err != nil { |
||||
|
// Windows cannot delete a file if a handle is still open (tail keeps one open)
|
||||
|
// so it gives access denied to anything trying to read it until all handles are released.
|
||||
|
if os.IsNotExist(err) || (runtime.GOOS == "windows" && os.IsPermission(err)) { |
||||
|
// File does not exist (has been deleted).
|
||||
|
changes.NotifyDeleted() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// XXX: report this error back to the user
|
||||
|
util.Fatal("Failed to stat file %v: %v", fw.Filename, err) |
||||
|
} |
||||
|
|
||||
|
// File got moved/renamed?
|
||||
|
if !os.SameFile(origFi, fi) { |
||||
|
changes.NotifyDeleted() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// File got truncated?
|
||||
|
fw.Size = fi.Size() |
||||
|
if prevSize > 0 && prevSize > fw.Size { |
||||
|
changes.NotifyTruncated() |
||||
|
prevSize = fw.Size |
||||
|
continue |
||||
|
} |
||||
|
// File got bigger?
|
||||
|
if prevSize > 0 && prevSize < fw.Size { |
||||
|
changes.NotifyModified() |
||||
|
prevSize = fw.Size |
||||
|
continue |
||||
|
} |
||||
|
prevSize = fw.Size |
||||
|
|
||||
|
// File was appended to (changed)?
|
||||
|
modTime := fi.ModTime() |
||||
|
if modTime != prevModTime { |
||||
|
prevModTime = modTime |
||||
|
changes.NotifyModified() |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
return changes, nil |
||||
|
} |
||||
|
|
||||
|
func init() { |
||||
|
POLL_DURATION = 250 * time.Millisecond |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
|
||||
|
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
|
||||
|
|
||||
|
package watch |
||||
|
|
||||
|
import "gopkg.in/tomb.v1" |
||||
|
|
||||
|
// FileWatcher monitors file-level events.
|
||||
|
type FileWatcher interface { |
||||
|
// BlockUntilExists blocks until the file comes into existence.
|
||||
|
BlockUntilExists(*tomb.Tomb) error |
||||
|
|
||||
|
// ChangeEvents reports on changes to a file, be it modification,
|
||||
|
// deletion, renames or truncations. Returned FileChanges group of
|
||||
|
// channels will be closed, thus become unusable, after a deletion
|
||||
|
// or truncation event.
|
||||
|
// In order to properly report truncations, ChangeEvents requires
|
||||
|
// the caller to pass their current offset in the file.
|
||||
|
ChangeEvents(*tomb.Tomb, int64) (*FileChanges, error) |
||||
|
} |
@ -0,0 +1,93 @@ |
|||||
|
// Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail
|
||||
|
// +build windows
|
||||
|
|
||||
|
package winfile |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// issue also described here
|
||||
|
//https://codereview.appspot.com/8203043/
|
||||
|
|
||||
|
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/syscall/syscall_windows.go#L218
|
||||
|
func Open(path string, mode int, perm uint32) (fd syscall.Handle, err error) { |
||||
|
if len(path) == 0 { |
||||
|
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND |
||||
|
} |
||||
|
pathp, err := syscall.UTF16PtrFromString(path) |
||||
|
if err != nil { |
||||
|
return syscall.InvalidHandle, err |
||||
|
} |
||||
|
var access uint32 |
||||
|
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) { |
||||
|
case syscall.O_RDONLY: |
||||
|
access = syscall.GENERIC_READ |
||||
|
case syscall.O_WRONLY: |
||||
|
access = syscall.GENERIC_WRITE |
||||
|
case syscall.O_RDWR: |
||||
|
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE |
||||
|
} |
||||
|
if mode&syscall.O_CREAT != 0 { |
||||
|
access |= syscall.GENERIC_WRITE |
||||
|
} |
||||
|
if mode&syscall.O_APPEND != 0 { |
||||
|
access &^= syscall.GENERIC_WRITE |
||||
|
access |= syscall.FILE_APPEND_DATA |
||||
|
} |
||||
|
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE) |
||||
|
var sa *syscall.SecurityAttributes |
||||
|
if mode&syscall.O_CLOEXEC == 0 { |
||||
|
sa = makeInheritSa() |
||||
|
} |
||||
|
var createmode uint32 |
||||
|
switch { |
||||
|
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL): |
||||
|
createmode = syscall.CREATE_NEW |
||||
|
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC): |
||||
|
createmode = syscall.CREATE_ALWAYS |
||||
|
case mode&syscall.O_CREAT == syscall.O_CREAT: |
||||
|
createmode = syscall.OPEN_ALWAYS |
||||
|
case mode&syscall.O_TRUNC == syscall.O_TRUNC: |
||||
|
createmode = syscall.TRUNCATE_EXISTING |
||||
|
default: |
||||
|
createmode = syscall.OPEN_EXISTING |
||||
|
} |
||||
|
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, syscall.FILE_ATTRIBUTE_NORMAL, 0) |
||||
|
return h, e |
||||
|
} |
||||
|
|
||||
|
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/syscall/syscall_windows.go#L211
|
||||
|
func makeInheritSa() *syscall.SecurityAttributes { |
||||
|
var sa syscall.SecurityAttributes |
||||
|
sa.Length = uint32(unsafe.Sizeof(sa)) |
||||
|
sa.InheritHandle = 1 |
||||
|
return &sa |
||||
|
} |
||||
|
|
||||
|
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/os/file_windows.go#L133
|
||||
|
func OpenFile(name string, flag int, perm os.FileMode) (file *os.File, err error) { |
||||
|
r, e := Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm)) |
||||
|
if e != nil { |
||||
|
return nil, e |
||||
|
} |
||||
|
return os.NewFile(uintptr(r), name), nil |
||||
|
} |
||||
|
|
||||
|
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/os/file_posix.go#L61
|
||||
|
func syscallMode(i os.FileMode) (o uint32) { |
||||
|
o |= uint32(i.Perm()) |
||||
|
if i&os.ModeSetuid != 0 { |
||||
|
o |= syscall.S_ISUID |
||||
|
} |
||||
|
if i&os.ModeSetgid != 0 { |
||||
|
o |= syscall.S_ISGID |
||||
|
} |
||||
|
if i&os.ModeSticky != 0 { |
||||
|
o |= syscall.S_ISVTX |
||||
|
} |
||||
|
// No mapping for Go's ModeTemporary (plan9 only).
|
||||
|
return |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
.DS_Store |
||||
|
TODO |
||||
|
tmp/**/* |
||||
|
*.coverprofile |
||||
|
.vscode |
||||
|
.idea/ |
||||
|
*.log |
@ -0,0 +1,24 @@ |
|||||
|
language: go |
||||
|
go: |
||||
|
- tip |
||||
|
- 1.16.x |
||||
|
- 1.15.x |
||||
|
|
||||
|
cache: |
||||
|
directories: |
||||
|
- $GOPATH/pkg/mod |
||||
|
|
||||
|
# allow internal package imports, necessary for forked repositories |
||||
|
go_import_path: github.com/onsi/ginkgo |
||||
|
|
||||
|
install: |
||||
|
- GO111MODULE="off" go get -v -t ./... |
||||
|
- GO111MODULE="off" go get golang.org/x/tools/cmd/cover |
||||
|
- GO111MODULE="off" go get github.com/onsi/gomega |
||||
|
- GO111MODULE="off" go install github.com/onsi/ginkgo/ginkgo |
||||
|
- export PATH=$GOPATH/bin:$PATH |
||||
|
|
||||
|
script: |
||||
|
- GO111MODULE="on" go mod tidy && git diff --exit-code go.mod go.sum |
||||
|
- go vet |
||||
|
- ginkgo -r --randomizeAllSpecs --randomizeSuites --race --trace |
@ -0,0 +1,376 @@ |
|||||
|
## 1.16.2 |
||||
|
|
||||
|
### Fixes |
||||
|
- Deprecations can be suppressed by setting an `ACK_GINKGO_DEPRECATIONS=<semver>` environment variable. |
||||
|
|
||||
|
## 1.16.1 |
||||
|
|
||||
|
### Fixes |
||||
|
- Supress --stream deprecation warning on windows (#793) |
||||
|
|
||||
|
## 1.16.0 |
||||
|
|
||||
|
### Features |
||||
|
- Advertise Ginkgo 2.0. Introduce deprecations. [9ef1913] |
||||
|
- Update README.md to advertise that Ginkgo 2.0 is coming. |
||||
|
- Backport the 2.0 DeprecationTracker and start alerting users |
||||
|
about upcoming deprecations. |
||||
|
|
||||
|
- Add slim-sprig template functions to bootstrap/generate (#775) [9162b86] |
||||
|
|
||||
|
### Fixes |
||||
|
- Fix accidental reference to 1488 (#784) [9fb7fe4] |
||||
|
|
||||
|
## 1.15.2 |
||||
|
|
||||
|
### Fixes |
||||
|
- ignore blank `-focus` and `-skip` flags (#780) [e90a4a0] |
||||
|
|
||||
|
## 1.15.1 |
||||
|
|
||||
|
### Fixes |
||||
|
- reporters/junit: Use `system-out` element instead of `passed` (#769) [9eda305] |
||||
|
|
||||
|
## 1.15.0 |
||||
|
|
||||
|
### Features |
||||
|
- Adds 'outline' command to print the outline of specs/containers in a file (#754) [071c369] [6803cc3] [935b538] [06744e8] [0c40583] |
||||
|
- Add support for using template to generate tests (#752) [efb9e69] |
||||
|
- Add a Chinese Doc #755 (#756) [5207632] |
||||
|
- cli: allow multiple -focus and -skip flags (#736) [9a782fb] |
||||
|
|
||||
|
### Fixes |
||||
|
- Add _internal to filename of tests created with internal flag (#751) [43c12da] |
||||
|
|
||||
|
## 1.14.2 |
||||
|
|
||||
|
### Fixes |
||||
|
- correct handling windows backslash in import path (#721) [97f3d51] |
||||
|
- Add additional methods to GinkgoT() to improve compatibility with the testing.TB interface [b5fe44d] |
||||
|
|
||||
|
## 1.14.1 |
||||
|
|
||||
|
### Fixes |
||||
|
- Discard exported method declaration when running ginkgo bootstrap (#558) [f4b0240] |
||||
|
|
||||
|
## 1.14.0 |
||||
|
|
||||
|
### Features |
||||
|
- Defer running top-level container nodes until RunSpecs is called [d44dedf] |
||||
|
- [Document Ginkgo lifecycle](http://onsi.github.io/ginkgo/#understanding-ginkgos-lifecycle) |
||||
|
- Add `extensions/globals` package (#692) [3295c8f] - this can be helpful in contexts where you are test-driving your test-generation code (see [#692](https://github.com/onsi/ginkgo/pull/692)) |
||||
|
- Print Skip reason in JUnit reporter if one was provided [820dfab] |
||||
|
|
||||
|
## 1.13.0 |
||||
|
|
||||
|
### Features |
||||
|
- Add a version of table.Entry that allows dumping the entry parameters. (#689) [21eaef2] |
||||
|
|
||||
|
### Fixes |
||||
|
- Ensure integration tests pass in an environment sans GOPATH [606fba2] |
||||
|
- Add books package (#568) [fc0e44e] |
||||
|
- doc(readme): installation via "tools package" (#677) [83bb20e] |
||||
|
- Solve the undefined: unix.Dup2 compile error on mips64le (#680) [0624f75] |
||||
|
- Import package without dot (#687) [6321024] |
||||
|
- Fix integration tests to stop require GOPATH (#686) [a912ec5] |
||||
|
|
||||
|
## 1.12.3 |
||||
|
|
||||
|
### Fixes |
||||
|
- Print correct code location of failing table test (#666) [c6d7afb] |
||||
|
|
||||
|
## 1.12.2 |
||||
|
|
||||
|
### Fixes |
||||
|
- Update dependencies [ea4a036] |
||||
|
|
||||
|
## 1.12.1 |
||||
|
|
||||
|
### Fixes |
||||
|
- Make unfocus ("blur") much faster (#674) [8b18061] |
||||
|
- Fix typo (#673) [7fdcbe8] |
||||
|
- Test against 1.14 and remove 1.12 [d5c2ad6] |
||||
|
- Test if a coverprofile content is empty before checking its latest character (#670) [14d9fa2] |
||||
|
- replace tail package with maintained one. this fixes go get errors (#667) [4ba33d4] |
||||
|
- improve ginkgo performance - makes progress on #644 [a14f98e] |
||||
|
- fix convert integration tests [1f8ba69] |
||||
|
- fix typo succesful -> successful (#663) [1ea49cf] |
||||
|
- Fix invalid link (#658) [b886136] |
||||
|
- convert utility : Include comments from source (#657) [1077c6d] |
||||
|
- Explain what BDD means [d79e7fb] |
||||
|
- skip race detector test on unsupported platform (#642) [f8ab89d] |
||||
|
- Use Dup2 from golang.org/x/sys/unix instead of syscallDup (#638) [5d53c55] |
||||
|
- Fix missing newline in combined coverage file (#641) [6a07ea2] |
||||
|
- check if a spec is run before returning SpecSummary (#645) [8850000] |
||||
|
|
||||
|
## 1.12.0 |
||||
|
|
||||
|
### Features |
||||
|
- Add module definition (#630) [78916ab] |
||||
|
|
||||
|
## 1.11.0 |
||||
|
|
||||
|
### Features |
||||
|
- Add syscall for riscv64 architecture [f66e896] |
||||
|
- teamcity reporter: output location of test failure as well as test definition (#626) [9869142] |
||||
|
- teamcity reporter: output newline after every service message (#625) [3cfa02d] |
||||
|
- Add support for go module when running `generate` command (#578) [9c89e3f] |
||||
|
|
||||
|
## 1.10.3 |
||||
|
|
||||
|
### Fixes |
||||
|
- Set go_import_path in travis.yml to allow internal packages in forks (#607) [3b721db] |
||||
|
- Add integration test [d90e0dc] |
||||
|
- Fix coverage files combining [e5dde8c] |
||||
|
- A new CLI option: -ginkgo.reportFile <file path> (#601) [034fd25] |
||||
|
|
||||
|
## 1.10.2 |
||||
|
|
||||
|
### Fixes |
||||
|
- speed up table entry generateIt() (#609) [5049dc5] |
||||
|
- Fix. Write errors to stderr instead of stdout (#610) [7bb3091] |
||||
|
|
||||
|
## 1.10.1 |
||||
|
|
||||
|
### Fixes |
||||
|
- stack backtrace: fix skipping (#600) [2a4c0bd] |
||||
|
|
||||
|
## 1.10.0 |
||||
|
|
||||
|
### Fixes |
||||
|
- stack backtrace: fix alignment and skipping [66915d6] |
||||
|
- fix typo in documentation [8f97b93] |
||||
|
|
||||
|
## 1.9.0 |
||||
|
|
||||
|
### Features |
||||
|
- Option to print output into report, when tests have passed [0545415] |
||||
|
|
||||
|
### Fixes |
||||
|
- Fixed typos in comments [0ecbc58] |
||||
|
- gofmt code [a7f8bfb] |
||||
|
- Simplify code [7454d00] |
||||
|
- Simplify concatenation, incrementation and function assignment [4825557] |
||||
|
- Avoid unnecessary conversions [9d9403c] |
||||
|
- JUnit: include more detailed information about panic [19cca4b] |
||||
|
- Print help to stdout when the user asks for help [4cb7441] |
||||
|
|
||||
|
|
||||
|
## 1.8.0 |
||||
|
|
||||
|
### New Features |
||||
|
- allow config of the vet flag for `go test` (#562) [3cd45fa] |
||||
|
- Support projects using go modules [d56ee76] |
||||
|
|
||||
|
### Fixes and Minor Improvements |
||||
|
- chore(godoc): fixes typos in Measurement funcs [dbaca8e] |
||||
|
- Optimize focus to avoid allocations [f493786] |
||||
|
- Ensure generated test file names are underscored [505cc35] |
||||
|
|
||||
|
## 1.7.0 |
||||
|
|
||||
|
### New Features |
||||
|
- Add JustAfterEach (#484) [0d4f080] |
||||
|
|
||||
|
### Fixes |
||||
|
- Correctly round suite time in junit reporter [2445fc1] |
||||
|
- Avoid using -i argument to go test for Golang 1.10+ [46bbc26] |
||||
|
|
||||
|
## 1.6.0 |
||||
|
|
||||
|
### New Features |
||||
|
- add --debug flag to emit node output to files (#499) [39febac] |
||||
|
|
||||
|
### Fixes |
||||
|
- fix: for `go vet` to pass [69338ec] |
||||
|
- docs: fix for contributing instructions [7004cb1] |
||||
|
- consolidate and streamline contribution docs (#494) [d848015] |
||||
|
- Make generated Junit file compatable with "Maven Surefire" (#488) [e51bee6] |
||||
|
- all: gofmt [000d317] |
||||
|
- Increase eventually timeout to 30s [c73579c] |
||||
|
- Clarify asynchronous test behaviour [294d8f4] |
||||
|
- Travis badge should only show master [26d2143] |
||||
|
|
||||
|
## 1.5.0 5/10/2018 |
||||
|
|
||||
|
### New Features |
||||
|
- Supports go v1.10 (#443, #446, #451) [e873237, 468e89e, e37dbfe, a37f4c0, c0b857d, bca5260, 4177ca8] |
||||
|
- Add a When() synonym for Context() (#386) [747514b, 7484dad, 7354a07, dd826c8] |
||||
|
- Re-add noisySkippings flag [652e15c] |
||||
|
- Allow coverage to be displayed for focused specs (#367) [11459a8] |
||||
|
- Handle -outputdir flag (#364) [228e3a8] |
||||
|
- Handle -coverprofile flag (#355) [43392d5] |
||||
|
|
||||
|
### Fixes |
||||
|
- When using custom reporters register the custom reporters *before* the default reporter. This allows users to see the output of any print statements in their customer reporters. (#365) [8382b23] |
||||
|
- When running a test and calculating the coverage using the `-coverprofile` and `-outputdir` flags, Ginkgo fails with an error if the directory does not exist. This is due to an [issue in go 1.10](https://github.com/golang/go/issues/24588) (#446) [b36a6e0] |
||||
|
- `unfocus` command ignores vendor folder (#459) [e5e551c, c556e43, a3b6351, 9a820dd] |
||||
|
- Ignore packages whose tests are all ignored by go (#456) [7430ca7, 6d8be98] |
||||
|
- Increase the threshold when checking time measuments (#455) [2f714bf, 68f622c] |
||||
|
- Fix race condition in coverage tests (#423) [a5a8ff7, ab9c08b] |
||||
|
- Add an extra new line after reporting spec run completion for test2json [874520d] |
||||
|
- added name name field to junit reported testsuite [ae61c63] |
||||
|
- Do not set the run time of a spec when the dryRun flag is used (#438) [457e2d9, ba8e856] |
||||
|
- Process FWhen and FSpecify when unfocusing (#434) [9008c7b, ee65bd, df87dfe] |
||||
|
- Synchronise the access to the state of specs to avoid race conditions (#430) [7d481bc, ae6829d] |
||||
|
- Added Duration on GinkgoTestDescription (#383) [5f49dad, 528417e, 0747408, 329d7ed] |
||||
|
- Fix Ginkgo stack trace on failure for Specify (#415) [b977ede, 65ca40e, 6c46eb8] |
||||
|
- Update README with Go 1.6+, Golang -> Go (#409) [17f6b97, bc14b66, 20d1598] |
||||
|
- Use fmt.Errorf instead of errors.New(fmt.Sprintf (#401) [a299f56, 44e2eaa] |
||||
|
- Imports in generated code should follow conventions (#398) [0bec0b0, e8536d8] |
||||
|
- Prevent data race error when Recording a benchmark value from multiple go routines (#390) [c0c4881, 7a241e9] |
||||
|
- Replace GOPATH in Environment [4b883f0] |
||||
|
|
||||
|
|
||||
|
## 1.4.0 7/16/2017 |
||||
|
|
||||
|
- `ginkgo` now provides a hint if you accidentally forget to run `ginkgo bootstrap` to generate a `*_suite_test.go` file that actually invokes the Ginkgo test runner. [#345](https://github.com/onsi/ginkgo/pull/345) |
||||
|
- thanks to improvements in `go test -c` `ginkgo` no longer needs to fix Go's compilation output to ensure compilation errors are expressed relative to the CWD. [#357] |
||||
|
- `ginkgo watch -watchRegExp=...` allows you to specify a custom regular expression to watch. Only files matching the regular expression are watched for changes (the default is `\.go$`) [#356] |
||||
|
- `ginkgo` now always emits compilation output. Previously, only failed compilation output was printed out. [#277] |
||||
|
- `ginkgo -requireSuite` now fails the test run if there are `*_test.go` files but `go test` fails to detect any tests. Typically this means you forgot to run `ginkgo bootstrap` to generate a suite file. [#344] |
||||
|
- `ginkgo -timeout=DURATION` allows you to adjust the timeout for the entire test suite (default is 24 hours) [#248] |
||||
|
|
||||
|
## 1.3.0 3/28/2017 |
||||
|
|
||||
|
Improvements: |
||||
|
|
||||
|
- Significantly improved parallel test distribution. Now instead of pre-sharding test cases across workers (which can result in idle workers and poor test performance) Ginkgo uses a shared queue to keep all workers busy until all tests are complete. This improves test-time performance and consistency. |
||||
|
- `Skip(message)` can be used to skip the current test. |
||||
|
- Added `extensions/table` - a Ginkgo DSL for [Table Driven Tests](http://onsi.github.io/ginkgo/#table-driven-tests) |
||||
|
- Add `GinkgoRandomSeed()` - shorthand for `config.GinkgoConfig.RandomSeed` |
||||
|
- Support for retrying flaky tests with `--flakeAttempts` |
||||
|
- `ginkgo ./...` now recurses as you'd expect |
||||
|
- Added `Specify` a synonym for `It` |
||||
|
- Support colorise on Windows |
||||
|
- Broader support for various go compilation flags in the `ginkgo` CLI |
||||
|
|
||||
|
Bug Fixes: |
||||
|
|
||||
|
- Ginkgo tests now fail when you `panic(nil)` (#167) |
||||
|
|
||||
|
## 1.2.0 5/31/2015 |
||||
|
|
||||
|
Improvements |
||||
|
|
||||
|
- `ginkgo -coverpkg` calls down to `go test -coverpkg` (#160) |
||||
|
- `ginkgo -afterSuiteHook COMMAND` invokes the passed-in `COMMAND` after a test suite completes (#152) |
||||
|
- Relaxed requirement for Go 1.4+. `ginkgo` now works with Go v1.3+ (#166) |
||||
|
|
||||
|
## 1.2.0-beta |
||||
|
|
||||
|
Ginkgo now requires Go 1.4+ |
||||
|
|
||||
|
Improvements: |
||||
|
|
||||
|
- Call reporters in reverse order when announcing spec completion -- allows custom reporters to emit output before the default reporter does. |
||||
|
- Improved focus behavior. Now, this: |
||||
|
|
||||
|
```golang |
||||
|
FDescribe("Some describe", func() { |
||||
|
It("A", func() {}) |
||||
|
|
||||
|
FIt("B", func() {}) |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
will run `B` but *not* `A`. This tends to be a common usage pattern when in the thick of writing and debugging tests. |
||||
|
- When `SIGINT` is received, Ginkgo will emit the contents of the `GinkgoWriter` before running the `AfterSuite`. Useful for debugging stuck tests. |
||||
|
- When `--progress` is set, Ginkgo will write test progress (in particular, Ginkgo will say when it is about to run a BeforeEach, AfterEach, It, etc...) to the `GinkgoWriter`. This is useful for debugging stuck tests and tests that generate many logs. |
||||
|
- Improved output when an error occurs in a setup or teardown block. |
||||
|
- When `--dryRun` is set, Ginkgo will walk the spec tree and emit to its reporter *without* actually running anything. Best paired with `-v` to understand which specs will run in which order. |
||||
|
- Add `By` to help document long `It`s. `By` simply writes to the `GinkgoWriter`. |
||||
|
- Add support for precompiled tests: |
||||
|
- `ginkgo build <path-to-package>` will now compile the package, producing a file named `package.test` |
||||
|
- The compiled `package.test` file can be run directly. This runs the tests in series. |
||||
|
- To run precompiled tests in parallel, you can run: `ginkgo -p package.test` |
||||
|
- Support `bootstrap`ping and `generate`ing [Agouti](http://agouti.org) specs. |
||||
|
- `ginkgo generate` and `ginkgo bootstrap` now honor the package name already defined in a given directory |
||||
|
- The `ginkgo` CLI ignores `SIGQUIT`. Prevents its stack dump from interlacing with the underlying test suite's stack dump. |
||||
|
- The `ginkgo` CLI now compiles tests into a temporary directory instead of the package directory. This necessitates upgrading to Go v1.4+. |
||||
|
- `ginkgo -notify` now works on Linux |
||||
|
|
||||
|
Bug Fixes: |
||||
|
|
||||
|
- If --skipPackages is used and all packages are skipped, Ginkgo should exit 0. |
||||
|
- Fix tempfile leak when running in parallel |
||||
|
- Fix incorrect failure message when a panic occurs during a parallel test run |
||||
|
- Fixed an issue where a pending test within a focused context (or a focused test within a pending context) would skip all other tests. |
||||
|
- Be more consistent about handling SIGTERM as well as SIGINT |
||||
|
- When interupted while concurrently compiling test suites in the background, Ginkgo now cleans up the compiled artifacts. |
||||
|
- Fixed a long standing bug where `ginkgo -p` would hang if a process spawned by one of the Ginkgo parallel nodes does not exit. (Hooray!) |
||||
|
|
||||
|
## 1.1.0 (8/2/2014) |
||||
|
|
||||
|
No changes, just dropping the beta. |
||||
|
|
||||
|
## 1.1.0-beta (7/22/2014) |
||||
|
New Features: |
||||
|
|
||||
|
- `ginkgo watch` now monitors packages *and their dependencies* for changes. The depth of the dependency tree can be modified with the `-depth` flag. |
||||
|
- Test suites with a programmatic focus (`FIt`, `FDescribe`, etc...) exit with non-zero status code, even when they pass. This allows CI systems to detect accidental commits of focused test suites. |
||||
|
- `ginkgo -p` runs the testsuite in parallel with an auto-detected number of nodes. |
||||
|
- `ginkgo -tags=TAG_LIST` passes a list of tags down to the `go build` command. |
||||
|
- `ginkgo --failFast` aborts the test suite after the first failure. |
||||
|
- `ginkgo generate file_1 file_2` can take multiple file arguments. |
||||
|
- Ginkgo now summarizes any spec failures that occurred at the end of the test run. |
||||
|
- `ginkgo --randomizeSuites` will run tests *suites* in random order using the generated/passed-in seed. |
||||
|
|
||||
|
Improvements: |
||||
|
|
||||
|
- `ginkgo -skipPackage` now takes a comma-separated list of strings. If the *relative path* to a package matches one of the entries in the comma-separated list, that package is skipped. |
||||
|
- `ginkgo --untilItFails` no longer recompiles between attempts. |
||||
|
- Ginkgo now panics when a runnable node (`It`, `BeforeEach`, `JustBeforeEach`, `AfterEach`, `Measure`) is nested within another runnable node. This is always a mistake. Any test suites that panic because of this change should be fixed. |
||||
|
|
||||
|
Bug Fixes: |
||||
|
|
||||
|
- `ginkgo boostrap` and `ginkgo generate` no longer fail when dealing with `hyphen-separated-packages`. |
||||
|
- parallel specs are now better distributed across nodes - fixed a crashing bug where (for example) distributing 11 tests across 7 nodes would panic |
||||
|
|
||||
|
## 1.0.0 (5/24/2014) |
||||
|
New Features: |
||||
|
|
||||
|
- Add `GinkgoParallelNode()` - shorthand for `config.GinkgoConfig.ParallelNode` |
||||
|
|
||||
|
Improvements: |
||||
|
|
||||
|
- When compilation fails, the compilation output is rewritten to present a correct *relative* path. Allows ⌘-clicking in iTerm open the file in your text editor. |
||||
|
- `--untilItFails` and `ginkgo watch` now generate new random seeds between test runs, unless a particular random seed is specified. |
||||
|
|
||||
|
Bug Fixes: |
||||
|
|
||||
|
- `-cover` now generates a correctly combined coverprofile when running with in parallel with multiple `-node`s. |
||||
|
- Print out the contents of the `GinkgoWriter` when `BeforeSuite` or `AfterSuite` fail. |
||||
|
- Fix all remaining race conditions in Ginkgo's test suite. |
||||
|
|
||||
|
## 1.0.0-beta (4/14/2014) |
||||
|
Breaking changes: |
||||
|
|
||||
|
- `thirdparty/gomocktestreporter` is gone. Use `GinkgoT()` instead |
||||
|
- Modified the Reporter interface |
||||
|
- `watch` is now a subcommand, not a flag. |
||||
|
|
||||
|
DSL changes: |
||||
|
|
||||
|
- `BeforeSuite` and `AfterSuite` for setting up and tearing down test suites. |
||||
|
- `AfterSuite` is triggered on interrupt (`^C`) as well as exit. |
||||
|
- `SynchronizedBeforeSuite` and `SynchronizedAfterSuite` for setting up and tearing down singleton resources across parallel nodes. |
||||
|
|
||||
|
CLI changes: |
||||
|
|
||||
|
- `watch` is now a subcommand, not a flag |
||||
|
- `--nodot` flag can be passed to `ginkgo generate` and `ginkgo bootstrap` to avoid dot imports. This explicitly imports all exported identifiers in Ginkgo and Gomega. Refreshing this list can be done by running `ginkgo nodot` |
||||
|
- Additional arguments can be passed to specs. Pass them after the `--` separator |
||||
|
- `--skipPackage` flag takes a regexp and ignores any packages with package names passing said regexp. |
||||
|
- `--trace` flag prints out full stack traces when errors occur, not just the line at which the error occurs. |
||||
|
|
||||
|
Misc: |
||||
|
|
||||
|
- Start using semantic versioning |
||||
|
- Start maintaining changelog |
||||
|
|
||||
|
Major refactor: |
||||
|
|
||||
|
- Pull out Ginkgo's internal to `internal` |
||||
|
- Rename `example` everywhere to `spec` |
||||
|
- Much more! |
@ -0,0 +1,33 @@ |
|||||
|
# Contributing to Ginkgo |
||||
|
|
||||
|
Your contributions to Ginkgo are essential for its long-term maintenance and improvement. |
||||
|
|
||||
|
- Please **open an issue first** - describe what problem you are trying to solve and give the community a forum for input and feedback ahead of investing time in writing code! |
||||
|
- Ensure adequate test coverage: |
||||
|
- When adding to the Ginkgo library, add unit and/or integration tests (under the `integration` folder). |
||||
|
- When adding to the Ginkgo CLI, note that there are very few unit tests. Please add an integration test. |
||||
|
- Update the documentation. Ginko uses `godoc` comments and documentation on the `gh-pages` branch. |
||||
|
If relevant, please submit a docs PR to that branch alongside your code PR. |
||||
|
|
||||
|
Thanks for supporting Ginkgo! |
||||
|
|
||||
|
## Setup |
||||
|
|
||||
|
Fork the repo, then: |
||||
|
|
||||
|
``` |
||||
|
go get github.com/onsi/ginkgo |
||||
|
go get github.com/onsi/gomega/... |
||||
|
cd $GOPATH/src/github.com/onsi/ginkgo |
||||
|
git remote add fork git@github.com:<NAME>/ginkgo.git |
||||
|
|
||||
|
ginkgo -r -p # ensure tests are green |
||||
|
go vet ./... # ensure linter is happy |
||||
|
``` |
||||
|
|
||||
|
## Making the PR |
||||
|
- go to a new branch `git checkout -b my-feature` |
||||
|
- make your changes |
||||
|
- run tests and linter again (see above) |
||||
|
- `git push fork` |
||||
|
- open PR 🎉 |
@ -0,0 +1,20 @@ |
|||||
|
Copyright (c) 2013-2014 Onsi Fakhouri |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||
|
a copy of this software and associated documentation files (the |
||||
|
"Software"), to deal in the Software without restriction, including |
||||
|
without limitation the rights to use, copy, modify, merge, publish, |
||||
|
distribute, sublicense, and/or sell copies of the Software, and to |
||||
|
permit persons to whom the Software is furnished to do so, subject to |
||||
|
the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be |
||||
|
included in all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,174 @@ |
|||||
|
![Ginkgo: A Go BDD Testing Framework](https://onsi.github.io/ginkgo/images/ginkgo.png) |
||||
|
|
||||
|
[![Build Status](https://travis-ci.org/onsi/ginkgo.svg?branch=master)](https://travis-ci.org/onsi/ginkgo) |
||||
|
[![test](https://github.com/onsi/ginkgo/workflows/test/badge.svg?branch=master)](https://github.com/onsi/ginkgo/actions?query=workflow%3Atest+branch%3Amaster) |
||||
|
|
||||
|
Jump to the [docs](https://onsi.github.io/ginkgo/) | [中文文档](https://ke-chain.github.io/ginkgodoc) to learn more. To start rolling your Ginkgo tests *now* [keep reading](#set-me-up)! |
||||
|
|
||||
|
If you have a question, comment, bug report, feature request, etc. please open a GitHub issue, or visit the [Ginkgo Slack channel](https://app.slack.com/client/T029RQSE6/CQQ50BBNW). |
||||
|
|
||||
|
# Ginkgo 2.0 is coming soon! |
||||
|
|
||||
|
An effort is underway to develop and deliver Ginkgo 2.0. The work is happening in the [v2](https://github.com/onsi/ginkgo/tree/v2) branch and a changelog and migration guide is being maintained on that branch [here](https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md). Issue [#711](https://github.com/onsi/ginkgo/issues/711) is the central place for discussion and links to the original [proposal doc](https://docs.google.com/document/d/1h28ZknXRsTLPNNiOjdHIO-F2toCzq4xoZDXbfYaBdoQ/edit#). |
||||
|
|
||||
|
As described in the [changelog](https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md) and [proposal](https://docs.google.com/document/d/1h28ZknXRsTLPNNiOjdHIO-F2toCzq4xoZDXbfYaBdoQ/edit#), Ginkgo 2.0 will clean up the Ginkgo codebase, deprecate and remove some v1 functionality, and add several new much-requested features. To help users get ready for the migration, Ginkgo v1 has started emitting deprecation warnings for features that will no longer be supported with links to documentation for how to migrate away from these features. If you have concerns or comments please chime in on [#711](https://github.com/onsi/ginkgo/issues/711). |
||||
|
|
||||
|
The current timeline for completion of 2.0 looks like: |
||||
|
|
||||
|
- Early April 2021: first public release of 2.0, deprecation warnings land in v1. |
||||
|
- May 2021: first beta/rc of 2.0 with most new functionality in place. |
||||
|
- June/July 2021: 2.0 ships and fully replaces the 1.x codebase on master. |
||||
|
|
||||
|
## TLDR |
||||
|
Ginkgo builds on Go's `testing` package, allowing expressive [Behavior-Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) ("BDD") style tests. |
||||
|
It is typically (and optionally) paired with the [Gomega](https://github.com/onsi/gomega) matcher library. |
||||
|
|
||||
|
```go |
||||
|
Describe("the strings package", func() { |
||||
|
Context("strings.Contains()", func() { |
||||
|
When("the string contains the substring in the middle", func() { |
||||
|
It("returns `true`", func() { |
||||
|
Expect(strings.Contains("Ginkgo is awesome", "is")).To(BeTrue()) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
## Feature List |
||||
|
|
||||
|
- Ginkgo uses Go's `testing` package and can live alongside your existing `testing` tests. It's easy to [bootstrap](https://onsi.github.io/ginkgo/#bootstrapping-a-suite) and start writing your [first tests](https://onsi.github.io/ginkgo/#adding-specs-to-a-suite) |
||||
|
|
||||
|
- Ginkgo allows you to write tests in Go using expressive [Behavior-Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) ("BDD") style: |
||||
|
- Nestable [`Describe`, `Context` and `When` container blocks](https://onsi.github.io/ginkgo/#organizing-specs-with-containers-describe-and-context) |
||||
|
- [`BeforeEach` and `AfterEach` blocks](https://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach) for setup and teardown |
||||
|
- [`It` and `Specify` blocks](https://onsi.github.io/ginkgo/#individual-specs-it) that hold your assertions |
||||
|
- [`JustBeforeEach` blocks](https://onsi.github.io/ginkgo/#separating-creation-and-configuration-justbeforeeach) that separate creation from configuration (also known as the subject action pattern). |
||||
|
- [`BeforeSuite` and `AfterSuite` blocks](https://onsi.github.io/ginkgo/#global-setup-and-teardown-beforesuite-and-aftersuite) to prep for and cleanup after a suite. |
||||
|
|
||||
|
- A comprehensive test runner that lets you: |
||||
|
- Mark specs as [pending](https://onsi.github.io/ginkgo/#pending-specs) |
||||
|
- [Focus](https://onsi.github.io/ginkgo/#focused-specs) individual specs, and groups of specs, either programmatically or on the command line |
||||
|
- Run your tests in [random order](https://onsi.github.io/ginkgo/#spec-permutation), and then reuse random seeds to replicate the same order. |
||||
|
- Break up your test suite into parallel processes for straightforward [test parallelization](https://onsi.github.io/ginkgo/#parallel-specs) |
||||
|
|
||||
|
- `ginkgo`: a command line interface with plenty of handy command line arguments for [running your tests](https://onsi.github.io/ginkgo/#running-tests) and [generating](https://onsi.github.io/ginkgo/#generators) test files. Here are a few choice examples: |
||||
|
- `ginkgo -nodes=N` runs your tests in `N` parallel processes and print out coherent output in realtime |
||||
|
- `ginkgo -cover` runs your tests using Go's code coverage tool |
||||
|
- `ginkgo convert` converts an XUnit-style `testing` package to a Ginkgo-style package |
||||
|
- `ginkgo -focus="REGEXP"` and `ginkgo -skip="REGEXP"` allow you to specify a subset of tests to run via regular expression |
||||
|
- `ginkgo -r` runs all tests suites under the current directory |
||||
|
- `ginkgo -v` prints out identifying information for each tests just before it runs |
||||
|
|
||||
|
And much more: run `ginkgo help` for details! |
||||
|
|
||||
|
The `ginkgo` CLI is convenient, but purely optional -- Ginkgo works just fine with `go test` |
||||
|
|
||||
|
- `ginkgo watch` [watches](https://onsi.github.io/ginkgo/#watching-for-changes) packages *and their dependencies* for changes, then reruns tests. Run tests immediately as you develop! |
||||
|
|
||||
|
- Built-in support for testing [asynchronicity](https://onsi.github.io/ginkgo/#asynchronous-tests) |
||||
|
|
||||
|
- Built-in support for [benchmarking](https://onsi.github.io/ginkgo/#benchmark-tests) your code. Control the number of benchmark samples as you gather runtimes and other, arbitrary, bits of numerical information about your code. |
||||
|
|
||||
|
- [Completions for Sublime Text](https://github.com/onsi/ginkgo-sublime-completions): just use [Package Control](https://sublime.wbond.net/) to install `Ginkgo Completions`. |
||||
|
|
||||
|
- [Completions for VSCode](https://github.com/onsi/vscode-ginkgo): just use VSCode's extension installer to install `vscode-ginkgo`. |
||||
|
|
||||
|
- [Ginkgo tools for VSCode](https://marketplace.visualstudio.com/items?itemName=joselitofilho.ginkgotestexplorer): just use VSCode's extension installer to install `ginkgoTestExplorer`. |
||||
|
|
||||
|
- Straightforward support for third-party testing libraries such as [Gomock](https://code.google.com/p/gomock/) and [Testify](https://github.com/stretchr/testify). Check out the [docs](https://onsi.github.io/ginkgo/#third-party-integrations) for details. |
||||
|
|
||||
|
- A modular architecture that lets you easily: |
||||
|
- Write [custom reporters](https://onsi.github.io/ginkgo/#writing-custom-reporters) (for example, Ginkgo comes with a [JUnit XML reporter](https://onsi.github.io/ginkgo/#generating-junit-xml-output) and a TeamCity reporter). |
||||
|
- [Adapt an existing matcher library (or write your own!)](https://onsi.github.io/ginkgo/#using-other-matcher-libraries) to work with Ginkgo |
||||
|
|
||||
|
## [Gomega](https://github.com/onsi/gomega): Ginkgo's Preferred Matcher Library |
||||
|
|
||||
|
Ginkgo is best paired with Gomega. Learn more about Gomega [here](https://onsi.github.io/gomega/) |
||||
|
|
||||
|
## [Agouti](https://github.com/sclevine/agouti): A Go Acceptance Testing Framework |
||||
|
|
||||
|
Agouti allows you run WebDriver integration tests. Learn more about Agouti [here](https://agouti.org) |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You'll need the Go command-line tools. Follow the [installation instructions](https://golang.org/doc/install) if you don't have it installed. |
||||
|
|
||||
|
### Global installation |
||||
|
To install the Ginkgo command line interface: |
||||
|
```bash |
||||
|
go get -u github.com/onsi/ginkgo/ginkgo |
||||
|
``` |
||||
|
Note that this will install it to `$GOBIN`, which will need to be in the `$PATH` (or equivalent). Run `go help install` for more information. |
||||
|
|
||||
|
### Go module ["tools package"](https://github.com/golang/go/issues/25922): |
||||
|
Create (or update) a file called `tools/tools.go` with the following contents: |
||||
|
```go |
||||
|
// +build tools |
||||
|
|
||||
|
package tools |
||||
|
|
||||
|
import ( |
||||
|
_ "github.com/onsi/ginkgo/ginkgo" |
||||
|
) |
||||
|
|
||||
|
// This file imports packages that are used when running go generate, or used |
||||
|
// during the development process but not otherwise depended on by built code. |
||||
|
``` |
||||
|
The Ginkgo command can then be run via `go run github.com/onsi/ginkgo/ginkgo`. |
||||
|
This approach allows the version of Ginkgo to be maintained under source control for reproducible results, |
||||
|
and is well suited to automated test pipelines. |
||||
|
|
||||
|
### Bootstrapping |
||||
|
```bash |
||||
|
cd path/to/package/you/want/to/test |
||||
|
|
||||
|
ginkgo bootstrap # set up a new ginkgo suite |
||||
|
ginkgo generate # will create a sample test file. edit this file and add your tests then... |
||||
|
|
||||
|
go test # to run your tests |
||||
|
|
||||
|
ginkgo # also runs your tests |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## I'm new to Go: What are my testing options? |
||||
|
|
||||
|
Of course, I heartily recommend [Ginkgo](https://github.com/onsi/ginkgo) and [Gomega](https://github.com/onsi/gomega). Both packages are seeing heavy, daily, production use on a number of projects and boast a mature and comprehensive feature-set. |
||||
|
|
||||
|
With that said, it's great to know what your options are :) |
||||
|
|
||||
|
### What Go gives you out of the box |
||||
|
|
||||
|
Testing is a first class citizen in Go, however Go's built-in testing primitives are somewhat limited: The [testing](https://golang.org/pkg/testing) package provides basic XUnit style tests and no assertion library. |
||||
|
|
||||
|
### Matcher libraries for Go's XUnit style tests |
||||
|
|
||||
|
A number of matcher libraries have been written to augment Go's built-in XUnit style tests. Here are two that have gained traction: |
||||
|
|
||||
|
- [testify](https://github.com/stretchr/testify) |
||||
|
- [gocheck](https://labix.org/gocheck) |
||||
|
|
||||
|
You can also use Ginkgo's matcher library [Gomega](https://github.com/onsi/gomega) in [XUnit style tests](https://onsi.github.io/gomega/#using-gomega-with-golangs-xunitstyle-tests) |
||||
|
|
||||
|
### BDD style testing frameworks |
||||
|
|
||||
|
There are a handful of BDD-style testing frameworks written for Go. Here are a few: |
||||
|
|
||||
|
- [Ginkgo](https://github.com/onsi/ginkgo) ;) |
||||
|
- [GoConvey](https://github.com/smartystreets/goconvey) |
||||
|
- [Goblin](https://github.com/franela/goblin) |
||||
|
- [Mao](https://github.com/azer/mao) |
||||
|
- [Zen](https://github.com/pranavraja/zen) |
||||
|
|
||||
|
Finally, @shageman has [put together](https://github.com/shageman/gotestit) a comprehensive comparison of Go testing libraries. |
||||
|
|
||||
|
Go explore! |
||||
|
|
||||
|
## License |
||||
|
|
||||
|
Ginkgo is MIT-Licensed |
||||
|
|
||||
|
## Contributing |
||||
|
|
||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) |
@ -0,0 +1,14 @@ |
|||||
|
A Ginkgo release is a tagged git sha and a GitHub release. To cut a release: |
||||
|
|
||||
|
1. Ensure CHANGELOG.md is up to date. |
||||
|
- Use `git log --pretty=format:'- %s [%h]' HEAD...vX.X.X` to list all the commits since the last release |
||||
|
- Categorize the changes into |
||||
|
- Breaking Changes (requires a major version) |
||||
|
- New Features (minor version) |
||||
|
- Fixes (fix version) |
||||
|
- Maintenance (which in general should not be mentioned in `CHANGELOG.md` as they have no user impact) |
||||
|
1. Update `VERSION` in `config/config.go` |
||||
|
1. Create a commit with the version number as the commit message (e.g. `v1.3.0`) |
||||
|
1. Tag the commit with the version number as the tag name (e.g. `v1.3.0`) |
||||
|
1. Push the commit and tag to GitHub |
||||
|
1. Create a new [GitHub release](https://help.github.com/articles/creating-releases/) with the version number as the tag (e.g. `v1.3.0`). List the key changes in the release notes. |
@ -0,0 +1,232 @@ |
|||||
|
/* |
||||
|
Ginkgo accepts a number of configuration options. |
||||
|
|
||||
|
These are documented [here](http://onsi.github.io/ginkgo/#the-ginkgo-cli)
|
||||
|
|
||||
|
You can also learn more via |
||||
|
|
||||
|
ginkgo help |
||||
|
|
||||
|
or (I kid you not): |
||||
|
|
||||
|
go test -asdf |
||||
|
*/ |
||||
|
package config |
||||
|
|
||||
|
import ( |
||||
|
"flag" |
||||
|
"time" |
||||
|
|
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
const VERSION = "1.16.2" |
||||
|
|
||||
|
type GinkgoConfigType struct { |
||||
|
RandomSeed int64 |
||||
|
RandomizeAllSpecs bool |
||||
|
RegexScansFilePath bool |
||||
|
FocusStrings []string |
||||
|
SkipStrings []string |
||||
|
SkipMeasurements bool |
||||
|
FailOnPending bool |
||||
|
FailFast bool |
||||
|
FlakeAttempts int |
||||
|
EmitSpecProgress bool |
||||
|
DryRun bool |
||||
|
DebugParallel bool |
||||
|
|
||||
|
ParallelNode int |
||||
|
ParallelTotal int |
||||
|
SyncHost string |
||||
|
StreamHost string |
||||
|
} |
||||
|
|
||||
|
var GinkgoConfig = GinkgoConfigType{} |
||||
|
|
||||
|
type DefaultReporterConfigType struct { |
||||
|
NoColor bool |
||||
|
SlowSpecThreshold float64 |
||||
|
NoisyPendings bool |
||||
|
NoisySkippings bool |
||||
|
Succinct bool |
||||
|
Verbose bool |
||||
|
FullTrace bool |
||||
|
ReportPassed bool |
||||
|
ReportFile string |
||||
|
} |
||||
|
|
||||
|
var DefaultReporterConfig = DefaultReporterConfigType{} |
||||
|
|
||||
|
func processPrefix(prefix string) string { |
||||
|
if prefix != "" { |
||||
|
prefix += "." |
||||
|
} |
||||
|
return prefix |
||||
|
} |
||||
|
|
||||
|
type flagFunc func(string) |
||||
|
|
||||
|
func (f flagFunc) String() string { return "" } |
||||
|
func (f flagFunc) Set(s string) error { f(s); return nil } |
||||
|
|
||||
|
func Flags(flagSet *flag.FlagSet, prefix string, includeParallelFlags bool) { |
||||
|
prefix = processPrefix(prefix) |
||||
|
flagSet.Int64Var(&(GinkgoConfig.RandomSeed), prefix+"seed", time.Now().Unix(), "The seed used to randomize the spec suite.") |
||||
|
flagSet.BoolVar(&(GinkgoConfig.RandomizeAllSpecs), prefix+"randomizeAllSpecs", false, "If set, ginkgo will randomize all specs together. By default, ginkgo only randomizes the top level Describe, Context and When groups.") |
||||
|
flagSet.BoolVar(&(GinkgoConfig.SkipMeasurements), prefix+"skipMeasurements", false, "If set, ginkgo will skip any measurement specs.") |
||||
|
flagSet.BoolVar(&(GinkgoConfig.FailOnPending), prefix+"failOnPending", false, "If set, ginkgo will mark the test suite as failed if any specs are pending.") |
||||
|
flagSet.BoolVar(&(GinkgoConfig.FailFast), prefix+"failFast", false, "If set, ginkgo will stop running a test suite after a failure occurs.") |
||||
|
|
||||
|
flagSet.BoolVar(&(GinkgoConfig.DryRun), prefix+"dryRun", false, "If set, ginkgo will walk the test hierarchy without actually running anything. Best paired with -v.") |
||||
|
|
||||
|
flagSet.Var(flagFunc(flagFocus), prefix+"focus", "If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.") |
||||
|
flagSet.Var(flagFunc(flagSkip), prefix+"skip", "If set, ginkgo will only run specs that do not match this regular expression. Can be specified multiple times, values are ORed.") |
||||
|
|
||||
|
flagSet.BoolVar(&(GinkgoConfig.RegexScansFilePath), prefix+"regexScansFilePath", false, "If set, ginkgo regex matching also will look at the file path (code location).") |
||||
|
|
||||
|
flagSet.IntVar(&(GinkgoConfig.FlakeAttempts), prefix+"flakeAttempts", 1, "Make up to this many attempts to run each spec. Please note that if any of the attempts succeed, the suite will not be failed. But any failures will still be recorded.") |
||||
|
|
||||
|
flagSet.BoolVar(&(GinkgoConfig.EmitSpecProgress), prefix+"progress", false, "If set, ginkgo will emit progress information as each spec runs to the GinkgoWriter.") |
||||
|
|
||||
|
flagSet.BoolVar(&(GinkgoConfig.DebugParallel), prefix+"debug", false, "If set, ginkgo will emit node output to files when running in parallel.") |
||||
|
|
||||
|
if includeParallelFlags { |
||||
|
flagSet.IntVar(&(GinkgoConfig.ParallelNode), prefix+"parallel.node", 1, "This worker node's (one-indexed) node number. For running specs in parallel.") |
||||
|
flagSet.IntVar(&(GinkgoConfig.ParallelTotal), prefix+"parallel.total", 1, "The total number of worker nodes. For running specs in parallel.") |
||||
|
flagSet.StringVar(&(GinkgoConfig.SyncHost), prefix+"parallel.synchost", "", "The address for the server that will synchronize the running nodes.") |
||||
|
flagSet.StringVar(&(GinkgoConfig.StreamHost), prefix+"parallel.streamhost", "", "The address for the server that the running nodes should stream data to.") |
||||
|
} |
||||
|
|
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.NoColor), prefix+"noColor", false, "If set, suppress color output in default reporter.") |
||||
|
flagSet.Float64Var(&(DefaultReporterConfig.SlowSpecThreshold), prefix+"slowSpecThreshold", 5.0, "(in seconds) Specs that take longer to run than this threshold are flagged as slow by the default reporter.") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.NoisyPendings), prefix+"noisyPendings", true, "If set, default reporter will shout about pending tests.") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.NoisySkippings), prefix+"noisySkippings", true, "If set, default reporter will shout about skipping tests.") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.Verbose), prefix+"v", false, "If set, default reporter print out all specs as they begin.") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.Succinct), prefix+"succinct", false, "If set, default reporter prints out a very succinct report") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.FullTrace), prefix+"trace", false, "If set, default reporter prints out the full stack trace when a failure occurs") |
||||
|
flagSet.BoolVar(&(DefaultReporterConfig.ReportPassed), prefix+"reportPassed", false, "If set, default reporter prints out captured output of passed tests.") |
||||
|
flagSet.StringVar(&(DefaultReporterConfig.ReportFile), prefix+"reportFile", "", "Override the default reporter output file path.") |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func BuildFlagArgs(prefix string, ginkgo GinkgoConfigType, reporter DefaultReporterConfigType) []string { |
||||
|
prefix = processPrefix(prefix) |
||||
|
result := make([]string, 0) |
||||
|
|
||||
|
if ginkgo.RandomSeed > 0 { |
||||
|
result = append(result, fmt.Sprintf("--%sseed=%d", prefix, ginkgo.RandomSeed)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.RandomizeAllSpecs { |
||||
|
result = append(result, fmt.Sprintf("--%srandomizeAllSpecs", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.SkipMeasurements { |
||||
|
result = append(result, fmt.Sprintf("--%sskipMeasurements", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.FailOnPending { |
||||
|
result = append(result, fmt.Sprintf("--%sfailOnPending", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.FailFast { |
||||
|
result = append(result, fmt.Sprintf("--%sfailFast", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.DryRun { |
||||
|
result = append(result, fmt.Sprintf("--%sdryRun", prefix)) |
||||
|
} |
||||
|
|
||||
|
for _, s := range ginkgo.FocusStrings { |
||||
|
result = append(result, fmt.Sprintf("--%sfocus=%s", prefix, s)) |
||||
|
} |
||||
|
|
||||
|
for _, s := range ginkgo.SkipStrings { |
||||
|
result = append(result, fmt.Sprintf("--%sskip=%s", prefix, s)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.FlakeAttempts > 1 { |
||||
|
result = append(result, fmt.Sprintf("--%sflakeAttempts=%d", prefix, ginkgo.FlakeAttempts)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.EmitSpecProgress { |
||||
|
result = append(result, fmt.Sprintf("--%sprogress", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.DebugParallel { |
||||
|
result = append(result, fmt.Sprintf("--%sdebug", prefix)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.ParallelNode != 0 { |
||||
|
result = append(result, fmt.Sprintf("--%sparallel.node=%d", prefix, ginkgo.ParallelNode)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.ParallelTotal != 0 { |
||||
|
result = append(result, fmt.Sprintf("--%sparallel.total=%d", prefix, ginkgo.ParallelTotal)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.StreamHost != "" { |
||||
|
result = append(result, fmt.Sprintf("--%sparallel.streamhost=%s", prefix, ginkgo.StreamHost)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.SyncHost != "" { |
||||
|
result = append(result, fmt.Sprintf("--%sparallel.synchost=%s", prefix, ginkgo.SyncHost)) |
||||
|
} |
||||
|
|
||||
|
if ginkgo.RegexScansFilePath { |
||||
|
result = append(result, fmt.Sprintf("--%sregexScansFilePath", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.NoColor { |
||||
|
result = append(result, fmt.Sprintf("--%snoColor", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.SlowSpecThreshold > 0 { |
||||
|
result = append(result, fmt.Sprintf("--%sslowSpecThreshold=%.5f", prefix, reporter.SlowSpecThreshold)) |
||||
|
} |
||||
|
|
||||
|
if !reporter.NoisyPendings { |
||||
|
result = append(result, fmt.Sprintf("--%snoisyPendings=false", prefix)) |
||||
|
} |
||||
|
|
||||
|
if !reporter.NoisySkippings { |
||||
|
result = append(result, fmt.Sprintf("--%snoisySkippings=false", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.Verbose { |
||||
|
result = append(result, fmt.Sprintf("--%sv", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.Succinct { |
||||
|
result = append(result, fmt.Sprintf("--%ssuccinct", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.FullTrace { |
||||
|
result = append(result, fmt.Sprintf("--%strace", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.ReportPassed { |
||||
|
result = append(result, fmt.Sprintf("--%sreportPassed", prefix)) |
||||
|
} |
||||
|
|
||||
|
if reporter.ReportFile != "" { |
||||
|
result = append(result, fmt.Sprintf("--%sreportFile=%s", prefix, reporter.ReportFile)) |
||||
|
} |
||||
|
|
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
// flagFocus implements the -focus flag.
|
||||
|
func flagFocus(arg string) { |
||||
|
if arg != "" { |
||||
|
GinkgoConfig.FocusStrings = append(GinkgoConfig.FocusStrings, arg) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// flagSkip implements the -skip flag.
|
||||
|
func flagSkip(arg string) { |
||||
|
if arg != "" { |
||||
|
GinkgoConfig.SkipStrings = append(GinkgoConfig.SkipStrings, arg) |
||||
|
} |
||||
|
} |
@ -0,0 +1,190 @@ |
|||||
|
package formatter |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"regexp" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
const COLS = 80 |
||||
|
|
||||
|
type ColorMode uint8 |
||||
|
|
||||
|
const ( |
||||
|
ColorModeNone ColorMode = iota |
||||
|
ColorModeTerminal |
||||
|
ColorModePassthrough |
||||
|
) |
||||
|
|
||||
|
var SingletonFormatter = New(ColorModeTerminal) |
||||
|
|
||||
|
func F(format string, args ...interface{}) string { |
||||
|
return SingletonFormatter.F(format, args...) |
||||
|
} |
||||
|
|
||||
|
func Fi(indentation uint, format string, args ...interface{}) string { |
||||
|
return SingletonFormatter.Fi(indentation, format, args...) |
||||
|
} |
||||
|
|
||||
|
func Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string { |
||||
|
return SingletonFormatter.Fiw(indentation, maxWidth, format, args...) |
||||
|
} |
||||
|
|
||||
|
type Formatter struct { |
||||
|
ColorMode ColorMode |
||||
|
colors map[string]string |
||||
|
styleRe *regexp.Regexp |
||||
|
preserveColorStylingTags bool |
||||
|
} |
||||
|
|
||||
|
func NewWithNoColorBool(noColor bool) Formatter { |
||||
|
if noColor { |
||||
|
return New(ColorModeNone) |
||||
|
} |
||||
|
return New(ColorModeTerminal) |
||||
|
} |
||||
|
|
||||
|
func New(colorMode ColorMode) Formatter { |
||||
|
f := Formatter{ |
||||
|
ColorMode: colorMode, |
||||
|
colors: map[string]string{ |
||||
|
"/": "\x1b[0m", |
||||
|
"bold": "\x1b[1m", |
||||
|
"underline": "\x1b[4m", |
||||
|
|
||||
|
"red": "\x1b[38;5;9m", |
||||
|
"orange": "\x1b[38;5;214m", |
||||
|
"coral": "\x1b[38;5;204m", |
||||
|
"magenta": "\x1b[38;5;13m", |
||||
|
"green": "\x1b[38;5;10m", |
||||
|
"dark-green": "\x1b[38;5;28m", |
||||
|
"yellow": "\x1b[38;5;11m", |
||||
|
"light-yellow": "\x1b[38;5;228m", |
||||
|
"cyan": "\x1b[38;5;14m", |
||||
|
"gray": "\x1b[38;5;243m", |
||||
|
"light-gray": "\x1b[38;5;246m", |
||||
|
"blue": "\x1b[38;5;12m", |
||||
|
}, |
||||
|
} |
||||
|
colors := []string{} |
||||
|
for color := range f.colors { |
||||
|
colors = append(colors, color) |
||||
|
} |
||||
|
f.styleRe = regexp.MustCompile("{{(" + strings.Join(colors, "|") + ")}}") |
||||
|
return f |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) F(format string, args ...interface{}) string { |
||||
|
return f.Fi(0, format, args...) |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) Fi(indentation uint, format string, args ...interface{}) string { |
||||
|
return f.Fiw(indentation, 0, format, args...) |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string { |
||||
|
out := fmt.Sprintf(f.style(format), args...) |
||||
|
|
||||
|
if indentation == 0 && maxWidth == 0 { |
||||
|
return out |
||||
|
} |
||||
|
|
||||
|
lines := strings.Split(out, "\n") |
||||
|
|
||||
|
if maxWidth != 0 { |
||||
|
outLines := []string{} |
||||
|
|
||||
|
maxWidth = maxWidth - indentation*2 |
||||
|
for _, line := range lines { |
||||
|
if f.length(line) <= maxWidth { |
||||
|
outLines = append(outLines, line) |
||||
|
continue |
||||
|
} |
||||
|
outWords := []string{} |
||||
|
length := uint(0) |
||||
|
words := strings.Split(line, " ") |
||||
|
for _, word := range words { |
||||
|
wordLength := f.length(word) |
||||
|
if length+wordLength <= maxWidth { |
||||
|
length += wordLength |
||||
|
outWords = append(outWords, word) |
||||
|
continue |
||||
|
} |
||||
|
outLines = append(outLines, strings.Join(outWords, " ")) |
||||
|
outWords = []string{word} |
||||
|
length = wordLength |
||||
|
} |
||||
|
if len(outWords) > 0 { |
||||
|
outLines = append(outLines, strings.Join(outWords, " ")) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
lines = outLines |
||||
|
} |
||||
|
|
||||
|
if indentation == 0 { |
||||
|
return strings.Join(lines, "\n") |
||||
|
} |
||||
|
|
||||
|
padding := strings.Repeat(" ", int(indentation)) |
||||
|
for i := range lines { |
||||
|
if lines[i] != "" { |
||||
|
lines[i] = padding + lines[i] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return strings.Join(lines, "\n") |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) length(styled string) uint { |
||||
|
n := uint(0) |
||||
|
inStyle := false |
||||
|
for _, b := range styled { |
||||
|
if inStyle { |
||||
|
if b == 'm' { |
||||
|
inStyle = false |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
if b == '\x1b' { |
||||
|
inStyle = true |
||||
|
continue |
||||
|
} |
||||
|
n += 1 |
||||
|
} |
||||
|
return n |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) CycleJoin(elements []string, joiner string, cycle []string) string { |
||||
|
if len(elements) == 0 { |
||||
|
return "" |
||||
|
} |
||||
|
n := len(cycle) |
||||
|
out := "" |
||||
|
for i, text := range elements { |
||||
|
out += cycle[i%n] + text |
||||
|
if i < len(elements)-1 { |
||||
|
out += joiner |
||||
|
} |
||||
|
} |
||||
|
out += "{{/}}" |
||||
|
return f.style(out) |
||||
|
} |
||||
|
|
||||
|
func (f Formatter) style(s string) string { |
||||
|
switch f.ColorMode { |
||||
|
case ColorModeNone: |
||||
|
return f.styleRe.ReplaceAllString(s, "") |
||||
|
case ColorModePassthrough: |
||||
|
return s |
||||
|
case ColorModeTerminal: |
||||
|
return f.styleRe.ReplaceAllStringFunc(s, func(match string) string { |
||||
|
if out, ok := f.colors[strings.Trim(match, "{}")]; ok { |
||||
|
return out |
||||
|
} |
||||
|
return match |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return "" |
||||
|
} |
@ -0,0 +1,670 @@ |
|||||
|
/* |
||||
|
Ginkgo is a BDD-style testing framework for Golang |
||||
|
|
||||
|
The godoc documentation describes Ginkgo's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/ginkgo/
|
||||
|
|
||||
|
Ginkgo's preferred matcher library is [Gomega](http://github.com/onsi/gomega)
|
||||
|
|
||||
|
Ginkgo on Github: http://github.com/onsi/ginkgo
|
||||
|
|
||||
|
Ginkgo is MIT-Licensed |
||||
|
*/ |
||||
|
package ginkgo |
||||
|
|
||||
|
import ( |
||||
|
"flag" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/internal/codelocation" |
||||
|
"github.com/onsi/ginkgo/internal/global" |
||||
|
"github.com/onsi/ginkgo/internal/remote" |
||||
|
"github.com/onsi/ginkgo/internal/testingtproxy" |
||||
|
"github.com/onsi/ginkgo/internal/writer" |
||||
|
"github.com/onsi/ginkgo/reporters" |
||||
|
"github.com/onsi/ginkgo/reporters/stenographer" |
||||
|
colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
var deprecationTracker = types.NewDeprecationTracker() |
||||
|
|
||||
|
const GINKGO_VERSION = config.VERSION |
||||
|
const GINKGO_PANIC = ` |
||||
|
Your test failed. |
||||
|
Ginkgo panics to prevent subsequent assertions from running. |
||||
|
Normally Ginkgo rescues this panic so you shouldn't see it. |
||||
|
|
||||
|
But, if you make an assertion in a goroutine, Ginkgo can't capture the panic. |
||||
|
To circumvent this, you should call |
||||
|
|
||||
|
defer GinkgoRecover() |
||||
|
|
||||
|
at the top of the goroutine that caused this panic. |
||||
|
` |
||||
|
|
||||
|
func init() { |
||||
|
config.Flags(flag.CommandLine, "ginkgo", true) |
||||
|
GinkgoWriter = writer.New(os.Stdout) |
||||
|
} |
||||
|
|
||||
|
//GinkgoWriter implements an io.Writer
|
||||
|
//When running in verbose mode any writes to GinkgoWriter will be immediately printed
|
||||
|
//to stdout. Otherwise, GinkgoWriter will buffer any writes produced during the current test and flush them to screen
|
||||
|
//only if the current test fails.
|
||||
|
var GinkgoWriter io.Writer |
||||
|
|
||||
|
//The interface by which Ginkgo receives *testing.T
|
||||
|
type GinkgoTestingT interface { |
||||
|
Fail() |
||||
|
} |
||||
|
|
||||
|
//GinkgoRandomSeed returns the seed used to randomize spec execution order. It is
|
||||
|
//useful for seeding your own pseudorandom number generators (PRNGs) to ensure
|
||||
|
//consistent executions from run to run, where your tests contain variability (for
|
||||
|
//example, when selecting random test data).
|
||||
|
func GinkgoRandomSeed() int64 { |
||||
|
return config.GinkgoConfig.RandomSeed |
||||
|
} |
||||
|
|
||||
|
//GinkgoParallelNode returns the parallel node number for the current ginkgo process
|
||||
|
//The node number is 1-indexed
|
||||
|
func GinkgoParallelNode() int { |
||||
|
return config.GinkgoConfig.ParallelNode |
||||
|
} |
||||
|
|
||||
|
//Some matcher libraries or legacy codebases require a *testing.T
|
||||
|
//GinkgoT implements an interface analogous to *testing.T and can be used if
|
||||
|
//the library in question accepts *testing.T through an interface
|
||||
|
//
|
||||
|
// For example, with testify:
|
||||
|
// assert.Equal(GinkgoT(), 123, 123, "they should be equal")
|
||||
|
//
|
||||
|
// Or with gomock:
|
||||
|
// gomock.NewController(GinkgoT())
|
||||
|
//
|
||||
|
// GinkgoT() takes an optional offset argument that can be used to get the
|
||||
|
// correct line number associated with the failure.
|
||||
|
func GinkgoT(optionalOffset ...int) GinkgoTInterface { |
||||
|
offset := 3 |
||||
|
if len(optionalOffset) > 0 { |
||||
|
offset = optionalOffset[0] |
||||
|
} |
||||
|
failedFunc := func() bool { |
||||
|
return CurrentGinkgoTestDescription().Failed |
||||
|
} |
||||
|
nameFunc := func() string { |
||||
|
return CurrentGinkgoTestDescription().FullTestText |
||||
|
} |
||||
|
return testingtproxy.New(GinkgoWriter, Fail, Skip, failedFunc, nameFunc, offset) |
||||
|
} |
||||
|
|
||||
|
//The interface returned by GinkgoT(). This covers most of the methods
|
||||
|
//in the testing package's T.
|
||||
|
type GinkgoTInterface interface { |
||||
|
Cleanup(func()) |
||||
|
Error(args ...interface{}) |
||||
|
Errorf(format string, args ...interface{}) |
||||
|
Fail() |
||||
|
FailNow() |
||||
|
Failed() bool |
||||
|
Fatal(args ...interface{}) |
||||
|
Fatalf(format string, args ...interface{}) |
||||
|
Helper() |
||||
|
Log(args ...interface{}) |
||||
|
Logf(format string, args ...interface{}) |
||||
|
Name() string |
||||
|
Parallel() |
||||
|
Skip(args ...interface{}) |
||||
|
SkipNow() |
||||
|
Skipf(format string, args ...interface{}) |
||||
|
Skipped() bool |
||||
|
TempDir() string |
||||
|
} |
||||
|
|
||||
|
//Custom Ginkgo test reporters must implement the Reporter interface.
|
||||
|
//
|
||||
|
//The custom reporter is passed in a SuiteSummary when the suite begins and ends,
|
||||
|
//and a SpecSummary just before a spec begins and just after a spec ends
|
||||
|
type Reporter reporters.Reporter |
||||
|
|
||||
|
//Asynchronous specs are given a channel of the Done type. You must close or write to the channel
|
||||
|
//to tell Ginkgo that your async test is done.
|
||||
|
type Done chan<- interface{} |
||||
|
|
||||
|
//GinkgoTestDescription represents the information about the current running test returned by CurrentGinkgoTestDescription
|
||||
|
// FullTestText: a concatenation of ComponentTexts and the TestText
|
||||
|
// ComponentTexts: a list of all texts for the Describes & Contexts leading up to the current test
|
||||
|
// TestText: the text in the actual It or Measure node
|
||||
|
// IsMeasurement: true if the current test is a measurement
|
||||
|
// FileName: the name of the file containing the current test
|
||||
|
// LineNumber: the line number for the current test
|
||||
|
// Failed: if the current test has failed, this will be true (useful in an AfterEach)
|
||||
|
type GinkgoTestDescription struct { |
||||
|
FullTestText string |
||||
|
ComponentTexts []string |
||||
|
TestText string |
||||
|
|
||||
|
IsMeasurement bool |
||||
|
|
||||
|
FileName string |
||||
|
LineNumber int |
||||
|
|
||||
|
Failed bool |
||||
|
Duration time.Duration |
||||
|
} |
||||
|
|
||||
|
//CurrentGinkgoTestDescripton returns information about the current running test.
|
||||
|
func CurrentGinkgoTestDescription() GinkgoTestDescription { |
||||
|
summary, ok := global.Suite.CurrentRunningSpecSummary() |
||||
|
if !ok { |
||||
|
return GinkgoTestDescription{} |
||||
|
} |
||||
|
|
||||
|
subjectCodeLocation := summary.ComponentCodeLocations[len(summary.ComponentCodeLocations)-1] |
||||
|
|
||||
|
return GinkgoTestDescription{ |
||||
|
ComponentTexts: summary.ComponentTexts[1:], |
||||
|
FullTestText: strings.Join(summary.ComponentTexts[1:], " "), |
||||
|
TestText: summary.ComponentTexts[len(summary.ComponentTexts)-1], |
||||
|
IsMeasurement: summary.IsMeasurement, |
||||
|
FileName: subjectCodeLocation.FileName, |
||||
|
LineNumber: subjectCodeLocation.LineNumber, |
||||
|
Failed: summary.HasFailureState(), |
||||
|
Duration: summary.RunTime, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Measurement tests receive a Benchmarker.
|
||||
|
//
|
||||
|
//You use the Time() function to time how long the passed in body function takes to run
|
||||
|
//You use the RecordValue() function to track arbitrary numerical measurements.
|
||||
|
//The RecordValueWithPrecision() function can be used alternatively to provide the unit
|
||||
|
//and resolution of the numeric measurement.
|
||||
|
//The optional info argument is passed to the test reporter and can be used to
|
||||
|
// provide the measurement data to a custom reporter with context.
|
||||
|
//
|
||||
|
//See http://onsi.github.io/ginkgo/#benchmark_tests for more details
|
||||
|
type Benchmarker interface { |
||||
|
Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) |
||||
|
RecordValue(name string, value float64, info ...interface{}) |
||||
|
RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) |
||||
|
} |
||||
|
|
||||
|
//RunSpecs is the entry point for the Ginkgo test runner.
|
||||
|
//You must call this within a Golang testing TestX(t *testing.T) function.
|
||||
|
//
|
||||
|
//To bootstrap a test suite you can use the Ginkgo CLI:
|
||||
|
//
|
||||
|
// ginkgo bootstrap
|
||||
|
func RunSpecs(t GinkgoTestingT, description string) bool { |
||||
|
specReporters := []Reporter{buildDefaultReporter()} |
||||
|
if config.DefaultReporterConfig.ReportFile != "" { |
||||
|
reportFile := config.DefaultReporterConfig.ReportFile |
||||
|
specReporters[0] = reporters.NewJUnitReporter(reportFile) |
||||
|
specReporters = append(specReporters, buildDefaultReporter()) |
||||
|
} |
||||
|
return runSpecsWithCustomReporters(t, description, specReporters) |
||||
|
} |
||||
|
|
||||
|
//To run your tests with Ginkgo's default reporter and your custom reporter(s), replace
|
||||
|
//RunSpecs() with this method.
|
||||
|
func RunSpecsWithDefaultAndCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { |
||||
|
deprecationTracker.TrackDeprecation(types.Deprecations.CustomReporter()) |
||||
|
specReporters = append(specReporters, buildDefaultReporter()) |
||||
|
return runSpecsWithCustomReporters(t, description, specReporters) |
||||
|
} |
||||
|
|
||||
|
//To run your tests with your custom reporter(s) (and *not* Ginkgo's default reporter), replace
|
||||
|
//RunSpecs() with this method. Note that parallel tests will not work correctly without the default reporter
|
||||
|
func RunSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { |
||||
|
deprecationTracker.TrackDeprecation(types.Deprecations.CustomReporter()) |
||||
|
return runSpecsWithCustomReporters(t, description, specReporters) |
||||
|
} |
||||
|
|
||||
|
func runSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { |
||||
|
writer := GinkgoWriter.(*writer.Writer) |
||||
|
writer.SetStream(config.DefaultReporterConfig.Verbose) |
||||
|
reporters := make([]reporters.Reporter, len(specReporters)) |
||||
|
for i, reporter := range specReporters { |
||||
|
reporters[i] = reporter |
||||
|
} |
||||
|
passed, hasFocusedTests := global.Suite.Run(t, description, reporters, writer, config.GinkgoConfig) |
||||
|
|
||||
|
if deprecationTracker.DidTrackDeprecations() { |
||||
|
fmt.Fprintln(colorable.NewColorableStderr(), deprecationTracker.DeprecationsReport()) |
||||
|
} |
||||
|
|
||||
|
if passed && hasFocusedTests && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" { |
||||
|
fmt.Println("PASS | FOCUSED") |
||||
|
os.Exit(types.GINKGO_FOCUS_EXIT_CODE) |
||||
|
} |
||||
|
return passed |
||||
|
} |
||||
|
|
||||
|
func buildDefaultReporter() Reporter { |
||||
|
remoteReportingServer := config.GinkgoConfig.StreamHost |
||||
|
if remoteReportingServer == "" { |
||||
|
stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor, config.GinkgoConfig.FlakeAttempts > 1, colorable.NewColorableStdout()) |
||||
|
return reporters.NewDefaultReporter(config.DefaultReporterConfig, stenographer) |
||||
|
} else { |
||||
|
debugFile := "" |
||||
|
if config.GinkgoConfig.DebugParallel { |
||||
|
debugFile = fmt.Sprintf("ginkgo-node-%d.log", config.GinkgoConfig.ParallelNode) |
||||
|
} |
||||
|
return remote.NewForwardingReporter(config.DefaultReporterConfig, remoteReportingServer, &http.Client{}, remote.NewOutputInterceptor(), GinkgoWriter.(*writer.Writer), debugFile) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Skip notifies Ginkgo that the current spec was skipped.
|
||||
|
func Skip(message string, callerSkip ...int) { |
||||
|
skip := 0 |
||||
|
if len(callerSkip) > 0 { |
||||
|
skip = callerSkip[0] |
||||
|
} |
||||
|
|
||||
|
global.Failer.Skip(message, codelocation.New(skip+1)) |
||||
|
panic(GINKGO_PANIC) |
||||
|
} |
||||
|
|
||||
|
//Fail notifies Ginkgo that the current spec has failed. (Gomega will call Fail for you automatically when an assertion fails.)
|
||||
|
func Fail(message string, callerSkip ...int) { |
||||
|
skip := 0 |
||||
|
if len(callerSkip) > 0 { |
||||
|
skip = callerSkip[0] |
||||
|
} |
||||
|
|
||||
|
global.Failer.Fail(message, codelocation.New(skip+1)) |
||||
|
panic(GINKGO_PANIC) |
||||
|
} |
||||
|
|
||||
|
//GinkgoRecover should be deferred at the top of any spawned goroutine that (may) call `Fail`
|
||||
|
//Since Gomega assertions call fail, you should throw a `defer GinkgoRecover()` at the top of any goroutine that
|
||||
|
//calls out to Gomega
|
||||
|
//
|
||||
|
//Here's why: Ginkgo's `Fail` method records the failure and then panics to prevent
|
||||
|
//further assertions from running. This panic must be recovered. Ginkgo does this for you
|
||||
|
//if the panic originates in a Ginkgo node (an It, BeforeEach, etc...)
|
||||
|
//
|
||||
|
//Unfortunately, if a panic originates on a goroutine *launched* from one of these nodes there's no
|
||||
|
//way for Ginkgo to rescue the panic. To do this, you must remember to `defer GinkgoRecover()` at the top of such a goroutine.
|
||||
|
func GinkgoRecover() { |
||||
|
e := recover() |
||||
|
if e != nil { |
||||
|
global.Failer.Panic(codelocation.New(1), e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Describe blocks allow you to organize your specs. A Describe block can contain any number of
|
||||
|
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
|
||||
|
//
|
||||
|
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
|
||||
|
//equivalent. The difference is purely semantic -- you typically Describe the behavior of an object
|
||||
|
//or method and, within that Describe, outline a number of Contexts and Whens.
|
||||
|
func Describe(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus the tests within a describe block using FDescribe
|
||||
|
func FDescribe(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using PDescribe
|
||||
|
func PDescribe(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using XDescribe
|
||||
|
func XDescribe(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//Context blocks allow you to organize your specs. A Context block can contain any number of
|
||||
|
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
|
||||
|
//
|
||||
|
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
|
||||
|
//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object
|
||||
|
//or method and, within that Describe, outline a number of Contexts and Whens.
|
||||
|
func Context(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus the tests within a describe block using FContext
|
||||
|
func FContext(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using PContext
|
||||
|
func PContext(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using XContext
|
||||
|
func XContext(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//When blocks allow you to organize your specs. A When block can contain any number of
|
||||
|
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
|
||||
|
//
|
||||
|
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
|
||||
|
//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object
|
||||
|
//or method and, within that Describe, outline a number of Contexts and Whens.
|
||||
|
func When(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode("when "+text, body, types.FlagTypeNone, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus the tests within a describe block using FWhen
|
||||
|
func FWhen(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode("when "+text, body, types.FlagTypeFocused, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using PWhen
|
||||
|
func PWhen(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark the tests within a describe block as pending using XWhen
|
||||
|
func XWhen(text string, body func()) bool { |
||||
|
global.Suite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//It blocks contain your test code and assertions. You cannot nest any other Ginkgo blocks
|
||||
|
//within an It block.
|
||||
|
//
|
||||
|
//Ginkgo will normally run It blocks synchronously. To perform asynchronous tests, pass a
|
||||
|
//function that accepts a Done channel. When you do this, you can also provide an optional timeout.
|
||||
|
func It(text string, body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus individual Its using FIt
|
||||
|
func FIt(text string, body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Its as pending using PIt
|
||||
|
func PIt(text string, _ ...interface{}) bool { |
||||
|
global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Its as pending using XIt
|
||||
|
func XIt(text string, _ ...interface{}) bool { |
||||
|
global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//Specify blocks are aliases for It blocks and allow for more natural wording in situations
|
||||
|
//which "It" does not fit into a natural sentence flow. All the same protocols apply for Specify blocks
|
||||
|
//which apply to It blocks.
|
||||
|
func Specify(text string, body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus individual Specifys using FSpecify
|
||||
|
func FSpecify(text string, body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Specifys as pending using PSpecify
|
||||
|
func PSpecify(text string, is ...interface{}) bool { |
||||
|
global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Specifys as pending using XSpecify
|
||||
|
func XSpecify(text string, is ...interface{}) bool { |
||||
|
global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//By allows you to better document large Its.
|
||||
|
//
|
||||
|
//Generally you should try to keep your Its short and to the point. This is not always possible, however,
|
||||
|
//especially in the context of integration tests that capture a particular workflow.
|
||||
|
//
|
||||
|
//By allows you to document such flows. By must be called within a runnable node (It, BeforeEach, Measure, etc...)
|
||||
|
//By will simply log the passed in text to the GinkgoWriter. If By is handed a function it will immediately run the function.
|
||||
|
func By(text string, callbacks ...func()) { |
||||
|
preamble := "\x1b[1mSTEP\x1b[0m" |
||||
|
if config.DefaultReporterConfig.NoColor { |
||||
|
preamble = "STEP" |
||||
|
} |
||||
|
fmt.Fprintln(GinkgoWriter, preamble+": "+text) |
||||
|
if len(callbacks) == 1 { |
||||
|
callbacks[0]() |
||||
|
} |
||||
|
if len(callbacks) > 1 { |
||||
|
panic("just one callback per By, please") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Measure blocks run the passed in body function repeatedly (determined by the samples argument)
|
||||
|
//and accumulate metrics provided to the Benchmarker by the body function.
|
||||
|
//
|
||||
|
//The body function must have the signature:
|
||||
|
// func(b Benchmarker)
|
||||
|
func Measure(text string, body interface{}, samples int) bool { |
||||
|
global.Suite.PushMeasureNode(text, body, types.FlagTypeNone, codelocation.New(1), samples) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can focus individual Measures using FMeasure
|
||||
|
func FMeasure(text string, body interface{}, samples int) bool { |
||||
|
global.Suite.PushMeasureNode(text, body, types.FlagTypeFocused, codelocation.New(1), samples) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Measurements as pending using PMeasure
|
||||
|
func PMeasure(text string, _ ...interface{}) bool { |
||||
|
global.Suite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//You can mark Measurements as pending using XMeasure
|
||||
|
func XMeasure(text string, _ ...interface{}) bool { |
||||
|
global.Suite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//BeforeSuite blocks are run just once before any specs are run. When running in parallel, each
|
||||
|
//parallel node process will call BeforeSuite.
|
||||
|
//
|
||||
|
//BeforeSuite blocks can be made asynchronous by providing a body function that accepts a Done channel
|
||||
|
//
|
||||
|
//You may only register *one* BeforeSuite handler per test suite. You typically do so in your bootstrap file at the top level.
|
||||
|
func BeforeSuite(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.SetBeforeSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//AfterSuite blocks are *always* run after all the specs regardless of whether specs have passed or failed.
|
||||
|
//Moreover, if Ginkgo receives an interrupt signal (^C) it will attempt to run the AfterSuite before exiting.
|
||||
|
//
|
||||
|
//When running in parallel, each parallel node process will call AfterSuite.
|
||||
|
//
|
||||
|
//AfterSuite blocks can be made asynchronous by providing a body function that accepts a Done channel
|
||||
|
//
|
||||
|
//You may only register *one* AfterSuite handler per test suite. You typically do so in your bootstrap file at the top level.
|
||||
|
func AfterSuite(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.SetAfterSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//SynchronizedBeforeSuite blocks are primarily meant to solve the problem of setting up singleton external resources shared across
|
||||
|
//nodes when running tests in parallel. For example, say you have a shared database that you can only start one instance of that
|
||||
|
//must be used in your tests. When running in parallel, only one node should set up the database and all other nodes should wait
|
||||
|
//until that node is done before running.
|
||||
|
//
|
||||
|
//SynchronizedBeforeSuite accomplishes this by taking *two* function arguments. The first is only run on parallel node #1. The second is
|
||||
|
//run on all nodes, but *only* after the first function completes successfully. Ginkgo also makes it possible to send data from the first function (on Node 1)
|
||||
|
//to the second function (on all the other nodes).
|
||||
|
//
|
||||
|
//The functions have the following signatures. The first function (which only runs on node 1) has the signature:
|
||||
|
//
|
||||
|
// func() []byte
|
||||
|
//
|
||||
|
//or, to run asynchronously:
|
||||
|
//
|
||||
|
// func(done Done) []byte
|
||||
|
//
|
||||
|
//The byte array returned by the first function is then passed to the second function, which has the signature:
|
||||
|
//
|
||||
|
// func(data []byte)
|
||||
|
//
|
||||
|
//or, to run asynchronously:
|
||||
|
//
|
||||
|
// func(data []byte, done Done)
|
||||
|
//
|
||||
|
//Here's a simple pseudo-code example that starts a shared database on Node 1 and shares the database's address with the other nodes:
|
||||
|
//
|
||||
|
// var dbClient db.Client
|
||||
|
// var dbRunner db.Runner
|
||||
|
//
|
||||
|
// var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
|
// dbRunner = db.NewRunner()
|
||||
|
// err := dbRunner.Start()
|
||||
|
// Ω(err).ShouldNot(HaveOccurred())
|
||||
|
// return []byte(dbRunner.URL)
|
||||
|
// }, func(data []byte) {
|
||||
|
// dbClient = db.NewClient()
|
||||
|
// err := dbClient.Connect(string(data))
|
||||
|
// Ω(err).ShouldNot(HaveOccurred())
|
||||
|
// })
|
||||
|
func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, timeout ...float64) bool { |
||||
|
global.Suite.SetSynchronizedBeforeSuiteNode( |
||||
|
node1Body, |
||||
|
allNodesBody, |
||||
|
codelocation.New(1), |
||||
|
parseTimeout(timeout...), |
||||
|
) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//SynchronizedAfterSuite blocks complement the SynchronizedBeforeSuite blocks in solving the problem of setting up
|
||||
|
//external singleton resources shared across nodes when running tests in parallel.
|
||||
|
//
|
||||
|
//SynchronizedAfterSuite accomplishes this by taking *two* function arguments. The first runs on all nodes. The second runs only on parallel node #1
|
||||
|
//and *only* after all other nodes have finished and exited. This ensures that node 1, and any resources it is running, remain alive until
|
||||
|
//all other nodes are finished.
|
||||
|
//
|
||||
|
//Both functions have the same signature: either func() or func(done Done) to run asynchronously.
|
||||
|
//
|
||||
|
//Here's a pseudo-code example that complements that given in SynchronizedBeforeSuite. Here, SynchronizedAfterSuite is used to tear down the shared database
|
||||
|
//only after all nodes have finished:
|
||||
|
//
|
||||
|
// var _ = SynchronizedAfterSuite(func() {
|
||||
|
// dbClient.Cleanup()
|
||||
|
// }, func() {
|
||||
|
// dbRunner.Stop()
|
||||
|
// })
|
||||
|
func SynchronizedAfterSuite(allNodesBody interface{}, node1Body interface{}, timeout ...float64) bool { |
||||
|
global.Suite.SetSynchronizedAfterSuiteNode( |
||||
|
allNodesBody, |
||||
|
node1Body, |
||||
|
codelocation.New(1), |
||||
|
parseTimeout(timeout...), |
||||
|
) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//BeforeEach blocks are run before It blocks. When multiple BeforeEach blocks are defined in nested
|
||||
|
//Describe and Context blocks the outermost BeforeEach blocks are run first.
|
||||
|
//
|
||||
|
//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts
|
||||
|
//a Done channel
|
||||
|
func BeforeEach(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//JustBeforeEach blocks are run before It blocks but *after* all BeforeEach blocks. For more details,
|
||||
|
//read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_)
|
||||
|
//
|
||||
|
//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts
|
||||
|
//a Done channel
|
||||
|
func JustBeforeEach(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushJustBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//JustAfterEach blocks are run after It blocks but *before* all AfterEach blocks. For more details,
|
||||
|
//read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_)
|
||||
|
//
|
||||
|
//Like It blocks, JustAfterEach blocks can be made asynchronous by providing a body function that accepts
|
||||
|
//a Done channel
|
||||
|
func JustAfterEach(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushJustAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
//AfterEach blocks are run after It blocks. When multiple AfterEach blocks are defined in nested
|
||||
|
//Describe and Context blocks the innermost AfterEach blocks are run first.
|
||||
|
//
|
||||
|
//Like It blocks, AfterEach blocks can be made asynchronous by providing a body function that accepts
|
||||
|
//a Done channel
|
||||
|
func AfterEach(body interface{}, timeout ...float64) bool { |
||||
|
validateBodyFunc(body, codelocation.New(1)) |
||||
|
global.Suite.PushAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...)) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func validateBodyFunc(body interface{}, cl types.CodeLocation) { |
||||
|
t := reflect.TypeOf(body) |
||||
|
if t.Kind() != reflect.Func { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if t.NumOut() > 0 { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if t.NumIn() == 0 { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if t.In(0) == reflect.TypeOf(make(Done)) { |
||||
|
deprecationTracker.TrackDeprecation(types.Deprecations.Async(), cl) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func parseTimeout(timeout ...float64) time.Duration { |
||||
|
if len(timeout) == 0 { |
||||
|
return global.DefaultTimeout |
||||
|
} else { |
||||
|
return time.Duration(timeout[0] * float64(time.Second)) |
||||
|
} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
module github.com/onsi/ginkgo |
||||
|
|
||||
|
go 1.15 |
||||
|
|
||||
|
require ( |
||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 |
||||
|
github.com/nxadm/tail v1.4.8 |
||||
|
github.com/onsi/gomega v1.10.1 |
||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091 |
||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e |
||||
|
) |
@ -0,0 +1,86 @@ |
|||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= |
||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= |
||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= |
||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= |
||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= |
||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= |
||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= |
||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= |
||||
|
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= |
||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= |
||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= |
||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= |
||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= |
||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= |
||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= |
||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= |
||||
|
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= |
||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= |
||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= |
||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= |
||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= |
||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= |
||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= |
||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= |
||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= |
||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= |
||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |
||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= |
||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= |
||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= |
||||
|
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= |
||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= |
||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
@ -0,0 +1,48 @@ |
|||||
|
package codelocation |
||||
|
|
||||
|
import ( |
||||
|
"regexp" |
||||
|
"runtime" |
||||
|
"runtime/debug" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
func New(skip int) types.CodeLocation { |
||||
|
_, file, line, _ := runtime.Caller(skip + 1) |
||||
|
stackTrace := PruneStack(string(debug.Stack()), skip+1) |
||||
|
return types.CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace} |
||||
|
} |
||||
|
|
||||
|
// PruneStack removes references to functions that are internal to Ginkgo
|
||||
|
// and the Go runtime from a stack string and a certain number of stack entries
|
||||
|
// at the beginning of the stack. The stack string has the format
|
||||
|
// as returned by runtime/debug.Stack. The leading goroutine information is
|
||||
|
// optional and always removed if present. Beware that runtime/debug.Stack
|
||||
|
// adds itself as first entry, so typically skip must be >= 1 to remove that
|
||||
|
// entry.
|
||||
|
func PruneStack(fullStackTrace string, skip int) string { |
||||
|
stack := strings.Split(fullStackTrace, "\n") |
||||
|
// Ensure that the even entries are the method names and the
|
||||
|
// the odd entries the source code information.
|
||||
|
if len(stack) > 0 && strings.HasPrefix(stack[0], "goroutine ") { |
||||
|
// Ignore "goroutine 29 [running]:" line.
|
||||
|
stack = stack[1:] |
||||
|
} |
||||
|
// The "+1" is for skipping over the initial entry, which is
|
||||
|
// runtime/debug.Stack() itself.
|
||||
|
if len(stack) > 2*(skip+1) { |
||||
|
stack = stack[2*(skip+1):] |
||||
|
} |
||||
|
prunedStack := []string{} |
||||
|
re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) |
||||
|
for i := 0; i < len(stack)/2; i++ { |
||||
|
// We filter out based on the source code file name.
|
||||
|
if !re.Match([]byte(stack[i*2+1])) { |
||||
|
prunedStack = append(prunedStack, stack[i*2]) |
||||
|
prunedStack = append(prunedStack, stack[i*2+1]) |
||||
|
} |
||||
|
} |
||||
|
return strings.Join(prunedStack, "\n") |
||||
|
} |
@ -0,0 +1,151 @@ |
|||||
|
package containernode |
||||
|
|
||||
|
import ( |
||||
|
"math/rand" |
||||
|
"sort" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/leafnodes" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type subjectOrContainerNode struct { |
||||
|
containerNode *ContainerNode |
||||
|
subjectNode leafnodes.SubjectNode |
||||
|
} |
||||
|
|
||||
|
func (n subjectOrContainerNode) text() string { |
||||
|
if n.containerNode != nil { |
||||
|
return n.containerNode.Text() |
||||
|
} else { |
||||
|
return n.subjectNode.Text() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type CollatedNodes struct { |
||||
|
Containers []*ContainerNode |
||||
|
Subject leafnodes.SubjectNode |
||||
|
} |
||||
|
|
||||
|
type ContainerNode struct { |
||||
|
text string |
||||
|
flag types.FlagType |
||||
|
codeLocation types.CodeLocation |
||||
|
|
||||
|
setupNodes []leafnodes.BasicNode |
||||
|
subjectAndContainerNodes []subjectOrContainerNode |
||||
|
} |
||||
|
|
||||
|
func New(text string, flag types.FlagType, codeLocation types.CodeLocation) *ContainerNode { |
||||
|
return &ContainerNode{ |
||||
|
text: text, |
||||
|
flag: flag, |
||||
|
codeLocation: codeLocation, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (container *ContainerNode) Shuffle(r *rand.Rand) { |
||||
|
sort.Sort(container) |
||||
|
permutation := r.Perm(len(container.subjectAndContainerNodes)) |
||||
|
shuffledNodes := make([]subjectOrContainerNode, len(container.subjectAndContainerNodes)) |
||||
|
for i, j := range permutation { |
||||
|
shuffledNodes[i] = container.subjectAndContainerNodes[j] |
||||
|
} |
||||
|
container.subjectAndContainerNodes = shuffledNodes |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) BackPropagateProgrammaticFocus() bool { |
||||
|
if node.flag == types.FlagTypePending { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
shouldUnfocus := false |
||||
|
for _, subjectOrContainerNode := range node.subjectAndContainerNodes { |
||||
|
if subjectOrContainerNode.containerNode != nil { |
||||
|
shouldUnfocus = subjectOrContainerNode.containerNode.BackPropagateProgrammaticFocus() || shouldUnfocus |
||||
|
} else { |
||||
|
shouldUnfocus = (subjectOrContainerNode.subjectNode.Flag() == types.FlagTypeFocused) || shouldUnfocus |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if shouldUnfocus { |
||||
|
if node.flag == types.FlagTypeFocused { |
||||
|
node.flag = types.FlagTypeNone |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
return node.flag == types.FlagTypeFocused |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) Collate() []CollatedNodes { |
||||
|
return node.collate([]*ContainerNode{}) |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) collate(enclosingContainers []*ContainerNode) []CollatedNodes { |
||||
|
collated := make([]CollatedNodes, 0) |
||||
|
|
||||
|
containers := make([]*ContainerNode, len(enclosingContainers)) |
||||
|
copy(containers, enclosingContainers) |
||||
|
containers = append(containers, node) |
||||
|
|
||||
|
for _, subjectOrContainer := range node.subjectAndContainerNodes { |
||||
|
if subjectOrContainer.containerNode != nil { |
||||
|
collated = append(collated, subjectOrContainer.containerNode.collate(containers)...) |
||||
|
} else { |
||||
|
collated = append(collated, CollatedNodes{ |
||||
|
Containers: containers, |
||||
|
Subject: subjectOrContainer.subjectNode, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return collated |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) PushContainerNode(container *ContainerNode) { |
||||
|
node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{containerNode: container}) |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) PushSubjectNode(subject leafnodes.SubjectNode) { |
||||
|
node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{subjectNode: subject}) |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) PushSetupNode(setupNode leafnodes.BasicNode) { |
||||
|
node.setupNodes = append(node.setupNodes, setupNode) |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) SetupNodesOfType(nodeType types.SpecComponentType) []leafnodes.BasicNode { |
||||
|
nodes := []leafnodes.BasicNode{} |
||||
|
for _, setupNode := range node.setupNodes { |
||||
|
if setupNode.Type() == nodeType { |
||||
|
nodes = append(nodes, setupNode) |
||||
|
} |
||||
|
} |
||||
|
return nodes |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) Text() string { |
||||
|
return node.text |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) CodeLocation() types.CodeLocation { |
||||
|
return node.codeLocation |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) Flag() types.FlagType { |
||||
|
return node.flag |
||||
|
} |
||||
|
|
||||
|
//sort.Interface
|
||||
|
|
||||
|
func (node *ContainerNode) Len() int { |
||||
|
return len(node.subjectAndContainerNodes) |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) Less(i, j int) bool { |
||||
|
return node.subjectAndContainerNodes[i].text() < node.subjectAndContainerNodes[j].text() |
||||
|
} |
||||
|
|
||||
|
func (node *ContainerNode) Swap(i, j int) { |
||||
|
node.subjectAndContainerNodes[i], node.subjectAndContainerNodes[j] = node.subjectAndContainerNodes[j], node.subjectAndContainerNodes[i] |
||||
|
} |
@ -0,0 +1,92 @@ |
|||||
|
package failer |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"sync" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type Failer struct { |
||||
|
lock *sync.Mutex |
||||
|
failure types.SpecFailure |
||||
|
state types.SpecState |
||||
|
} |
||||
|
|
||||
|
func New() *Failer { |
||||
|
return &Failer{ |
||||
|
lock: &sync.Mutex{}, |
||||
|
state: types.SpecStatePassed, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (f *Failer) Panic(location types.CodeLocation, forwardedPanic interface{}) { |
||||
|
f.lock.Lock() |
||||
|
defer f.lock.Unlock() |
||||
|
|
||||
|
if f.state == types.SpecStatePassed { |
||||
|
f.state = types.SpecStatePanicked |
||||
|
f.failure = types.SpecFailure{ |
||||
|
Message: "Test Panicked", |
||||
|
Location: location, |
||||
|
ForwardedPanic: fmt.Sprintf("%v", forwardedPanic), |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (f *Failer) Timeout(location types.CodeLocation) { |
||||
|
f.lock.Lock() |
||||
|
defer f.lock.Unlock() |
||||
|
|
||||
|
if f.state == types.SpecStatePassed { |
||||
|
f.state = types.SpecStateTimedOut |
||||
|
f.failure = types.SpecFailure{ |
||||
|
Message: "Timed out", |
||||
|
Location: location, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (f *Failer) Fail(message string, location types.CodeLocation) { |
||||
|
f.lock.Lock() |
||||
|
defer f.lock.Unlock() |
||||
|
|
||||
|
if f.state == types.SpecStatePassed { |
||||
|
f.state = types.SpecStateFailed |
||||
|
f.failure = types.SpecFailure{ |
||||
|
Message: message, |
||||
|
Location: location, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (f *Failer) Drain(componentType types.SpecComponentType, componentIndex int, componentCodeLocation types.CodeLocation) (types.SpecFailure, types.SpecState) { |
||||
|
f.lock.Lock() |
||||
|
defer f.lock.Unlock() |
||||
|
|
||||
|
failure := f.failure |
||||
|
outcome := f.state |
||||
|
if outcome != types.SpecStatePassed { |
||||
|
failure.ComponentType = componentType |
||||
|
failure.ComponentIndex = componentIndex |
||||
|
failure.ComponentCodeLocation = componentCodeLocation |
||||
|
} |
||||
|
|
||||
|
f.state = types.SpecStatePassed |
||||
|
f.failure = types.SpecFailure{} |
||||
|
|
||||
|
return failure, outcome |
||||
|
} |
||||
|
|
||||
|
func (f *Failer) Skip(message string, location types.CodeLocation) { |
||||
|
f.lock.Lock() |
||||
|
defer f.lock.Unlock() |
||||
|
|
||||
|
if f.state == types.SpecStatePassed { |
||||
|
f.state = types.SpecStateSkipped |
||||
|
f.failure = types.SpecFailure{ |
||||
|
Message: message, |
||||
|
Location: location, |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
package global |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/internal/suite" |
||||
|
) |
||||
|
|
||||
|
const DefaultTimeout = time.Duration(1 * time.Second) |
||||
|
|
||||
|
var Suite *suite.Suite |
||||
|
var Failer *failer.Failer |
||||
|
|
||||
|
func init() { |
||||
|
InitializeGlobals() |
||||
|
} |
||||
|
|
||||
|
func InitializeGlobals() { |
||||
|
Failer = failer.New() |
||||
|
Suite = suite.New(Failer) |
||||
|
} |
@ -0,0 +1,103 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"math" |
||||
|
"time" |
||||
|
|
||||
|
"sync" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type benchmarker struct { |
||||
|
mu sync.Mutex |
||||
|
measurements map[string]*types.SpecMeasurement |
||||
|
orderCounter int |
||||
|
} |
||||
|
|
||||
|
func newBenchmarker() *benchmarker { |
||||
|
return &benchmarker{ |
||||
|
measurements: make(map[string]*types.SpecMeasurement), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) { |
||||
|
t := time.Now() |
||||
|
body() |
||||
|
elapsedTime = time.Since(t) |
||||
|
|
||||
|
b.mu.Lock() |
||||
|
defer b.mu.Unlock() |
||||
|
measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", 3, info...) |
||||
|
measurement.Results = append(measurement.Results, elapsedTime.Seconds()) |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) { |
||||
|
b.mu.Lock() |
||||
|
measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", 3, info...) |
||||
|
defer b.mu.Unlock() |
||||
|
measurement.Results = append(measurement.Results, value) |
||||
|
} |
||||
|
|
||||
|
func (b *benchmarker) RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) { |
||||
|
b.mu.Lock() |
||||
|
measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", units, precision, info...) |
||||
|
defer b.mu.Unlock() |
||||
|
measurement.Results = append(measurement.Results, value) |
||||
|
} |
||||
|
|
||||
|
func (b *benchmarker) getMeasurement(name string, smallestLabel string, largestLabel string, averageLabel string, units string, precision int, info ...interface{}) *types.SpecMeasurement { |
||||
|
measurement, ok := b.measurements[name] |
||||
|
if !ok { |
||||
|
var computedInfo interface{} |
||||
|
computedInfo = nil |
||||
|
if len(info) > 0 { |
||||
|
computedInfo = info[0] |
||||
|
} |
||||
|
measurement = &types.SpecMeasurement{ |
||||
|
Name: name, |
||||
|
Info: computedInfo, |
||||
|
Order: b.orderCounter, |
||||
|
SmallestLabel: smallestLabel, |
||||
|
LargestLabel: largestLabel, |
||||
|
AverageLabel: averageLabel, |
||||
|
Units: units, |
||||
|
Precision: precision, |
||||
|
Results: make([]float64, 0), |
||||
|
} |
||||
|
b.measurements[name] = measurement |
||||
|
b.orderCounter++ |
||||
|
} |
||||
|
|
||||
|
return measurement |
||||
|
} |
||||
|
|
||||
|
func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement { |
||||
|
b.mu.Lock() |
||||
|
defer b.mu.Unlock() |
||||
|
for _, measurement := range b.measurements { |
||||
|
measurement.Smallest = math.MaxFloat64 |
||||
|
measurement.Largest = -math.MaxFloat64 |
||||
|
sum := float64(0) |
||||
|
sumOfSquares := float64(0) |
||||
|
|
||||
|
for _, result := range measurement.Results { |
||||
|
if result > measurement.Largest { |
||||
|
measurement.Largest = result |
||||
|
} |
||||
|
if result < measurement.Smallest { |
||||
|
measurement.Smallest = result |
||||
|
} |
||||
|
sum += result |
||||
|
sumOfSquares += result * result |
||||
|
} |
||||
|
|
||||
|
n := float64(len(measurement.Results)) |
||||
|
measurement.Average = sum / n |
||||
|
measurement.StdDeviation = math.Sqrt(sumOfSquares/n - (sum/n)*(sum/n)) |
||||
|
} |
||||
|
|
||||
|
return b.measurements |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type BasicNode interface { |
||||
|
Type() types.SpecComponentType |
||||
|
Run() (types.SpecState, types.SpecFailure) |
||||
|
CodeLocation() types.CodeLocation |
||||
|
} |
||||
|
|
||||
|
type SubjectNode interface { |
||||
|
BasicNode |
||||
|
|
||||
|
Text() string |
||||
|
Flag() types.FlagType |
||||
|
Samples() int |
||||
|
} |
@ -0,0 +1,47 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type ItNode struct { |
||||
|
runner *runner |
||||
|
|
||||
|
flag types.FlagType |
||||
|
text string |
||||
|
} |
||||
|
|
||||
|
func NewItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *ItNode { |
||||
|
return &ItNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeIt, componentIndex), |
||||
|
flag: flag, |
||||
|
text: text, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) Run() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
return node.runner.run() |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) Type() types.SpecComponentType { |
||||
|
return types.SpecComponentTypeIt |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) Text() string { |
||||
|
return node.text |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) Flag() types.FlagType { |
||||
|
return node.flag |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) CodeLocation() types.CodeLocation { |
||||
|
return node.runner.codeLocation |
||||
|
} |
||||
|
|
||||
|
func (node *ItNode) Samples() int { |
||||
|
return 1 |
||||
|
} |
@ -0,0 +1,62 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"reflect" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type MeasureNode struct { |
||||
|
runner *runner |
||||
|
|
||||
|
text string |
||||
|
flag types.FlagType |
||||
|
samples int |
||||
|
benchmarker *benchmarker |
||||
|
} |
||||
|
|
||||
|
func NewMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int, failer *failer.Failer, componentIndex int) *MeasureNode { |
||||
|
benchmarker := newBenchmarker() |
||||
|
|
||||
|
wrappedBody := func() { |
||||
|
reflect.ValueOf(body).Call([]reflect.Value{reflect.ValueOf(benchmarker)}) |
||||
|
} |
||||
|
|
||||
|
return &MeasureNode{ |
||||
|
runner: newRunner(wrappedBody, codeLocation, 0, failer, types.SpecComponentTypeMeasure, componentIndex), |
||||
|
|
||||
|
text: text, |
||||
|
flag: flag, |
||||
|
samples: samples, |
||||
|
benchmarker: benchmarker, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) Run() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
return node.runner.run() |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) MeasurementsReport() map[string]*types.SpecMeasurement { |
||||
|
return node.benchmarker.measurementsReport() |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) Type() types.SpecComponentType { |
||||
|
return types.SpecComponentTypeMeasure |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) Text() string { |
||||
|
return node.text |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) Flag() types.FlagType { |
||||
|
return node.flag |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) CodeLocation() types.CodeLocation { |
||||
|
return node.runner.codeLocation |
||||
|
} |
||||
|
|
||||
|
func (node *MeasureNode) Samples() int { |
||||
|
return node.samples |
||||
|
} |
@ -0,0 +1,117 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"reflect" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/codelocation" |
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type runner struct { |
||||
|
isAsync bool |
||||
|
asyncFunc func(chan<- interface{}) |
||||
|
syncFunc func() |
||||
|
codeLocation types.CodeLocation |
||||
|
timeoutThreshold time.Duration |
||||
|
nodeType types.SpecComponentType |
||||
|
componentIndex int |
||||
|
failer *failer.Failer |
||||
|
} |
||||
|
|
||||
|
func newRunner(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, nodeType types.SpecComponentType, componentIndex int) *runner { |
||||
|
bodyType := reflect.TypeOf(body) |
||||
|
if bodyType.Kind() != reflect.Func { |
||||
|
panic(fmt.Sprintf("Expected a function but got something else at %v", codeLocation)) |
||||
|
} |
||||
|
|
||||
|
runner := &runner{ |
||||
|
codeLocation: codeLocation, |
||||
|
timeoutThreshold: timeout, |
||||
|
failer: failer, |
||||
|
nodeType: nodeType, |
||||
|
componentIndex: componentIndex, |
||||
|
} |
||||
|
|
||||
|
switch bodyType.NumIn() { |
||||
|
case 0: |
||||
|
runner.syncFunc = body.(func()) |
||||
|
return runner |
||||
|
case 1: |
||||
|
if !(bodyType.In(0).Kind() == reflect.Chan && bodyType.In(0).Elem().Kind() == reflect.Interface) { |
||||
|
panic(fmt.Sprintf("Must pass a Done channel to function at %v", codeLocation)) |
||||
|
} |
||||
|
|
||||
|
wrappedBody := func(done chan<- interface{}) { |
||||
|
bodyValue := reflect.ValueOf(body) |
||||
|
bodyValue.Call([]reflect.Value{reflect.ValueOf(done)}) |
||||
|
} |
||||
|
|
||||
|
runner.isAsync = true |
||||
|
runner.asyncFunc = wrappedBody |
||||
|
return runner |
||||
|
} |
||||
|
|
||||
|
panic(fmt.Sprintf("Too many arguments to function at %v", codeLocation)) |
||||
|
} |
||||
|
|
||||
|
func (r *runner) run() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
if r.isAsync { |
||||
|
return r.runAsync() |
||||
|
} else { |
||||
|
return r.runSync() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (r *runner) runAsync() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
done := make(chan interface{}, 1) |
||||
|
|
||||
|
go func() { |
||||
|
finished := false |
||||
|
|
||||
|
defer func() { |
||||
|
if e := recover(); e != nil || !finished { |
||||
|
r.failer.Panic(codelocation.New(2), e) |
||||
|
select { |
||||
|
case <-done: |
||||
|
break |
||||
|
default: |
||||
|
close(done) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
r.asyncFunc(done) |
||||
|
finished = true |
||||
|
}() |
||||
|
|
||||
|
// If this goroutine gets no CPU time before the select block,
|
||||
|
// the <-done case may complete even if the test took longer than the timeoutThreshold.
|
||||
|
// This can cause flaky behaviour, but we haven't seen it in the wild.
|
||||
|
select { |
||||
|
case <-done: |
||||
|
case <-time.After(r.timeoutThreshold): |
||||
|
r.failer.Timeout(r.codeLocation) |
||||
|
} |
||||
|
|
||||
|
failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) |
||||
|
return |
||||
|
} |
||||
|
func (r *runner) runSync() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
finished := false |
||||
|
|
||||
|
defer func() { |
||||
|
if e := recover(); e != nil || !finished { |
||||
|
r.failer.Panic(codelocation.New(2), e) |
||||
|
} |
||||
|
|
||||
|
failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) |
||||
|
}() |
||||
|
|
||||
|
r.syncFunc() |
||||
|
finished = true |
||||
|
|
||||
|
return |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type SetupNode struct { |
||||
|
runner *runner |
||||
|
} |
||||
|
|
||||
|
func (node *SetupNode) Run() (outcome types.SpecState, failure types.SpecFailure) { |
||||
|
return node.runner.run() |
||||
|
} |
||||
|
|
||||
|
func (node *SetupNode) Type() types.SpecComponentType { |
||||
|
return node.runner.nodeType |
||||
|
} |
||||
|
|
||||
|
func (node *SetupNode) CodeLocation() types.CodeLocation { |
||||
|
return node.runner.codeLocation |
||||
|
} |
||||
|
|
||||
|
func NewBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { |
||||
|
return &SetupNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeEach, componentIndex), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { |
||||
|
return &SetupNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterEach, componentIndex), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { |
||||
|
return &SetupNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustBeforeEach, componentIndex), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewJustAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { |
||||
|
return &SetupNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustAfterEach, componentIndex), |
||||
|
} |
||||
|
} |
@ -0,0 +1,55 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type SuiteNode interface { |
||||
|
Run(parallelNode int, parallelTotal int, syncHost string) bool |
||||
|
Passed() bool |
||||
|
Summary() *types.SetupSummary |
||||
|
} |
||||
|
|
||||
|
type simpleSuiteNode struct { |
||||
|
runner *runner |
||||
|
outcome types.SpecState |
||||
|
failure types.SpecFailure |
||||
|
runTime time.Duration |
||||
|
} |
||||
|
|
||||
|
func (node *simpleSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { |
||||
|
t := time.Now() |
||||
|
node.outcome, node.failure = node.runner.run() |
||||
|
node.runTime = time.Since(t) |
||||
|
|
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *simpleSuiteNode) Passed() bool { |
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *simpleSuiteNode) Summary() *types.SetupSummary { |
||||
|
return &types.SetupSummary{ |
||||
|
ComponentType: node.runner.nodeType, |
||||
|
CodeLocation: node.runner.codeLocation, |
||||
|
State: node.outcome, |
||||
|
RunTime: node.runTime, |
||||
|
Failure: node.failure, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { |
||||
|
return &simpleSuiteNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { |
||||
|
return &simpleSuiteNode{ |
||||
|
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), |
||||
|
} |
||||
|
} |
@ -0,0 +1,90 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"io/ioutil" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type synchronizedAfterSuiteNode struct { |
||||
|
runnerA *runner |
||||
|
runnerB *runner |
||||
|
|
||||
|
outcome types.SpecState |
||||
|
failure types.SpecFailure |
||||
|
runTime time.Duration |
||||
|
} |
||||
|
|
||||
|
func NewSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { |
||||
|
return &synchronizedAfterSuiteNode{ |
||||
|
runnerA: newRunner(bodyA, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), |
||||
|
runnerB: newRunner(bodyB, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedAfterSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { |
||||
|
node.outcome, node.failure = node.runnerA.run() |
||||
|
|
||||
|
if parallelNode == 1 { |
||||
|
if parallelTotal > 1 { |
||||
|
node.waitUntilOtherNodesAreDone(syncHost) |
||||
|
} |
||||
|
|
||||
|
outcome, failure := node.runnerB.run() |
||||
|
|
||||
|
if node.outcome == types.SpecStatePassed { |
||||
|
node.outcome, node.failure = outcome, failure |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedAfterSuiteNode) Passed() bool { |
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedAfterSuiteNode) Summary() *types.SetupSummary { |
||||
|
return &types.SetupSummary{ |
||||
|
ComponentType: node.runnerA.nodeType, |
||||
|
CodeLocation: node.runnerA.codeLocation, |
||||
|
State: node.outcome, |
||||
|
RunTime: node.runTime, |
||||
|
Failure: node.failure, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedAfterSuiteNode) waitUntilOtherNodesAreDone(syncHost string) { |
||||
|
for { |
||||
|
if node.canRun(syncHost) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
time.Sleep(50 * time.Millisecond) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedAfterSuiteNode) canRun(syncHost string) bool { |
||||
|
resp, err := http.Get(syncHost + "/RemoteAfterSuiteData") |
||||
|
if err != nil || resp.StatusCode != http.StatusOK { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
body, err := ioutil.ReadAll(resp.Body) |
||||
|
if err != nil { |
||||
|
return false |
||||
|
} |
||||
|
resp.Body.Close() |
||||
|
|
||||
|
afterSuiteData := types.RemoteAfterSuiteData{} |
||||
|
err = json.Unmarshal(body, &afterSuiteData) |
||||
|
if err != nil { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return afterSuiteData.CanRun |
||||
|
} |
@ -0,0 +1,181 @@ |
|||||
|
package leafnodes |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/json" |
||||
|
"io/ioutil" |
||||
|
"net/http" |
||||
|
"reflect" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type synchronizedBeforeSuiteNode struct { |
||||
|
runnerA *runner |
||||
|
runnerB *runner |
||||
|
|
||||
|
data []byte |
||||
|
|
||||
|
outcome types.SpecState |
||||
|
failure types.SpecFailure |
||||
|
runTime time.Duration |
||||
|
} |
||||
|
|
||||
|
func NewSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { |
||||
|
node := &synchronizedBeforeSuiteNode{} |
||||
|
|
||||
|
node.runnerA = newRunner(node.wrapA(bodyA), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) |
||||
|
node.runnerB = newRunner(node.wrapB(bodyB), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) |
||||
|
|
||||
|
return node |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { |
||||
|
t := time.Now() |
||||
|
defer func() { |
||||
|
node.runTime = time.Since(t) |
||||
|
}() |
||||
|
|
||||
|
if parallelNode == 1 { |
||||
|
node.outcome, node.failure = node.runA(parallelTotal, syncHost) |
||||
|
} else { |
||||
|
node.outcome, node.failure = node.waitForA(syncHost) |
||||
|
} |
||||
|
|
||||
|
if node.outcome != types.SpecStatePassed { |
||||
|
return false |
||||
|
} |
||||
|
node.outcome, node.failure = node.runnerB.run() |
||||
|
|
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) runA(parallelTotal int, syncHost string) (types.SpecState, types.SpecFailure) { |
||||
|
outcome, failure := node.runnerA.run() |
||||
|
|
||||
|
if parallelTotal > 1 { |
||||
|
state := types.RemoteBeforeSuiteStatePassed |
||||
|
if outcome != types.SpecStatePassed { |
||||
|
state = types.RemoteBeforeSuiteStateFailed |
||||
|
} |
||||
|
json := (types.RemoteBeforeSuiteData{ |
||||
|
Data: node.data, |
||||
|
State: state, |
||||
|
}).ToJSON() |
||||
|
http.Post(syncHost+"/BeforeSuiteState", "application/json", bytes.NewBuffer(json)) |
||||
|
} |
||||
|
|
||||
|
return outcome, failure |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) waitForA(syncHost string) (types.SpecState, types.SpecFailure) { |
||||
|
failure := func(message string) types.SpecFailure { |
||||
|
return types.SpecFailure{ |
||||
|
Message: message, |
||||
|
Location: node.runnerA.codeLocation, |
||||
|
ComponentType: node.runnerA.nodeType, |
||||
|
ComponentIndex: node.runnerA.componentIndex, |
||||
|
ComponentCodeLocation: node.runnerA.codeLocation, |
||||
|
} |
||||
|
} |
||||
|
for { |
||||
|
resp, err := http.Get(syncHost + "/BeforeSuiteState") |
||||
|
if err != nil || resp.StatusCode != http.StatusOK { |
||||
|
return types.SpecStateFailed, failure("Failed to fetch BeforeSuite state") |
||||
|
} |
||||
|
|
||||
|
body, err := ioutil.ReadAll(resp.Body) |
||||
|
if err != nil { |
||||
|
return types.SpecStateFailed, failure("Failed to read BeforeSuite state") |
||||
|
} |
||||
|
resp.Body.Close() |
||||
|
|
||||
|
beforeSuiteData := types.RemoteBeforeSuiteData{} |
||||
|
err = json.Unmarshal(body, &beforeSuiteData) |
||||
|
if err != nil { |
||||
|
return types.SpecStateFailed, failure("Failed to decode BeforeSuite state") |
||||
|
} |
||||
|
|
||||
|
switch beforeSuiteData.State { |
||||
|
case types.RemoteBeforeSuiteStatePassed: |
||||
|
node.data = beforeSuiteData.Data |
||||
|
return types.SpecStatePassed, types.SpecFailure{} |
||||
|
case types.RemoteBeforeSuiteStateFailed: |
||||
|
return types.SpecStateFailed, failure("BeforeSuite on Node 1 failed") |
||||
|
case types.RemoteBeforeSuiteStateDisappeared: |
||||
|
return types.SpecStateFailed, failure("Node 1 disappeared before completing BeforeSuite") |
||||
|
} |
||||
|
|
||||
|
time.Sleep(50 * time.Millisecond) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) Passed() bool { |
||||
|
return node.outcome == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) Summary() *types.SetupSummary { |
||||
|
return &types.SetupSummary{ |
||||
|
ComponentType: node.runnerA.nodeType, |
||||
|
CodeLocation: node.runnerA.codeLocation, |
||||
|
State: node.outcome, |
||||
|
RunTime: node.runTime, |
||||
|
Failure: node.failure, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) wrapA(bodyA interface{}) interface{} { |
||||
|
typeA := reflect.TypeOf(bodyA) |
||||
|
if typeA.Kind() != reflect.Func { |
||||
|
panic("SynchronizedBeforeSuite expects a function as its first argument") |
||||
|
} |
||||
|
|
||||
|
takesNothing := typeA.NumIn() == 0 |
||||
|
takesADoneChannel := typeA.NumIn() == 1 && typeA.In(0).Kind() == reflect.Chan && typeA.In(0).Elem().Kind() == reflect.Interface |
||||
|
returnsBytes := typeA.NumOut() == 1 && typeA.Out(0).Kind() == reflect.Slice && typeA.Out(0).Elem().Kind() == reflect.Uint8 |
||||
|
|
||||
|
if !((takesNothing || takesADoneChannel) && returnsBytes) { |
||||
|
panic("SynchronizedBeforeSuite's first argument should be a function that returns []byte and either takes no arguments or takes a Done channel.") |
||||
|
} |
||||
|
|
||||
|
if takesADoneChannel { |
||||
|
return func(done chan<- interface{}) { |
||||
|
out := reflect.ValueOf(bodyA).Call([]reflect.Value{reflect.ValueOf(done)}) |
||||
|
node.data = out[0].Interface().([]byte) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return func() { |
||||
|
out := reflect.ValueOf(bodyA).Call([]reflect.Value{}) |
||||
|
node.data = out[0].Interface().([]byte) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (node *synchronizedBeforeSuiteNode) wrapB(bodyB interface{}) interface{} { |
||||
|
typeB := reflect.TypeOf(bodyB) |
||||
|
if typeB.Kind() != reflect.Func { |
||||
|
panic("SynchronizedBeforeSuite expects a function as its second argument") |
||||
|
} |
||||
|
|
||||
|
returnsNothing := typeB.NumOut() == 0 |
||||
|
takesBytesOnly := typeB.NumIn() == 1 && typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 |
||||
|
takesBytesAndDone := typeB.NumIn() == 2 && |
||||
|
typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 && |
||||
|
typeB.In(1).Kind() == reflect.Chan && typeB.In(1).Elem().Kind() == reflect.Interface |
||||
|
|
||||
|
if !((takesBytesOnly || takesBytesAndDone) && returnsNothing) { |
||||
|
panic("SynchronizedBeforeSuite's second argument should be a function that returns nothing and either takes []byte or ([]byte, Done)") |
||||
|
} |
||||
|
|
||||
|
if takesBytesAndDone { |
||||
|
return func(done chan<- interface{}) { |
||||
|
reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data), reflect.ValueOf(done)}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return func() { |
||||
|
reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data)}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,249 @@ |
|||||
|
/* |
||||
|
|
||||
|
Aggregator is a reporter used by the Ginkgo CLI to aggregate and present parallel test output |
||||
|
coherently as tests complete. You shouldn't need to use this in your code. To run tests in parallel: |
||||
|
|
||||
|
ginkgo -nodes=N |
||||
|
|
||||
|
where N is the number of nodes you desire. |
||||
|
*/ |
||||
|
package remote |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/reporters/stenographer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type configAndSuite struct { |
||||
|
config config.GinkgoConfigType |
||||
|
summary *types.SuiteSummary |
||||
|
} |
||||
|
|
||||
|
type Aggregator struct { |
||||
|
nodeCount int |
||||
|
config config.DefaultReporterConfigType |
||||
|
stenographer stenographer.Stenographer |
||||
|
result chan bool |
||||
|
|
||||
|
suiteBeginnings chan configAndSuite |
||||
|
aggregatedSuiteBeginnings []configAndSuite |
||||
|
|
||||
|
beforeSuites chan *types.SetupSummary |
||||
|
aggregatedBeforeSuites []*types.SetupSummary |
||||
|
|
||||
|
afterSuites chan *types.SetupSummary |
||||
|
aggregatedAfterSuites []*types.SetupSummary |
||||
|
|
||||
|
specCompletions chan *types.SpecSummary |
||||
|
completedSpecs []*types.SpecSummary |
||||
|
|
||||
|
suiteEndings chan *types.SuiteSummary |
||||
|
aggregatedSuiteEndings []*types.SuiteSummary |
||||
|
specs []*types.SpecSummary |
||||
|
|
||||
|
startTime time.Time |
||||
|
} |
||||
|
|
||||
|
func NewAggregator(nodeCount int, result chan bool, config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *Aggregator { |
||||
|
aggregator := &Aggregator{ |
||||
|
nodeCount: nodeCount, |
||||
|
result: result, |
||||
|
config: config, |
||||
|
stenographer: stenographer, |
||||
|
|
||||
|
suiteBeginnings: make(chan configAndSuite), |
||||
|
beforeSuites: make(chan *types.SetupSummary), |
||||
|
afterSuites: make(chan *types.SetupSummary), |
||||
|
specCompletions: make(chan *types.SpecSummary), |
||||
|
suiteEndings: make(chan *types.SuiteSummary), |
||||
|
} |
||||
|
|
||||
|
go aggregator.mux() |
||||
|
|
||||
|
return aggregator |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { |
||||
|
aggregator.suiteBeginnings <- configAndSuite{config, summary} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
aggregator.beforeSuites <- setupSummary |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) AfterSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
aggregator.afterSuites <- setupSummary |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) SpecWillRun(specSummary *types.SpecSummary) { |
||||
|
//noop
|
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) SpecDidComplete(specSummary *types.SpecSummary) { |
||||
|
aggregator.specCompletions <- specSummary |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) SpecSuiteDidEnd(summary *types.SuiteSummary) { |
||||
|
aggregator.suiteEndings <- summary |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) mux() { |
||||
|
loop: |
||||
|
for { |
||||
|
select { |
||||
|
case configAndSuite := <-aggregator.suiteBeginnings: |
||||
|
aggregator.registerSuiteBeginning(configAndSuite) |
||||
|
case setupSummary := <-aggregator.beforeSuites: |
||||
|
aggregator.registerBeforeSuite(setupSummary) |
||||
|
case setupSummary := <-aggregator.afterSuites: |
||||
|
aggregator.registerAfterSuite(setupSummary) |
||||
|
case specSummary := <-aggregator.specCompletions: |
||||
|
aggregator.registerSpecCompletion(specSummary) |
||||
|
case suite := <-aggregator.suiteEndings: |
||||
|
finished, passed := aggregator.registerSuiteEnding(suite) |
||||
|
if finished { |
||||
|
aggregator.result <- passed |
||||
|
break loop |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) registerSuiteBeginning(configAndSuite configAndSuite) { |
||||
|
aggregator.aggregatedSuiteBeginnings = append(aggregator.aggregatedSuiteBeginnings, configAndSuite) |
||||
|
|
||||
|
if len(aggregator.aggregatedSuiteBeginnings) == 1 { |
||||
|
aggregator.startTime = time.Now() |
||||
|
} |
||||
|
|
||||
|
if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
aggregator.stenographer.AnnounceSuite(configAndSuite.summary.SuiteDescription, configAndSuite.config.RandomSeed, configAndSuite.config.RandomizeAllSpecs, aggregator.config.Succinct) |
||||
|
|
||||
|
totalNumberOfSpecs := 0 |
||||
|
if len(aggregator.aggregatedSuiteBeginnings) > 0 { |
||||
|
totalNumberOfSpecs = configAndSuite.summary.NumberOfSpecsBeforeParallelization |
||||
|
} |
||||
|
|
||||
|
aggregator.stenographer.AnnounceTotalNumberOfSpecs(totalNumberOfSpecs, aggregator.config.Succinct) |
||||
|
aggregator.stenographer.AnnounceAggregatedParallelRun(aggregator.nodeCount, aggregator.config.Succinct) |
||||
|
aggregator.flushCompletedSpecs() |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) registerBeforeSuite(setupSummary *types.SetupSummary) { |
||||
|
aggregator.aggregatedBeforeSuites = append(aggregator.aggregatedBeforeSuites, setupSummary) |
||||
|
aggregator.flushCompletedSpecs() |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) registerAfterSuite(setupSummary *types.SetupSummary) { |
||||
|
aggregator.aggregatedAfterSuites = append(aggregator.aggregatedAfterSuites, setupSummary) |
||||
|
aggregator.flushCompletedSpecs() |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) registerSpecCompletion(specSummary *types.SpecSummary) { |
||||
|
aggregator.completedSpecs = append(aggregator.completedSpecs, specSummary) |
||||
|
aggregator.specs = append(aggregator.specs, specSummary) |
||||
|
aggregator.flushCompletedSpecs() |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) flushCompletedSpecs() { |
||||
|
if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
for _, setupSummary := range aggregator.aggregatedBeforeSuites { |
||||
|
aggregator.announceBeforeSuite(setupSummary) |
||||
|
} |
||||
|
|
||||
|
for _, specSummary := range aggregator.completedSpecs { |
||||
|
aggregator.announceSpec(specSummary) |
||||
|
} |
||||
|
|
||||
|
for _, setupSummary := range aggregator.aggregatedAfterSuites { |
||||
|
aggregator.announceAfterSuite(setupSummary) |
||||
|
} |
||||
|
|
||||
|
aggregator.aggregatedBeforeSuites = []*types.SetupSummary{} |
||||
|
aggregator.completedSpecs = []*types.SpecSummary{} |
||||
|
aggregator.aggregatedAfterSuites = []*types.SetupSummary{} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) announceBeforeSuite(setupSummary *types.SetupSummary) { |
||||
|
aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) |
||||
|
if setupSummary.State != types.SpecStatePassed { |
||||
|
aggregator.stenographer.AnnounceBeforeSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) announceAfterSuite(setupSummary *types.SetupSummary) { |
||||
|
aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) |
||||
|
if setupSummary.State != types.SpecStatePassed { |
||||
|
aggregator.stenographer.AnnounceAfterSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) announceSpec(specSummary *types.SpecSummary) { |
||||
|
if aggregator.config.Verbose && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { |
||||
|
aggregator.stenographer.AnnounceSpecWillRun(specSummary) |
||||
|
} |
||||
|
|
||||
|
aggregator.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput) |
||||
|
|
||||
|
switch specSummary.State { |
||||
|
case types.SpecStatePassed: |
||||
|
if specSummary.IsMeasurement { |
||||
|
aggregator.stenographer.AnnounceSuccessfulMeasurement(specSummary, aggregator.config.Succinct) |
||||
|
} else if specSummary.RunTime.Seconds() >= aggregator.config.SlowSpecThreshold { |
||||
|
aggregator.stenographer.AnnounceSuccessfulSlowSpec(specSummary, aggregator.config.Succinct) |
||||
|
} else { |
||||
|
aggregator.stenographer.AnnounceSuccessfulSpec(specSummary) |
||||
|
} |
||||
|
|
||||
|
case types.SpecStatePending: |
||||
|
aggregator.stenographer.AnnouncePendingSpec(specSummary, aggregator.config.NoisyPendings && !aggregator.config.Succinct) |
||||
|
case types.SpecStateSkipped: |
||||
|
aggregator.stenographer.AnnounceSkippedSpec(specSummary, aggregator.config.Succinct || !aggregator.config.NoisySkippings, aggregator.config.FullTrace) |
||||
|
case types.SpecStateTimedOut: |
||||
|
aggregator.stenographer.AnnounceSpecTimedOut(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) |
||||
|
case types.SpecStatePanicked: |
||||
|
aggregator.stenographer.AnnounceSpecPanicked(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) |
||||
|
case types.SpecStateFailed: |
||||
|
aggregator.stenographer.AnnounceSpecFailed(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (aggregator *Aggregator) registerSuiteEnding(suite *types.SuiteSummary) (finished bool, passed bool) { |
||||
|
aggregator.aggregatedSuiteEndings = append(aggregator.aggregatedSuiteEndings, suite) |
||||
|
if len(aggregator.aggregatedSuiteEndings) < aggregator.nodeCount { |
||||
|
return false, false |
||||
|
} |
||||
|
|
||||
|
aggregatedSuiteSummary := &types.SuiteSummary{} |
||||
|
aggregatedSuiteSummary.SuiteSucceeded = true |
||||
|
|
||||
|
for _, suiteSummary := range aggregator.aggregatedSuiteEndings { |
||||
|
if !suiteSummary.SuiteSucceeded { |
||||
|
aggregatedSuiteSummary.SuiteSucceeded = false |
||||
|
} |
||||
|
|
||||
|
aggregatedSuiteSummary.NumberOfSpecsThatWillBeRun += suiteSummary.NumberOfSpecsThatWillBeRun |
||||
|
aggregatedSuiteSummary.NumberOfTotalSpecs += suiteSummary.NumberOfTotalSpecs |
||||
|
aggregatedSuiteSummary.NumberOfPassedSpecs += suiteSummary.NumberOfPassedSpecs |
||||
|
aggregatedSuiteSummary.NumberOfFailedSpecs += suiteSummary.NumberOfFailedSpecs |
||||
|
aggregatedSuiteSummary.NumberOfPendingSpecs += suiteSummary.NumberOfPendingSpecs |
||||
|
aggregatedSuiteSummary.NumberOfSkippedSpecs += suiteSummary.NumberOfSkippedSpecs |
||||
|
aggregatedSuiteSummary.NumberOfFlakedSpecs += suiteSummary.NumberOfFlakedSpecs |
||||
|
} |
||||
|
|
||||
|
aggregatedSuiteSummary.RunTime = time.Since(aggregator.startTime) |
||||
|
|
||||
|
aggregator.stenographer.SummarizeFailures(aggregator.specs) |
||||
|
aggregator.stenographer.AnnounceSpecRunCompletion(aggregatedSuiteSummary, aggregator.config.Succinct) |
||||
|
|
||||
|
return true, aggregatedSuiteSummary.SuiteSucceeded |
||||
|
} |
@ -0,0 +1,147 @@ |
|||||
|
package remote |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/writer" |
||||
|
"github.com/onsi/ginkgo/reporters" |
||||
|
"github.com/onsi/ginkgo/reporters/stenographer" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
//An interface to net/http's client to allow the injection of fakes under test
|
||||
|
type Poster interface { |
||||
|
Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
The ForwardingReporter is a Ginkgo reporter that forwards information to |
||||
|
a Ginkgo remote server. |
||||
|
|
||||
|
When streaming parallel test output, this repoter is automatically installed by Ginkgo. |
||||
|
|
||||
|
This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner |
||||
|
detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter |
||||
|
in place of Ginkgo's DefaultReporter. |
||||
|
*/ |
||||
|
|
||||
|
type ForwardingReporter struct { |
||||
|
serverHost string |
||||
|
poster Poster |
||||
|
outputInterceptor OutputInterceptor |
||||
|
debugMode bool |
||||
|
debugFile *os.File |
||||
|
nestedReporter *reporters.DefaultReporter |
||||
|
} |
||||
|
|
||||
|
func NewForwardingReporter(config config.DefaultReporterConfigType, serverHost string, poster Poster, outputInterceptor OutputInterceptor, ginkgoWriter *writer.Writer, debugFile string) *ForwardingReporter { |
||||
|
reporter := &ForwardingReporter{ |
||||
|
serverHost: serverHost, |
||||
|
poster: poster, |
||||
|
outputInterceptor: outputInterceptor, |
||||
|
} |
||||
|
|
||||
|
if debugFile != "" { |
||||
|
var err error |
||||
|
reporter.debugMode = true |
||||
|
reporter.debugFile, err = os.Create(debugFile) |
||||
|
if err != nil { |
||||
|
fmt.Println(err.Error()) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
|
||||
|
if !config.Verbose { |
||||
|
//if verbose is true then the GinkgoWriter emits to stdout. Don't _also_ redirect GinkgoWriter output as that will result in duplication.
|
||||
|
ginkgoWriter.AndRedirectTo(reporter.debugFile) |
||||
|
} |
||||
|
outputInterceptor.StreamTo(reporter.debugFile) //This is not working
|
||||
|
|
||||
|
stenographer := stenographer.New(false, true, reporter.debugFile) |
||||
|
config.Succinct = false |
||||
|
config.Verbose = true |
||||
|
config.FullTrace = true |
||||
|
reporter.nestedReporter = reporters.NewDefaultReporter(config, stenographer) |
||||
|
} |
||||
|
|
||||
|
return reporter |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) post(path string, data interface{}) { |
||||
|
encoded, _ := json.Marshal(data) |
||||
|
buffer := bytes.NewBuffer(encoded) |
||||
|
reporter.poster.Post(reporter.serverHost+path, "application/json", buffer) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary *types.SuiteSummary) { |
||||
|
data := struct { |
||||
|
Config config.GinkgoConfigType `json:"config"` |
||||
|
Summary *types.SuiteSummary `json:"suite-summary"` |
||||
|
}{ |
||||
|
conf, |
||||
|
summary, |
||||
|
} |
||||
|
|
||||
|
reporter.outputInterceptor.StartInterceptingOutput() |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.SpecSuiteWillBegin(conf, summary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/SpecSuiteWillBegin", data) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() |
||||
|
reporter.outputInterceptor.StartInterceptingOutput() |
||||
|
setupSummary.CapturedOutput = output |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.BeforeSuiteDidRun(setupSummary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/BeforeSuiteDidRun", setupSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) SpecWillRun(specSummary *types.SpecSummary) { |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.SpecWillRun(specSummary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/SpecWillRun", specSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) SpecDidComplete(specSummary *types.SpecSummary) { |
||||
|
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() |
||||
|
reporter.outputInterceptor.StartInterceptingOutput() |
||||
|
specSummary.CapturedOutput = output |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.SpecDidComplete(specSummary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/SpecDidComplete", specSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() |
||||
|
reporter.outputInterceptor.StartInterceptingOutput() |
||||
|
setupSummary.CapturedOutput = output |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.AfterSuiteDidRun(setupSummary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/AfterSuiteDidRun", setupSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { |
||||
|
reporter.outputInterceptor.StopInterceptingAndReturnOutput() |
||||
|
if reporter.debugMode { |
||||
|
reporter.nestedReporter.SpecSuiteDidEnd(summary) |
||||
|
reporter.debugFile.Sync() |
||||
|
} |
||||
|
reporter.post("/SpecSuiteDidEnd", summary) |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
package remote |
||||
|
|
||||
|
import "os" |
||||
|
|
||||
|
/* |
||||
|
The OutputInterceptor is used by the ForwardingReporter to |
||||
|
intercept and capture all stdin and stderr output during a test run. |
||||
|
*/ |
||||
|
type OutputInterceptor interface { |
||||
|
StartInterceptingOutput() error |
||||
|
StopInterceptingAndReturnOutput() (string, error) |
||||
|
StreamTo(*os.File) |
||||
|
} |
@ -0,0 +1,82 @@ |
|||||
|
// +build freebsd openbsd netbsd dragonfly darwin linux solaris
|
||||
|
|
||||
|
package remote |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
|
||||
|
"github.com/nxadm/tail" |
||||
|
"golang.org/x/sys/unix" |
||||
|
) |
||||
|
|
||||
|
func NewOutputInterceptor() OutputInterceptor { |
||||
|
return &outputInterceptor{} |
||||
|
} |
||||
|
|
||||
|
type outputInterceptor struct { |
||||
|
redirectFile *os.File |
||||
|
streamTarget *os.File |
||||
|
intercepting bool |
||||
|
tailer *tail.Tail |
||||
|
doneTailing chan bool |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StartInterceptingOutput() error { |
||||
|
if interceptor.intercepting { |
||||
|
return errors.New("Already intercepting output!") |
||||
|
} |
||||
|
interceptor.intercepting = true |
||||
|
|
||||
|
var err error |
||||
|
|
||||
|
interceptor.redirectFile, err = ioutil.TempFile("", "ginkgo-output") |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// This might call Dup3 if the dup2 syscall is not available, e.g. on
|
||||
|
// linux/arm64 or linux/riscv64
|
||||
|
unix.Dup2(int(interceptor.redirectFile.Fd()), 1) |
||||
|
unix.Dup2(int(interceptor.redirectFile.Fd()), 2) |
||||
|
|
||||
|
if interceptor.streamTarget != nil { |
||||
|
interceptor.tailer, _ = tail.TailFile(interceptor.redirectFile.Name(), tail.Config{Follow: true}) |
||||
|
interceptor.doneTailing = make(chan bool) |
||||
|
|
||||
|
go func() { |
||||
|
for line := range interceptor.tailer.Lines { |
||||
|
interceptor.streamTarget.Write([]byte(line.Text + "\n")) |
||||
|
} |
||||
|
close(interceptor.doneTailing) |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) { |
||||
|
if !interceptor.intercepting { |
||||
|
return "", errors.New("Not intercepting output!") |
||||
|
} |
||||
|
|
||||
|
interceptor.redirectFile.Close() |
||||
|
output, err := ioutil.ReadFile(interceptor.redirectFile.Name()) |
||||
|
os.Remove(interceptor.redirectFile.Name()) |
||||
|
|
||||
|
interceptor.intercepting = false |
||||
|
|
||||
|
if interceptor.streamTarget != nil { |
||||
|
interceptor.tailer.Stop() |
||||
|
interceptor.tailer.Cleanup() |
||||
|
<-interceptor.doneTailing |
||||
|
interceptor.streamTarget.Sync() |
||||
|
} |
||||
|
|
||||
|
return string(output), err |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StreamTo(out *os.File) { |
||||
|
interceptor.streamTarget = out |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
// +build windows
|
||||
|
|
||||
|
package remote |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func NewOutputInterceptor() OutputInterceptor { |
||||
|
return &outputInterceptor{} |
||||
|
} |
||||
|
|
||||
|
type outputInterceptor struct { |
||||
|
intercepting bool |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StartInterceptingOutput() error { |
||||
|
if interceptor.intercepting { |
||||
|
return errors.New("Already intercepting output!") |
||||
|
} |
||||
|
interceptor.intercepting = true |
||||
|
|
||||
|
// not working on windows...
|
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) { |
||||
|
// not working on windows...
|
||||
|
interceptor.intercepting = false |
||||
|
|
||||
|
return "", nil |
||||
|
} |
||||
|
|
||||
|
func (interceptor *outputInterceptor) StreamTo(*os.File) {} |
@ -0,0 +1,224 @@ |
|||||
|
/* |
||||
|
|
||||
|
The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners. |
||||
|
This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser). |
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
package remote |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"io/ioutil" |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"sync" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/spec_iterator" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/reporters" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
/* |
||||
|
Server spins up on an automatically selected port and listens for communication from the forwarding reporter. |
||||
|
It then forwards that communication to attached reporters. |
||||
|
*/ |
||||
|
type Server struct { |
||||
|
listener net.Listener |
||||
|
reporters []reporters.Reporter |
||||
|
alives []func() bool |
||||
|
lock *sync.Mutex |
||||
|
beforeSuiteData types.RemoteBeforeSuiteData |
||||
|
parallelTotal int |
||||
|
counter int |
||||
|
} |
||||
|
|
||||
|
//Create a new server, automatically selecting a port
|
||||
|
func NewServer(parallelTotal int) (*Server, error) { |
||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
return &Server{ |
||||
|
listener: listener, |
||||
|
lock: &sync.Mutex{}, |
||||
|
alives: make([]func() bool, parallelTotal), |
||||
|
beforeSuiteData: types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStatePending}, |
||||
|
parallelTotal: parallelTotal, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
//Start the server. You don't need to `go s.Start()`, just `s.Start()`
|
||||
|
func (server *Server) Start() { |
||||
|
httpServer := &http.Server{} |
||||
|
mux := http.NewServeMux() |
||||
|
httpServer.Handler = mux |
||||
|
|
||||
|
//streaming endpoints
|
||||
|
mux.HandleFunc("/SpecSuiteWillBegin", server.specSuiteWillBegin) |
||||
|
mux.HandleFunc("/BeforeSuiteDidRun", server.beforeSuiteDidRun) |
||||
|
mux.HandleFunc("/AfterSuiteDidRun", server.afterSuiteDidRun) |
||||
|
mux.HandleFunc("/SpecWillRun", server.specWillRun) |
||||
|
mux.HandleFunc("/SpecDidComplete", server.specDidComplete) |
||||
|
mux.HandleFunc("/SpecSuiteDidEnd", server.specSuiteDidEnd) |
||||
|
|
||||
|
//synchronization endpoints
|
||||
|
mux.HandleFunc("/BeforeSuiteState", server.handleBeforeSuiteState) |
||||
|
mux.HandleFunc("/RemoteAfterSuiteData", server.handleRemoteAfterSuiteData) |
||||
|
mux.HandleFunc("/counter", server.handleCounter) |
||||
|
mux.HandleFunc("/has-counter", server.handleHasCounter) //for backward compatibility
|
||||
|
|
||||
|
go httpServer.Serve(server.listener) |
||||
|
} |
||||
|
|
||||
|
//Stop the server
|
||||
|
func (server *Server) Close() { |
||||
|
server.listener.Close() |
||||
|
} |
||||
|
|
||||
|
//The address the server can be reached it. Pass this into the `ForwardingReporter`.
|
||||
|
func (server *Server) Address() string { |
||||
|
return "http://" + server.listener.Addr().String() |
||||
|
} |
||||
|
|
||||
|
//
|
||||
|
// Streaming Endpoints
|
||||
|
//
|
||||
|
|
||||
|
//The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters`
|
||||
|
func (server *Server) readAll(request *http.Request) []byte { |
||||
|
defer request.Body.Close() |
||||
|
body, _ := ioutil.ReadAll(request.Body) |
||||
|
return body |
||||
|
} |
||||
|
|
||||
|
func (server *Server) RegisterReporters(reporters ...reporters.Reporter) { |
||||
|
server.reporters = reporters |
||||
|
} |
||||
|
|
||||
|
func (server *Server) specSuiteWillBegin(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
|
||||
|
var data struct { |
||||
|
Config config.GinkgoConfigType `json:"config"` |
||||
|
Summary *types.SuiteSummary `json:"suite-summary"` |
||||
|
} |
||||
|
|
||||
|
json.Unmarshal(body, &data) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.SpecSuiteWillBegin(data.Config, data.Summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) beforeSuiteDidRun(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
var setupSummary *types.SetupSummary |
||||
|
json.Unmarshal(body, &setupSummary) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.BeforeSuiteDidRun(setupSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) afterSuiteDidRun(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
var setupSummary *types.SetupSummary |
||||
|
json.Unmarshal(body, &setupSummary) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.AfterSuiteDidRun(setupSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) specWillRun(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
var specSummary *types.SpecSummary |
||||
|
json.Unmarshal(body, &specSummary) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.SpecWillRun(specSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) specDidComplete(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
var specSummary *types.SpecSummary |
||||
|
json.Unmarshal(body, &specSummary) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.SpecDidComplete(specSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) { |
||||
|
body := server.readAll(request) |
||||
|
var suiteSummary *types.SuiteSummary |
||||
|
json.Unmarshal(body, &suiteSummary) |
||||
|
|
||||
|
for _, reporter := range server.reporters { |
||||
|
reporter.SpecSuiteDidEnd(suiteSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//
|
||||
|
// Synchronization Endpoints
|
||||
|
//
|
||||
|
|
||||
|
func (server *Server) RegisterAlive(node int, alive func() bool) { |
||||
|
server.lock.Lock() |
||||
|
defer server.lock.Unlock() |
||||
|
server.alives[node-1] = alive |
||||
|
} |
||||
|
|
||||
|
func (server *Server) nodeIsAlive(node int) bool { |
||||
|
server.lock.Lock() |
||||
|
defer server.lock.Unlock() |
||||
|
alive := server.alives[node-1] |
||||
|
if alive == nil { |
||||
|
return true |
||||
|
} |
||||
|
return alive() |
||||
|
} |
||||
|
|
||||
|
func (server *Server) handleBeforeSuiteState(writer http.ResponseWriter, request *http.Request) { |
||||
|
if request.Method == "POST" { |
||||
|
dec := json.NewDecoder(request.Body) |
||||
|
dec.Decode(&(server.beforeSuiteData)) |
||||
|
} else { |
||||
|
beforeSuiteData := server.beforeSuiteData |
||||
|
if beforeSuiteData.State == types.RemoteBeforeSuiteStatePending && !server.nodeIsAlive(1) { |
||||
|
beforeSuiteData.State = types.RemoteBeforeSuiteStateDisappeared |
||||
|
} |
||||
|
enc := json.NewEncoder(writer) |
||||
|
enc.Encode(beforeSuiteData) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (server *Server) handleRemoteAfterSuiteData(writer http.ResponseWriter, request *http.Request) { |
||||
|
afterSuiteData := types.RemoteAfterSuiteData{ |
||||
|
CanRun: true, |
||||
|
} |
||||
|
for i := 2; i <= server.parallelTotal; i++ { |
||||
|
afterSuiteData.CanRun = afterSuiteData.CanRun && !server.nodeIsAlive(i) |
||||
|
} |
||||
|
|
||||
|
enc := json.NewEncoder(writer) |
||||
|
enc.Encode(afterSuiteData) |
||||
|
} |
||||
|
|
||||
|
func (server *Server) handleCounter(writer http.ResponseWriter, request *http.Request) { |
||||
|
c := spec_iterator.Counter{} |
||||
|
server.lock.Lock() |
||||
|
c.Index = server.counter |
||||
|
server.counter++ |
||||
|
server.lock.Unlock() |
||||
|
|
||||
|
json.NewEncoder(writer).Encode(c) |
||||
|
} |
||||
|
|
||||
|
func (server *Server) handleHasCounter(writer http.ResponseWriter, request *http.Request) { |
||||
|
writer.Write([]byte("")) |
||||
|
} |
@ -0,0 +1,247 @@ |
|||||
|
package spec |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"time" |
||||
|
|
||||
|
"sync" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/containernode" |
||||
|
"github.com/onsi/ginkgo/internal/leafnodes" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type Spec struct { |
||||
|
subject leafnodes.SubjectNode |
||||
|
focused bool |
||||
|
announceProgress bool |
||||
|
|
||||
|
containers []*containernode.ContainerNode |
||||
|
|
||||
|
state types.SpecState |
||||
|
runTime time.Duration |
||||
|
startTime time.Time |
||||
|
failure types.SpecFailure |
||||
|
previousFailures bool |
||||
|
|
||||
|
stateMutex *sync.Mutex |
||||
|
} |
||||
|
|
||||
|
func New(subject leafnodes.SubjectNode, containers []*containernode.ContainerNode, announceProgress bool) *Spec { |
||||
|
spec := &Spec{ |
||||
|
subject: subject, |
||||
|
containers: containers, |
||||
|
focused: subject.Flag() == types.FlagTypeFocused, |
||||
|
announceProgress: announceProgress, |
||||
|
stateMutex: &sync.Mutex{}, |
||||
|
} |
||||
|
|
||||
|
spec.processFlag(subject.Flag()) |
||||
|
for i := len(containers) - 1; i >= 0; i-- { |
||||
|
spec.processFlag(containers[i].Flag()) |
||||
|
} |
||||
|
|
||||
|
return spec |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) processFlag(flag types.FlagType) { |
||||
|
if flag == types.FlagTypeFocused { |
||||
|
spec.focused = true |
||||
|
} else if flag == types.FlagTypePending { |
||||
|
spec.setState(types.SpecStatePending) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Skip() { |
||||
|
spec.setState(types.SpecStateSkipped) |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Failed() bool { |
||||
|
return spec.getState() == types.SpecStateFailed || spec.getState() == types.SpecStatePanicked || spec.getState() == types.SpecStateTimedOut |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Passed() bool { |
||||
|
return spec.getState() == types.SpecStatePassed |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Flaked() bool { |
||||
|
return spec.getState() == types.SpecStatePassed && spec.previousFailures |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Pending() bool { |
||||
|
return spec.getState() == types.SpecStatePending |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Skipped() bool { |
||||
|
return spec.getState() == types.SpecStateSkipped |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Focused() bool { |
||||
|
return spec.focused |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) IsMeasurement() bool { |
||||
|
return spec.subject.Type() == types.SpecComponentTypeMeasure |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Summary(suiteID string) *types.SpecSummary { |
||||
|
componentTexts := make([]string, len(spec.containers)+1) |
||||
|
componentCodeLocations := make([]types.CodeLocation, len(spec.containers)+1) |
||||
|
|
||||
|
for i, container := range spec.containers { |
||||
|
componentTexts[i] = container.Text() |
||||
|
componentCodeLocations[i] = container.CodeLocation() |
||||
|
} |
||||
|
|
||||
|
componentTexts[len(spec.containers)] = spec.subject.Text() |
||||
|
componentCodeLocations[len(spec.containers)] = spec.subject.CodeLocation() |
||||
|
|
||||
|
runTime := spec.runTime |
||||
|
if runTime == 0 && !spec.startTime.IsZero() { |
||||
|
runTime = time.Since(spec.startTime) |
||||
|
} |
||||
|
|
||||
|
return &types.SpecSummary{ |
||||
|
IsMeasurement: spec.IsMeasurement(), |
||||
|
NumberOfSamples: spec.subject.Samples(), |
||||
|
ComponentTexts: componentTexts, |
||||
|
ComponentCodeLocations: componentCodeLocations, |
||||
|
State: spec.getState(), |
||||
|
RunTime: runTime, |
||||
|
Failure: spec.failure, |
||||
|
Measurements: spec.measurementsReport(), |
||||
|
SuiteID: suiteID, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) ConcatenatedString() string { |
||||
|
s := "" |
||||
|
for _, container := range spec.containers { |
||||
|
s += container.Text() + " " |
||||
|
} |
||||
|
|
||||
|
return s + spec.subject.Text() |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) Run(writer io.Writer) { |
||||
|
if spec.getState() == types.SpecStateFailed { |
||||
|
spec.previousFailures = true |
||||
|
} |
||||
|
|
||||
|
spec.startTime = time.Now() |
||||
|
defer func() { |
||||
|
spec.runTime = time.Since(spec.startTime) |
||||
|
}() |
||||
|
|
||||
|
for sample := 0; sample < spec.subject.Samples(); sample++ { |
||||
|
spec.runSample(sample, writer) |
||||
|
|
||||
|
if spec.getState() != types.SpecStatePassed { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) getState() types.SpecState { |
||||
|
spec.stateMutex.Lock() |
||||
|
defer spec.stateMutex.Unlock() |
||||
|
return spec.state |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) setState(state types.SpecState) { |
||||
|
spec.stateMutex.Lock() |
||||
|
defer spec.stateMutex.Unlock() |
||||
|
spec.state = state |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) runSample(sample int, writer io.Writer) { |
||||
|
spec.setState(types.SpecStatePassed) |
||||
|
spec.failure = types.SpecFailure{} |
||||
|
innerMostContainerIndexToUnwind := -1 |
||||
|
|
||||
|
defer func() { |
||||
|
for i := innerMostContainerIndexToUnwind; i >= 0; i-- { |
||||
|
container := spec.containers[i] |
||||
|
for _, justAfterEach := range container.SetupNodesOfType(types.SpecComponentTypeJustAfterEach) { |
||||
|
spec.announceSetupNode(writer, "JustAfterEach", container, justAfterEach) |
||||
|
justAfterEachState, justAfterEachFailure := justAfterEach.Run() |
||||
|
if justAfterEachState != types.SpecStatePassed && spec.state == types.SpecStatePassed { |
||||
|
spec.state = justAfterEachState |
||||
|
spec.failure = justAfterEachFailure |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for i := innerMostContainerIndexToUnwind; i >= 0; i-- { |
||||
|
container := spec.containers[i] |
||||
|
for _, afterEach := range container.SetupNodesOfType(types.SpecComponentTypeAfterEach) { |
||||
|
spec.announceSetupNode(writer, "AfterEach", container, afterEach) |
||||
|
afterEachState, afterEachFailure := afterEach.Run() |
||||
|
if afterEachState != types.SpecStatePassed && spec.getState() == types.SpecStatePassed { |
||||
|
spec.setState(afterEachState) |
||||
|
spec.failure = afterEachFailure |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
for i, container := range spec.containers { |
||||
|
innerMostContainerIndexToUnwind = i |
||||
|
for _, beforeEach := range container.SetupNodesOfType(types.SpecComponentTypeBeforeEach) { |
||||
|
spec.announceSetupNode(writer, "BeforeEach", container, beforeEach) |
||||
|
s, f := beforeEach.Run() |
||||
|
spec.failure = f |
||||
|
spec.setState(s) |
||||
|
if spec.getState() != types.SpecStatePassed { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _, container := range spec.containers { |
||||
|
for _, justBeforeEach := range container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach) { |
||||
|
spec.announceSetupNode(writer, "JustBeforeEach", container, justBeforeEach) |
||||
|
s, f := justBeforeEach.Run() |
||||
|
spec.failure = f |
||||
|
spec.setState(s) |
||||
|
if spec.getState() != types.SpecStatePassed { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
spec.announceSubject(writer, spec.subject) |
||||
|
s, f := spec.subject.Run() |
||||
|
spec.failure = f |
||||
|
spec.setState(s) |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) announceSetupNode(writer io.Writer, nodeType string, container *containernode.ContainerNode, setupNode leafnodes.BasicNode) { |
||||
|
if spec.announceProgress { |
||||
|
s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, container.Text(), setupNode.CodeLocation().String()) |
||||
|
writer.Write([]byte(s)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) announceSubject(writer io.Writer, subject leafnodes.SubjectNode) { |
||||
|
if spec.announceProgress { |
||||
|
nodeType := "" |
||||
|
switch subject.Type() { |
||||
|
case types.SpecComponentTypeIt: |
||||
|
nodeType = "It" |
||||
|
case types.SpecComponentTypeMeasure: |
||||
|
nodeType = "Measure" |
||||
|
} |
||||
|
s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, subject.Text(), subject.CodeLocation().String()) |
||||
|
writer.Write([]byte(s)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (spec *Spec) measurementsReport() map[string]*types.SpecMeasurement { |
||||
|
if !spec.IsMeasurement() || spec.Failed() { |
||||
|
return map[string]*types.SpecMeasurement{} |
||||
|
} |
||||
|
|
||||
|
return spec.subject.(*leafnodes.MeasureNode).MeasurementsReport() |
||||
|
} |
@ -0,0 +1,144 @@ |
|||||
|
package spec |
||||
|
|
||||
|
import ( |
||||
|
"math/rand" |
||||
|
"regexp" |
||||
|
"sort" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
type Specs struct { |
||||
|
specs []*Spec |
||||
|
names []string |
||||
|
|
||||
|
hasProgrammaticFocus bool |
||||
|
RegexScansFilePath bool |
||||
|
} |
||||
|
|
||||
|
func NewSpecs(specs []*Spec) *Specs { |
||||
|
names := make([]string, len(specs)) |
||||
|
for i, spec := range specs { |
||||
|
names[i] = spec.ConcatenatedString() |
||||
|
} |
||||
|
return &Specs{ |
||||
|
specs: specs, |
||||
|
names: names, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) Specs() []*Spec { |
||||
|
return e.specs |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) HasProgrammaticFocus() bool { |
||||
|
return e.hasProgrammaticFocus |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) Shuffle(r *rand.Rand) { |
||||
|
sort.Sort(e) |
||||
|
permutation := r.Perm(len(e.specs)) |
||||
|
shuffledSpecs := make([]*Spec, len(e.specs)) |
||||
|
names := make([]string, len(e.specs)) |
||||
|
for i, j := range permutation { |
||||
|
shuffledSpecs[i] = e.specs[j] |
||||
|
names[i] = e.names[j] |
||||
|
} |
||||
|
e.specs = shuffledSpecs |
||||
|
e.names = names |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) ApplyFocus(description string, focus, skip []string) { |
||||
|
if len(focus)+len(skip) == 0 { |
||||
|
e.applyProgrammaticFocus() |
||||
|
} else { |
||||
|
e.applyRegExpFocusAndSkip(description, focus, skip) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) applyProgrammaticFocus() { |
||||
|
e.hasProgrammaticFocus = false |
||||
|
for _, spec := range e.specs { |
||||
|
if spec.Focused() && !spec.Pending() { |
||||
|
e.hasProgrammaticFocus = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if e.hasProgrammaticFocus { |
||||
|
for _, spec := range e.specs { |
||||
|
if !spec.Focused() { |
||||
|
spec.Skip() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// toMatch returns a byte[] to be used by regex matchers. When adding new behaviours to the matching function,
|
||||
|
// this is the place which we append to.
|
||||
|
func (e *Specs) toMatch(description string, i int) []byte { |
||||
|
if i > len(e.names) { |
||||
|
return nil |
||||
|
} |
||||
|
if e.RegexScansFilePath { |
||||
|
return []byte( |
||||
|
description + " " + |
||||
|
e.names[i] + " " + |
||||
|
e.specs[i].subject.CodeLocation().FileName) |
||||
|
} else { |
||||
|
return []byte( |
||||
|
description + " " + |
||||
|
e.names[i]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) applyRegExpFocusAndSkip(description string, focus, skip []string) { |
||||
|
var focusFilter, skipFilter *regexp.Regexp |
||||
|
if len(focus) > 0 { |
||||
|
focusFilter = regexp.MustCompile(strings.Join(focus, "|")) |
||||
|
} |
||||
|
if len(skip) > 0 { |
||||
|
skipFilter = regexp.MustCompile(strings.Join(skip, "|")) |
||||
|
} |
||||
|
|
||||
|
for i, spec := range e.specs { |
||||
|
matchesFocus := true |
||||
|
matchesSkip := false |
||||
|
|
||||
|
toMatch := e.toMatch(description, i) |
||||
|
|
||||
|
if focusFilter != nil { |
||||
|
matchesFocus = focusFilter.Match(toMatch) |
||||
|
} |
||||
|
|
||||
|
if skipFilter != nil { |
||||
|
matchesSkip = skipFilter.Match(toMatch) |
||||
|
} |
||||
|
|
||||
|
if !matchesFocus || matchesSkip { |
||||
|
spec.Skip() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) SkipMeasurements() { |
||||
|
for _, spec := range e.specs { |
||||
|
if spec.IsMeasurement() { |
||||
|
spec.Skip() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//sort.Interface
|
||||
|
|
||||
|
func (e *Specs) Len() int { |
||||
|
return len(e.specs) |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) Less(i, j int) bool { |
||||
|
return e.names[i] < e.names[j] |
||||
|
} |
||||
|
|
||||
|
func (e *Specs) Swap(i, j int) { |
||||
|
e.names[i], e.names[j] = e.names[j], e.names[i] |
||||
|
e.specs[i], e.specs[j] = e.specs[j], e.specs[i] |
||||
|
} |
@ -0,0 +1,55 @@ |
|||||
|
package spec_iterator |
||||
|
|
||||
|
func ParallelizedIndexRange(length int, parallelTotal int, parallelNode int) (startIndex int, count int) { |
||||
|
if length == 0 { |
||||
|
return 0, 0 |
||||
|
} |
||||
|
|
||||
|
// We have more nodes than tests. Trivial case.
|
||||
|
if parallelTotal >= length { |
||||
|
if parallelNode > length { |
||||
|
return 0, 0 |
||||
|
} else { |
||||
|
return parallelNode - 1, 1 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// This is the minimum amount of tests that a node will be required to run
|
||||
|
minTestsPerNode := length / parallelTotal |
||||
|
|
||||
|
// This is the maximum amount of tests that a node will be required to run
|
||||
|
// The algorithm guarantees that this would be equal to at least the minimum amount
|
||||
|
// and at most one more
|
||||
|
maxTestsPerNode := minTestsPerNode |
||||
|
if length%parallelTotal != 0 { |
||||
|
maxTestsPerNode++ |
||||
|
} |
||||
|
|
||||
|
// Number of nodes that will have to run the maximum amount of tests per node
|
||||
|
numMaxLoadNodes := length % parallelTotal |
||||
|
|
||||
|
// Number of nodes that precede the current node and will have to run the maximum amount of tests per node
|
||||
|
var numPrecedingMaxLoadNodes int |
||||
|
if parallelNode > numMaxLoadNodes { |
||||
|
numPrecedingMaxLoadNodes = numMaxLoadNodes |
||||
|
} else { |
||||
|
numPrecedingMaxLoadNodes = parallelNode - 1 |
||||
|
} |
||||
|
|
||||
|
// Number of nodes that precede the current node and will have to run the minimum amount of tests per node
|
||||
|
var numPrecedingMinLoadNodes int |
||||
|
if parallelNode <= numMaxLoadNodes { |
||||
|
numPrecedingMinLoadNodes = 0 |
||||
|
} else { |
||||
|
numPrecedingMinLoadNodes = parallelNode - numMaxLoadNodes - 1 |
||||
|
} |
||||
|
|
||||
|
// Evaluate the test start index and number of tests to run
|
||||
|
startIndex = numPrecedingMaxLoadNodes*maxTestsPerNode + numPrecedingMinLoadNodes*minTestsPerNode |
||||
|
if parallelNode > numMaxLoadNodes { |
||||
|
count = minTestsPerNode |
||||
|
} else { |
||||
|
count = maxTestsPerNode |
||||
|
} |
||||
|
return |
||||
|
} |
@ -0,0 +1,59 @@ |
|||||
|
package spec_iterator |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/spec" |
||||
|
) |
||||
|
|
||||
|
type ParallelIterator struct { |
||||
|
specs []*spec.Spec |
||||
|
host string |
||||
|
client *http.Client |
||||
|
} |
||||
|
|
||||
|
func NewParallelIterator(specs []*spec.Spec, host string) *ParallelIterator { |
||||
|
return &ParallelIterator{ |
||||
|
specs: specs, |
||||
|
host: host, |
||||
|
client: &http.Client{}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *ParallelIterator) Next() (*spec.Spec, error) { |
||||
|
resp, err := s.client.Get(s.host + "/counter") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
defer resp.Body.Close() |
||||
|
|
||||
|
if resp.StatusCode != http.StatusOK { |
||||
|
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) |
||||
|
} |
||||
|
|
||||
|
var counter Counter |
||||
|
err = json.NewDecoder(resp.Body).Decode(&counter) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if counter.Index >= len(s.specs) { |
||||
|
return nil, ErrClosed |
||||
|
} |
||||
|
|
||||
|
return s.specs[counter.Index], nil |
||||
|
} |
||||
|
|
||||
|
func (s *ParallelIterator) NumberOfSpecsPriorToIteration() int { |
||||
|
return len(s.specs) |
||||
|
} |
||||
|
|
||||
|
func (s *ParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { |
||||
|
return -1, false |
||||
|
} |
||||
|
|
||||
|
func (s *ParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { |
||||
|
return -1, false |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
package spec_iterator |
||||
|
|
||||
|
import ( |
||||
|
"github.com/onsi/ginkgo/internal/spec" |
||||
|
) |
||||
|
|
||||
|
type SerialIterator struct { |
||||
|
specs []*spec.Spec |
||||
|
index int |
||||
|
} |
||||
|
|
||||
|
func NewSerialIterator(specs []*spec.Spec) *SerialIterator { |
||||
|
return &SerialIterator{ |
||||
|
specs: specs, |
||||
|
index: 0, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *SerialIterator) Next() (*spec.Spec, error) { |
||||
|
if s.index >= len(s.specs) { |
||||
|
return nil, ErrClosed |
||||
|
} |
||||
|
|
||||
|
spec := s.specs[s.index] |
||||
|
s.index += 1 |
||||
|
return spec, nil |
||||
|
} |
||||
|
|
||||
|
func (s *SerialIterator) NumberOfSpecsPriorToIteration() int { |
||||
|
return len(s.specs) |
||||
|
} |
||||
|
|
||||
|
func (s *SerialIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { |
||||
|
return len(s.specs), true |
||||
|
} |
||||
|
|
||||
|
func (s *SerialIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { |
||||
|
count := 0 |
||||
|
for _, s := range s.specs { |
||||
|
if !s.Skipped() && !s.Pending() { |
||||
|
count += 1 |
||||
|
} |
||||
|
} |
||||
|
return count, true |
||||
|
} |
@ -0,0 +1,47 @@ |
|||||
|
package spec_iterator |
||||
|
|
||||
|
import "github.com/onsi/ginkgo/internal/spec" |
||||
|
|
||||
|
type ShardedParallelIterator struct { |
||||
|
specs []*spec.Spec |
||||
|
index int |
||||
|
maxIndex int |
||||
|
} |
||||
|
|
||||
|
func NewShardedParallelIterator(specs []*spec.Spec, total int, node int) *ShardedParallelIterator { |
||||
|
startIndex, count := ParallelizedIndexRange(len(specs), total, node) |
||||
|
|
||||
|
return &ShardedParallelIterator{ |
||||
|
specs: specs, |
||||
|
index: startIndex, |
||||
|
maxIndex: startIndex + count, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *ShardedParallelIterator) Next() (*spec.Spec, error) { |
||||
|
if s.index >= s.maxIndex { |
||||
|
return nil, ErrClosed |
||||
|
} |
||||
|
|
||||
|
spec := s.specs[s.index] |
||||
|
s.index += 1 |
||||
|
return spec, nil |
||||
|
} |
||||
|
|
||||
|
func (s *ShardedParallelIterator) NumberOfSpecsPriorToIteration() int { |
||||
|
return len(s.specs) |
||||
|
} |
||||
|
|
||||
|
func (s *ShardedParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { |
||||
|
return s.maxIndex - s.index, true |
||||
|
} |
||||
|
|
||||
|
func (s *ShardedParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { |
||||
|
count := 0 |
||||
|
for i := s.index; i < s.maxIndex; i += 1 { |
||||
|
if !s.specs[i].Skipped() && !s.specs[i].Pending() { |
||||
|
count += 1 |
||||
|
} |
||||
|
} |
||||
|
return count, true |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
package spec_iterator |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/spec" |
||||
|
) |
||||
|
|
||||
|
var ErrClosed = errors.New("no more specs to run") |
||||
|
|
||||
|
type SpecIterator interface { |
||||
|
Next() (*spec.Spec, error) |
||||
|
NumberOfSpecsPriorToIteration() int |
||||
|
NumberOfSpecsToProcessIfKnown() (int, bool) |
||||
|
NumberOfSpecsThatWillBeRunIfKnown() (int, bool) |
||||
|
} |
||||
|
|
||||
|
type Counter struct { |
||||
|
Index int `json:"index"` |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package specrunner |
||||
|
|
||||
|
import ( |
||||
|
"crypto/rand" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
func randomID() string { |
||||
|
b := make([]byte, 8) |
||||
|
_, err := rand.Read(b) |
||||
|
if err != nil { |
||||
|
return "" |
||||
|
} |
||||
|
return fmt.Sprintf("%x-%x-%x-%x", b[0:2], b[2:4], b[4:6], b[6:8]) |
||||
|
} |
@ -0,0 +1,411 @@ |
|||||
|
package specrunner |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"os/signal" |
||||
|
"sync" |
||||
|
"syscall" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/spec_iterator" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/internal/leafnodes" |
||||
|
"github.com/onsi/ginkgo/internal/spec" |
||||
|
Writer "github.com/onsi/ginkgo/internal/writer" |
||||
|
"github.com/onsi/ginkgo/reporters" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
|
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type SpecRunner struct { |
||||
|
description string |
||||
|
beforeSuiteNode leafnodes.SuiteNode |
||||
|
iterator spec_iterator.SpecIterator |
||||
|
afterSuiteNode leafnodes.SuiteNode |
||||
|
reporters []reporters.Reporter |
||||
|
startTime time.Time |
||||
|
suiteID string |
||||
|
runningSpec *spec.Spec |
||||
|
writer Writer.WriterInterface |
||||
|
config config.GinkgoConfigType |
||||
|
interrupted bool |
||||
|
processedSpecs []*spec.Spec |
||||
|
lock *sync.Mutex |
||||
|
} |
||||
|
|
||||
|
func New(description string, beforeSuiteNode leafnodes.SuiteNode, iterator spec_iterator.SpecIterator, afterSuiteNode leafnodes.SuiteNode, reporters []reporters.Reporter, writer Writer.WriterInterface, config config.GinkgoConfigType) *SpecRunner { |
||||
|
return &SpecRunner{ |
||||
|
description: description, |
||||
|
beforeSuiteNode: beforeSuiteNode, |
||||
|
iterator: iterator, |
||||
|
afterSuiteNode: afterSuiteNode, |
||||
|
reporters: reporters, |
||||
|
writer: writer, |
||||
|
config: config, |
||||
|
suiteID: randomID(), |
||||
|
lock: &sync.Mutex{}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) Run() bool { |
||||
|
if runner.config.DryRun { |
||||
|
runner.performDryRun() |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
runner.reportSuiteWillBegin() |
||||
|
signalRegistered := make(chan struct{}) |
||||
|
go runner.registerForInterrupts(signalRegistered) |
||||
|
<-signalRegistered |
||||
|
|
||||
|
suitePassed := runner.runBeforeSuite() |
||||
|
|
||||
|
if suitePassed { |
||||
|
suitePassed = runner.runSpecs() |
||||
|
} |
||||
|
|
||||
|
runner.blockForeverIfInterrupted() |
||||
|
|
||||
|
suitePassed = runner.runAfterSuite() && suitePassed |
||||
|
|
||||
|
runner.reportSuiteDidEnd(suitePassed) |
||||
|
|
||||
|
return suitePassed |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) performDryRun() { |
||||
|
runner.reportSuiteWillBegin() |
||||
|
|
||||
|
if runner.beforeSuiteNode != nil { |
||||
|
summary := runner.beforeSuiteNode.Summary() |
||||
|
summary.State = types.SpecStatePassed |
||||
|
runner.reportBeforeSuite(summary) |
||||
|
} |
||||
|
|
||||
|
for { |
||||
|
spec, err := runner.iterator.Next() |
||||
|
if err == spec_iterator.ErrClosed { |
||||
|
break |
||||
|
} |
||||
|
if err != nil { |
||||
|
fmt.Println("failed to iterate over tests:\n" + err.Error()) |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
runner.processedSpecs = append(runner.processedSpecs, spec) |
||||
|
|
||||
|
summary := spec.Summary(runner.suiteID) |
||||
|
runner.reportSpecWillRun(summary) |
||||
|
if summary.State == types.SpecStateInvalid { |
||||
|
summary.State = types.SpecStatePassed |
||||
|
} |
||||
|
runner.reportSpecDidComplete(summary, false) |
||||
|
} |
||||
|
|
||||
|
if runner.afterSuiteNode != nil { |
||||
|
summary := runner.afterSuiteNode.Summary() |
||||
|
summary.State = types.SpecStatePassed |
||||
|
runner.reportAfterSuite(summary) |
||||
|
} |
||||
|
|
||||
|
runner.reportSuiteDidEnd(true) |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) runBeforeSuite() bool { |
||||
|
if runner.beforeSuiteNode == nil || runner.wasInterrupted() { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
runner.writer.Truncate() |
||||
|
conf := runner.config |
||||
|
passed := runner.beforeSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) |
||||
|
if !passed { |
||||
|
runner.writer.DumpOut() |
||||
|
} |
||||
|
runner.reportBeforeSuite(runner.beforeSuiteNode.Summary()) |
||||
|
return passed |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) runAfterSuite() bool { |
||||
|
if runner.afterSuiteNode == nil { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
runner.writer.Truncate() |
||||
|
conf := runner.config |
||||
|
passed := runner.afterSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) |
||||
|
if !passed { |
||||
|
runner.writer.DumpOut() |
||||
|
} |
||||
|
runner.reportAfterSuite(runner.afterSuiteNode.Summary()) |
||||
|
return passed |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) runSpecs() bool { |
||||
|
suiteFailed := false |
||||
|
skipRemainingSpecs := false |
||||
|
for { |
||||
|
spec, err := runner.iterator.Next() |
||||
|
if err == spec_iterator.ErrClosed { |
||||
|
break |
||||
|
} |
||||
|
if err != nil { |
||||
|
fmt.Println("failed to iterate over tests:\n" + err.Error()) |
||||
|
suiteFailed = true |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
runner.processedSpecs = append(runner.processedSpecs, spec) |
||||
|
|
||||
|
if runner.wasInterrupted() { |
||||
|
break |
||||
|
} |
||||
|
if skipRemainingSpecs { |
||||
|
spec.Skip() |
||||
|
} |
||||
|
|
||||
|
if !spec.Skipped() && !spec.Pending() { |
||||
|
if passed := runner.runSpec(spec); !passed { |
||||
|
suiteFailed = true |
||||
|
} |
||||
|
} else if spec.Pending() && runner.config.FailOnPending { |
||||
|
runner.reportSpecWillRun(spec.Summary(runner.suiteID)) |
||||
|
suiteFailed = true |
||||
|
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) |
||||
|
} else { |
||||
|
runner.reportSpecWillRun(spec.Summary(runner.suiteID)) |
||||
|
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) |
||||
|
} |
||||
|
|
||||
|
if spec.Failed() && runner.config.FailFast { |
||||
|
skipRemainingSpecs = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return !suiteFailed |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) runSpec(spec *spec.Spec) (passed bool) { |
||||
|
maxAttempts := 1 |
||||
|
if runner.config.FlakeAttempts > 0 { |
||||
|
// uninitialized configs count as 1
|
||||
|
maxAttempts = runner.config.FlakeAttempts |
||||
|
} |
||||
|
|
||||
|
for i := 0; i < maxAttempts; i++ { |
||||
|
runner.reportSpecWillRun(spec.Summary(runner.suiteID)) |
||||
|
runner.runningSpec = spec |
||||
|
spec.Run(runner.writer) |
||||
|
runner.runningSpec = nil |
||||
|
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) |
||||
|
if !spec.Failed() { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) CurrentSpecSummary() (*types.SpecSummary, bool) { |
||||
|
if runner.runningSpec == nil { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
return runner.runningSpec.Summary(runner.suiteID), true |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) registerForInterrupts(signalRegistered chan struct{}) { |
||||
|
c := make(chan os.Signal, 1) |
||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) |
||||
|
close(signalRegistered) |
||||
|
|
||||
|
<-c |
||||
|
signal.Stop(c) |
||||
|
runner.markInterrupted() |
||||
|
go runner.registerForHardInterrupts() |
||||
|
runner.writer.DumpOutWithHeader(` |
||||
|
Received interrupt. Emitting contents of GinkgoWriter... |
||||
|
--------------------------------------------------------- |
||||
|
`) |
||||
|
if runner.afterSuiteNode != nil { |
||||
|
fmt.Fprint(os.Stderr, ` |
||||
|
--------------------------------------------------------- |
||||
|
Received interrupt. Running AfterSuite... |
||||
|
^C again to terminate immediately |
||||
|
`) |
||||
|
runner.runAfterSuite() |
||||
|
} |
||||
|
runner.reportSuiteDidEnd(false) |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) registerForHardInterrupts() { |
||||
|
c := make(chan os.Signal, 1) |
||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) |
||||
|
|
||||
|
<-c |
||||
|
fmt.Fprintln(os.Stderr, "\nReceived second interrupt. Shutting down.") |
||||
|
os.Exit(1) |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) blockForeverIfInterrupted() { |
||||
|
runner.lock.Lock() |
||||
|
interrupted := runner.interrupted |
||||
|
runner.lock.Unlock() |
||||
|
|
||||
|
if interrupted { |
||||
|
select {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) markInterrupted() { |
||||
|
runner.lock.Lock() |
||||
|
defer runner.lock.Unlock() |
||||
|
runner.interrupted = true |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) wasInterrupted() bool { |
||||
|
runner.lock.Lock() |
||||
|
defer runner.lock.Unlock() |
||||
|
return runner.interrupted |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportSuiteWillBegin() { |
||||
|
runner.startTime = time.Now() |
||||
|
summary := runner.suiteWillBeginSummary() |
||||
|
for _, reporter := range runner.reporters { |
||||
|
reporter.SpecSuiteWillBegin(runner.config, summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportBeforeSuite(summary *types.SetupSummary) { |
||||
|
for _, reporter := range runner.reporters { |
||||
|
reporter.BeforeSuiteDidRun(summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportAfterSuite(summary *types.SetupSummary) { |
||||
|
for _, reporter := range runner.reporters { |
||||
|
reporter.AfterSuiteDidRun(summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportSpecWillRun(summary *types.SpecSummary) { |
||||
|
runner.writer.Truncate() |
||||
|
|
||||
|
for _, reporter := range runner.reporters { |
||||
|
reporter.SpecWillRun(summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportSpecDidComplete(summary *types.SpecSummary, failed bool) { |
||||
|
if len(summary.CapturedOutput) == 0 { |
||||
|
summary.CapturedOutput = string(runner.writer.Bytes()) |
||||
|
} |
||||
|
for i := len(runner.reporters) - 1; i >= 1; i-- { |
||||
|
runner.reporters[i].SpecDidComplete(summary) |
||||
|
} |
||||
|
|
||||
|
if failed { |
||||
|
runner.writer.DumpOut() |
||||
|
} |
||||
|
|
||||
|
runner.reporters[0].SpecDidComplete(summary) |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) reportSuiteDidEnd(success bool) { |
||||
|
summary := runner.suiteDidEndSummary(success) |
||||
|
summary.RunTime = time.Since(runner.startTime) |
||||
|
for _, reporter := range runner.reporters { |
||||
|
reporter.SpecSuiteDidEnd(summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) countSpecsThatRanSatisfying(filter func(ex *spec.Spec) bool) (count int) { |
||||
|
count = 0 |
||||
|
|
||||
|
for _, spec := range runner.processedSpecs { |
||||
|
if filter(spec) { |
||||
|
count++ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return count |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) suiteDidEndSummary(success bool) *types.SuiteSummary { |
||||
|
numberOfSpecsThatWillBeRun := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return !ex.Skipped() && !ex.Pending() |
||||
|
}) |
||||
|
|
||||
|
numberOfPendingSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return ex.Pending() |
||||
|
}) |
||||
|
|
||||
|
numberOfSkippedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return ex.Skipped() |
||||
|
}) |
||||
|
|
||||
|
numberOfPassedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return ex.Passed() |
||||
|
}) |
||||
|
|
||||
|
numberOfFlakedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return ex.Flaked() |
||||
|
}) |
||||
|
|
||||
|
numberOfFailedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { |
||||
|
return ex.Failed() |
||||
|
}) |
||||
|
|
||||
|
if runner.beforeSuiteNode != nil && !runner.beforeSuiteNode.Passed() && !runner.config.DryRun { |
||||
|
var known bool |
||||
|
numberOfSpecsThatWillBeRun, known = runner.iterator.NumberOfSpecsThatWillBeRunIfKnown() |
||||
|
if !known { |
||||
|
numberOfSpecsThatWillBeRun = runner.iterator.NumberOfSpecsPriorToIteration() |
||||
|
} |
||||
|
numberOfFailedSpecs = numberOfSpecsThatWillBeRun |
||||
|
} |
||||
|
|
||||
|
return &types.SuiteSummary{ |
||||
|
SuiteDescription: runner.description, |
||||
|
SuiteSucceeded: success, |
||||
|
SuiteID: runner.suiteID, |
||||
|
|
||||
|
NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(), |
||||
|
NumberOfTotalSpecs: len(runner.processedSpecs), |
||||
|
NumberOfSpecsThatWillBeRun: numberOfSpecsThatWillBeRun, |
||||
|
NumberOfPendingSpecs: numberOfPendingSpecs, |
||||
|
NumberOfSkippedSpecs: numberOfSkippedSpecs, |
||||
|
NumberOfPassedSpecs: numberOfPassedSpecs, |
||||
|
NumberOfFailedSpecs: numberOfFailedSpecs, |
||||
|
NumberOfFlakedSpecs: numberOfFlakedSpecs, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (runner *SpecRunner) suiteWillBeginSummary() *types.SuiteSummary { |
||||
|
numTotal, known := runner.iterator.NumberOfSpecsToProcessIfKnown() |
||||
|
if !known { |
||||
|
numTotal = -1 |
||||
|
} |
||||
|
|
||||
|
numToRun, known := runner.iterator.NumberOfSpecsThatWillBeRunIfKnown() |
||||
|
if !known { |
||||
|
numToRun = -1 |
||||
|
} |
||||
|
|
||||
|
return &types.SuiteSummary{ |
||||
|
SuiteDescription: runner.description, |
||||
|
SuiteID: runner.suiteID, |
||||
|
|
||||
|
NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(), |
||||
|
NumberOfTotalSpecs: numTotal, |
||||
|
NumberOfSpecsThatWillBeRun: numToRun, |
||||
|
NumberOfPendingSpecs: -1, |
||||
|
NumberOfSkippedSpecs: -1, |
||||
|
NumberOfPassedSpecs: -1, |
||||
|
NumberOfFailedSpecs: -1, |
||||
|
NumberOfFlakedSpecs: -1, |
||||
|
} |
||||
|
} |
@ -0,0 +1,227 @@ |
|||||
|
package suite |
||||
|
|
||||
|
import ( |
||||
|
"math/rand" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/internal/spec_iterator" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/internal/containernode" |
||||
|
"github.com/onsi/ginkgo/internal/failer" |
||||
|
"github.com/onsi/ginkgo/internal/leafnodes" |
||||
|
"github.com/onsi/ginkgo/internal/spec" |
||||
|
"github.com/onsi/ginkgo/internal/specrunner" |
||||
|
"github.com/onsi/ginkgo/internal/writer" |
||||
|
"github.com/onsi/ginkgo/reporters" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type ginkgoTestingT interface { |
||||
|
Fail() |
||||
|
} |
||||
|
|
||||
|
type deferredContainerNode struct { |
||||
|
text string |
||||
|
body func() |
||||
|
flag types.FlagType |
||||
|
codeLocation types.CodeLocation |
||||
|
} |
||||
|
|
||||
|
type Suite struct { |
||||
|
topLevelContainer *containernode.ContainerNode |
||||
|
currentContainer *containernode.ContainerNode |
||||
|
|
||||
|
deferredContainerNodes []deferredContainerNode |
||||
|
|
||||
|
containerIndex int |
||||
|
beforeSuiteNode leafnodes.SuiteNode |
||||
|
afterSuiteNode leafnodes.SuiteNode |
||||
|
runner *specrunner.SpecRunner |
||||
|
failer *failer.Failer |
||||
|
running bool |
||||
|
expandTopLevelNodes bool |
||||
|
} |
||||
|
|
||||
|
func New(failer *failer.Failer) *Suite { |
||||
|
topLevelContainer := containernode.New("[Top Level]", types.FlagTypeNone, types.CodeLocation{}) |
||||
|
|
||||
|
return &Suite{ |
||||
|
topLevelContainer: topLevelContainer, |
||||
|
currentContainer: topLevelContainer, |
||||
|
failer: failer, |
||||
|
containerIndex: 1, |
||||
|
deferredContainerNodes: []deferredContainerNode{}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) Run(t ginkgoTestingT, description string, reporters []reporters.Reporter, writer writer.WriterInterface, config config.GinkgoConfigType) (bool, bool) { |
||||
|
if config.ParallelTotal < 1 { |
||||
|
panic("ginkgo.parallel.total must be >= 1") |
||||
|
} |
||||
|
|
||||
|
if config.ParallelNode > config.ParallelTotal || config.ParallelNode < 1 { |
||||
|
panic("ginkgo.parallel.node is one-indexed and must be <= ginkgo.parallel.total") |
||||
|
} |
||||
|
|
||||
|
suite.expandTopLevelNodes = true |
||||
|
for _, deferredNode := range suite.deferredContainerNodes { |
||||
|
suite.PushContainerNode(deferredNode.text, deferredNode.body, deferredNode.flag, deferredNode.codeLocation) |
||||
|
} |
||||
|
|
||||
|
r := rand.New(rand.NewSource(config.RandomSeed)) |
||||
|
suite.topLevelContainer.Shuffle(r) |
||||
|
iterator, hasProgrammaticFocus := suite.generateSpecsIterator(description, config) |
||||
|
suite.runner = specrunner.New(description, suite.beforeSuiteNode, iterator, suite.afterSuiteNode, reporters, writer, config) |
||||
|
|
||||
|
suite.running = true |
||||
|
success := suite.runner.Run() |
||||
|
if !success { |
||||
|
t.Fail() |
||||
|
} |
||||
|
return success, hasProgrammaticFocus |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) generateSpecsIterator(description string, config config.GinkgoConfigType) (spec_iterator.SpecIterator, bool) { |
||||
|
specsSlice := []*spec.Spec{} |
||||
|
suite.topLevelContainer.BackPropagateProgrammaticFocus() |
||||
|
for _, collatedNodes := range suite.topLevelContainer.Collate() { |
||||
|
specsSlice = append(specsSlice, spec.New(collatedNodes.Subject, collatedNodes.Containers, config.EmitSpecProgress)) |
||||
|
} |
||||
|
|
||||
|
specs := spec.NewSpecs(specsSlice) |
||||
|
specs.RegexScansFilePath = config.RegexScansFilePath |
||||
|
|
||||
|
if config.RandomizeAllSpecs { |
||||
|
specs.Shuffle(rand.New(rand.NewSource(config.RandomSeed))) |
||||
|
} |
||||
|
|
||||
|
specs.ApplyFocus(description, config.FocusStrings, config.SkipStrings) |
||||
|
|
||||
|
if config.SkipMeasurements { |
||||
|
specs.SkipMeasurements() |
||||
|
} |
||||
|
|
||||
|
var iterator spec_iterator.SpecIterator |
||||
|
|
||||
|
if config.ParallelTotal > 1 { |
||||
|
iterator = spec_iterator.NewParallelIterator(specs.Specs(), config.SyncHost) |
||||
|
resp, err := http.Get(config.SyncHost + "/has-counter") |
||||
|
if err != nil || resp.StatusCode != http.StatusOK { |
||||
|
iterator = spec_iterator.NewShardedParallelIterator(specs.Specs(), config.ParallelTotal, config.ParallelNode) |
||||
|
} |
||||
|
} else { |
||||
|
iterator = spec_iterator.NewSerialIterator(specs.Specs()) |
||||
|
} |
||||
|
|
||||
|
return iterator, specs.HasProgrammaticFocus() |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) CurrentRunningSpecSummary() (*types.SpecSummary, bool) { |
||||
|
if !suite.running { |
||||
|
return nil, false |
||||
|
} |
||||
|
return suite.runner.CurrentSpecSummary() |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) SetBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.beforeSuiteNode != nil { |
||||
|
panic("You may only call BeforeSuite once!") |
||||
|
} |
||||
|
suite.beforeSuiteNode = leafnodes.NewBeforeSuiteNode(body, codeLocation, timeout, suite.failer) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) SetAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.afterSuiteNode != nil { |
||||
|
panic("You may only call AfterSuite once!") |
||||
|
} |
||||
|
suite.afterSuiteNode = leafnodes.NewAfterSuiteNode(body, codeLocation, timeout, suite.failer) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) SetSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.beforeSuiteNode != nil { |
||||
|
panic("You may only call BeforeSuite once!") |
||||
|
} |
||||
|
suite.beforeSuiteNode = leafnodes.NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) SetSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.afterSuiteNode != nil { |
||||
|
panic("You may only call AfterSuite once!") |
||||
|
} |
||||
|
suite.afterSuiteNode = leafnodes.NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushContainerNode(text string, body func(), flag types.FlagType, codeLocation types.CodeLocation) { |
||||
|
/* |
||||
|
We defer walking the container nodes (which immediately evaluates the `body` function) |
||||
|
until `RunSpecs` is called. We do this by storing off the deferred container nodes. Then, when |
||||
|
`RunSpecs` is called we actually go through and add the container nodes to the test structure. |
||||
|
|
||||
|
This allows us to defer calling all the `body` functions until _after_ the top level functions |
||||
|
have been walked, _after_ func init()s have been called, and _after_ `go test` has called `flag.Parse()`. |
||||
|
|
||||
|
This allows users to load up configuration information in the `TestX` go test hook just before `RunSpecs` |
||||
|
is invoked and solves issues like #693 and makes the lifecycle easier to reason about. |
||||
|
|
||||
|
*/ |
||||
|
if !suite.expandTopLevelNodes { |
||||
|
suite.deferredContainerNodes = append(suite.deferredContainerNodes, deferredContainerNode{text, body, flag, codeLocation}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
container := containernode.New(text, flag, codeLocation) |
||||
|
suite.currentContainer.PushContainerNode(container) |
||||
|
|
||||
|
previousContainer := suite.currentContainer |
||||
|
suite.currentContainer = container |
||||
|
suite.containerIndex++ |
||||
|
|
||||
|
body() |
||||
|
|
||||
|
suite.containerIndex-- |
||||
|
suite.currentContainer = previousContainer |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call It from within a Describe, Context or When", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSubjectNode(leafnodes.NewItNode(text, body, flag, codeLocation, timeout, suite.failer, suite.containerIndex)) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call Measure from within a Describe, Context or When", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSubjectNode(leafnodes.NewMeasureNode(text, body, flag, codeLocation, samples, suite.failer, suite.containerIndex)) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call BeforeEach from within a Describe, Context or When", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSetupNode(leafnodes.NewBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call JustBeforeEach from within a Describe, Context or When", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSetupNode(leafnodes.NewJustBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushJustAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call JustAfterEach from within a Describe or Context", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSetupNode(leafnodes.NewJustAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) |
||||
|
} |
||||
|
|
||||
|
func (suite *Suite) PushAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { |
||||
|
if suite.running { |
||||
|
suite.failer.Fail("You may only call AfterEach from within a Describe, Context or When", codeLocation) |
||||
|
} |
||||
|
suite.currentContainer.PushSetupNode(leafnodes.NewAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) |
||||
|
} |
@ -0,0 +1,104 @@ |
|||||
|
package testingtproxy |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"io" |
||||
|
) |
||||
|
|
||||
|
type failFunc func(message string, callerSkip ...int) |
||||
|
type skipFunc func(message string, callerSkip ...int) |
||||
|
type failedFunc func() bool |
||||
|
type nameFunc func() string |
||||
|
|
||||
|
func New(writer io.Writer, fail failFunc, skip skipFunc, failed failedFunc, name nameFunc, offset int) *ginkgoTestingTProxy { |
||||
|
return &ginkgoTestingTProxy{ |
||||
|
fail: fail, |
||||
|
offset: offset, |
||||
|
writer: writer, |
||||
|
skip: skip, |
||||
|
failed: failed, |
||||
|
name: name, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type ginkgoTestingTProxy struct { |
||||
|
fail failFunc |
||||
|
skip skipFunc |
||||
|
failed failedFunc |
||||
|
name nameFunc |
||||
|
offset int |
||||
|
writer io.Writer |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Cleanup(func()) { |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Error(args ...interface{}) { |
||||
|
t.fail(fmt.Sprintln(args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Errorf(format string, args ...interface{}) { |
||||
|
t.fail(fmt.Sprintf(format, args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Fail() { |
||||
|
t.fail("failed", t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) FailNow() { |
||||
|
t.fail("failed", t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Failed() bool { |
||||
|
return t.failed() |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Fatal(args ...interface{}) { |
||||
|
t.fail(fmt.Sprintln(args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Fatalf(format string, args ...interface{}) { |
||||
|
t.fail(fmt.Sprintf(format, args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Helper() { |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Log(args ...interface{}) { |
||||
|
fmt.Fprintln(t.writer, args...) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Logf(format string, args ...interface{}) { |
||||
|
t.Log(fmt.Sprintf(format, args...)) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Name() string { |
||||
|
return t.name() |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Parallel() { |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Skip(args ...interface{}) { |
||||
|
t.skip(fmt.Sprintln(args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) SkipNow() { |
||||
|
t.skip("skip", t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Skipf(format string, args ...interface{}) { |
||||
|
t.skip(fmt.Sprintf(format, args...), t.offset) |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) Skipped() bool { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (t *ginkgoTestingTProxy) TempDir() string { |
||||
|
// No-op
|
||||
|
return "" |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
package writer |
||||
|
|
||||
|
type FakeGinkgoWriter struct { |
||||
|
EventStream []string |
||||
|
} |
||||
|
|
||||
|
func NewFake() *FakeGinkgoWriter { |
||||
|
return &FakeGinkgoWriter{ |
||||
|
EventStream: []string{}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) AddEvent(event string) { |
||||
|
writer.EventStream = append(writer.EventStream, event) |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) Truncate() { |
||||
|
writer.EventStream = append(writer.EventStream, "TRUNCATE") |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) DumpOut() { |
||||
|
writer.EventStream = append(writer.EventStream, "DUMP") |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) DumpOutWithHeader(header string) { |
||||
|
writer.EventStream = append(writer.EventStream, "DUMP_WITH_HEADER: "+header) |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) Bytes() []byte { |
||||
|
writer.EventStream = append(writer.EventStream, "BYTES") |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (writer *FakeGinkgoWriter) Write(data []byte) (n int, err error) { |
||||
|
return 0, nil |
||||
|
} |
@ -0,0 +1,89 @@ |
|||||
|
package writer |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"io" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
type WriterInterface interface { |
||||
|
io.Writer |
||||
|
|
||||
|
Truncate() |
||||
|
DumpOut() |
||||
|
DumpOutWithHeader(header string) |
||||
|
Bytes() []byte |
||||
|
} |
||||
|
|
||||
|
type Writer struct { |
||||
|
buffer *bytes.Buffer |
||||
|
outWriter io.Writer |
||||
|
lock *sync.Mutex |
||||
|
stream bool |
||||
|
redirector io.Writer |
||||
|
} |
||||
|
|
||||
|
func New(outWriter io.Writer) *Writer { |
||||
|
return &Writer{ |
||||
|
buffer: &bytes.Buffer{}, |
||||
|
lock: &sync.Mutex{}, |
||||
|
outWriter: outWriter, |
||||
|
stream: true, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) AndRedirectTo(writer io.Writer) { |
||||
|
w.redirector = writer |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) SetStream(stream bool) { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
w.stream = stream |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) Write(b []byte) (n int, err error) { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
|
||||
|
n, err = w.buffer.Write(b) |
||||
|
if w.redirector != nil { |
||||
|
w.redirector.Write(b) |
||||
|
} |
||||
|
if w.stream { |
||||
|
return w.outWriter.Write(b) |
||||
|
} |
||||
|
return n, err |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) Truncate() { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
w.buffer.Reset() |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) DumpOut() { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
if !w.stream { |
||||
|
w.buffer.WriteTo(w.outWriter) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) Bytes() []byte { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
b := w.buffer.Bytes() |
||||
|
copied := make([]byte, len(b)) |
||||
|
copy(copied, b) |
||||
|
return copied |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) DumpOutWithHeader(header string) { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
if !w.stream && w.buffer.Len() > 0 { |
||||
|
w.outWriter.Write([]byte(header)) |
||||
|
w.buffer.WriteTo(w.outWriter) |
||||
|
} |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
/* |
||||
|
Ginkgo's Default Reporter |
||||
|
|
||||
|
A number of command line flags are available to tweak Ginkgo's default output. |
||||
|
|
||||
|
These are documented [here](http://onsi.github.io/ginkgo/#running_tests)
|
||||
|
*/ |
||||
|
package reporters |
||||
|
|
||||
|
import ( |
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/reporters/stenographer" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type DefaultReporter struct { |
||||
|
config config.DefaultReporterConfigType |
||||
|
stenographer stenographer.Stenographer |
||||
|
specSummaries []*types.SpecSummary |
||||
|
} |
||||
|
|
||||
|
func NewDefaultReporter(config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *DefaultReporter { |
||||
|
return &DefaultReporter{ |
||||
|
config: config, |
||||
|
stenographer: stenographer, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { |
||||
|
reporter.stenographer.AnnounceSuite(summary.SuiteDescription, config.RandomSeed, config.RandomizeAllSpecs, reporter.config.Succinct) |
||||
|
if config.ParallelTotal > 1 { |
||||
|
reporter.stenographer.AnnounceParallelRun(config.ParallelNode, config.ParallelTotal, reporter.config.Succinct) |
||||
|
} else { |
||||
|
reporter.stenographer.AnnounceNumberOfSpecs(summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, reporter.config.Succinct) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
if setupSummary.State != types.SpecStatePassed { |
||||
|
reporter.stenographer.AnnounceBeforeSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
if setupSummary.State != types.SpecStatePassed { |
||||
|
reporter.stenographer.AnnounceAfterSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) SpecWillRun(specSummary *types.SpecSummary) { |
||||
|
if reporter.config.Verbose && !reporter.config.Succinct && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { |
||||
|
reporter.stenographer.AnnounceSpecWillRun(specSummary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) SpecDidComplete(specSummary *types.SpecSummary) { |
||||
|
switch specSummary.State { |
||||
|
case types.SpecStatePassed: |
||||
|
if specSummary.IsMeasurement { |
||||
|
reporter.stenographer.AnnounceSuccessfulMeasurement(specSummary, reporter.config.Succinct) |
||||
|
} else if specSummary.RunTime.Seconds() >= reporter.config.SlowSpecThreshold { |
||||
|
reporter.stenographer.AnnounceSuccessfulSlowSpec(specSummary, reporter.config.Succinct) |
||||
|
} else { |
||||
|
reporter.stenographer.AnnounceSuccessfulSpec(specSummary) |
||||
|
if reporter.config.ReportPassed { |
||||
|
reporter.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput) |
||||
|
} |
||||
|
} |
||||
|
case types.SpecStatePending: |
||||
|
reporter.stenographer.AnnouncePendingSpec(specSummary, reporter.config.NoisyPendings && !reporter.config.Succinct) |
||||
|
case types.SpecStateSkipped: |
||||
|
reporter.stenographer.AnnounceSkippedSpec(specSummary, reporter.config.Succinct || !reporter.config.NoisySkippings, reporter.config.FullTrace) |
||||
|
case types.SpecStateTimedOut: |
||||
|
reporter.stenographer.AnnounceSpecTimedOut(specSummary, reporter.config.Succinct, reporter.config.FullTrace) |
||||
|
case types.SpecStatePanicked: |
||||
|
reporter.stenographer.AnnounceSpecPanicked(specSummary, reporter.config.Succinct, reporter.config.FullTrace) |
||||
|
case types.SpecStateFailed: |
||||
|
reporter.stenographer.AnnounceSpecFailed(specSummary, reporter.config.Succinct, reporter.config.FullTrace) |
||||
|
} |
||||
|
|
||||
|
reporter.specSummaries = append(reporter.specSummaries, specSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *DefaultReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { |
||||
|
reporter.stenographer.SummarizeFailures(reporter.specSummaries) |
||||
|
reporter.stenographer.AnnounceSpecRunCompletion(summary, reporter.config.Succinct) |
||||
|
} |
@ -0,0 +1,59 @@ |
|||||
|
package reporters |
||||
|
|
||||
|
import ( |
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
//FakeReporter is useful for testing purposes
|
||||
|
type FakeReporter struct { |
||||
|
Config config.GinkgoConfigType |
||||
|
|
||||
|
BeginSummary *types.SuiteSummary |
||||
|
BeforeSuiteSummary *types.SetupSummary |
||||
|
SpecWillRunSummaries []*types.SpecSummary |
||||
|
SpecSummaries []*types.SpecSummary |
||||
|
AfterSuiteSummary *types.SetupSummary |
||||
|
EndSummary *types.SuiteSummary |
||||
|
|
||||
|
SpecWillRunStub func(specSummary *types.SpecSummary) |
||||
|
SpecDidCompleteStub func(specSummary *types.SpecSummary) |
||||
|
} |
||||
|
|
||||
|
func NewFakeReporter() *FakeReporter { |
||||
|
return &FakeReporter{ |
||||
|
SpecWillRunSummaries: make([]*types.SpecSummary, 0), |
||||
|
SpecSummaries: make([]*types.SpecSummary, 0), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { |
||||
|
fakeR.Config = config |
||||
|
fakeR.BeginSummary = summary |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
fakeR.BeforeSuiteSummary = setupSummary |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) SpecWillRun(specSummary *types.SpecSummary) { |
||||
|
if fakeR.SpecWillRunStub != nil { |
||||
|
fakeR.SpecWillRunStub(specSummary) |
||||
|
} |
||||
|
fakeR.SpecWillRunSummaries = append(fakeR.SpecWillRunSummaries, specSummary) |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) SpecDidComplete(specSummary *types.SpecSummary) { |
||||
|
if fakeR.SpecDidCompleteStub != nil { |
||||
|
fakeR.SpecDidCompleteStub(specSummary) |
||||
|
} |
||||
|
fakeR.SpecSummaries = append(fakeR.SpecSummaries, specSummary) |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
fakeR.AfterSuiteSummary = setupSummary |
||||
|
} |
||||
|
|
||||
|
func (fakeR *FakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { |
||||
|
fakeR.EndSummary = summary |
||||
|
} |
@ -0,0 +1,178 @@ |
|||||
|
/* |
||||
|
|
||||
|
JUnit XML Reporter for Ginkgo |
||||
|
|
||||
|
For usage instructions: http://onsi.github.io/ginkgo/#generating_junit_xml_output
|
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
package reporters |
||||
|
|
||||
|
import ( |
||||
|
"encoding/xml" |
||||
|
"fmt" |
||||
|
"math" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type JUnitTestSuite struct { |
||||
|
XMLName xml.Name `xml:"testsuite"` |
||||
|
TestCases []JUnitTestCase `xml:"testcase"` |
||||
|
Name string `xml:"name,attr"` |
||||
|
Tests int `xml:"tests,attr"` |
||||
|
Failures int `xml:"failures,attr"` |
||||
|
Errors int `xml:"errors,attr"` |
||||
|
Time float64 `xml:"time,attr"` |
||||
|
} |
||||
|
|
||||
|
type JUnitTestCase struct { |
||||
|
Name string `xml:"name,attr"` |
||||
|
ClassName string `xml:"classname,attr"` |
||||
|
FailureMessage *JUnitFailureMessage `xml:"failure,omitempty"` |
||||
|
Skipped *JUnitSkipped `xml:"skipped,omitempty"` |
||||
|
Time float64 `xml:"time,attr"` |
||||
|
SystemOut string `xml:"system-out,omitempty"` |
||||
|
} |
||||
|
|
||||
|
type JUnitFailureMessage struct { |
||||
|
Type string `xml:"type,attr"` |
||||
|
Message string `xml:",chardata"` |
||||
|
} |
||||
|
|
||||
|
type JUnitSkipped struct { |
||||
|
Message string `xml:",chardata"` |
||||
|
} |
||||
|
|
||||
|
type JUnitReporter struct { |
||||
|
suite JUnitTestSuite |
||||
|
filename string |
||||
|
testSuiteName string |
||||
|
ReporterConfig config.DefaultReporterConfigType |
||||
|
} |
||||
|
|
||||
|
//NewJUnitReporter creates a new JUnit XML reporter. The XML will be stored in the passed in filename.
|
||||
|
func NewJUnitReporter(filename string) *JUnitReporter { |
||||
|
return &JUnitReporter{ |
||||
|
filename: filename, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) SpecSuiteWillBegin(ginkgoConfig config.GinkgoConfigType, summary *types.SuiteSummary) { |
||||
|
reporter.suite = JUnitTestSuite{ |
||||
|
Name: summary.SuiteDescription, |
||||
|
TestCases: []JUnitTestCase{}, |
||||
|
} |
||||
|
reporter.testSuiteName = summary.SuiteDescription |
||||
|
reporter.ReporterConfig = config.DefaultReporterConfig |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) SpecWillRun(specSummary *types.SpecSummary) { |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
reporter.handleSetupSummary("BeforeSuite", setupSummary) |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { |
||||
|
reporter.handleSetupSummary("AfterSuite", setupSummary) |
||||
|
} |
||||
|
|
||||
|
func failureMessage(failure types.SpecFailure) string { |
||||
|
return fmt.Sprintf("%s\n%s\n%s", failure.ComponentCodeLocation.String(), failure.Message, failure.Location.String()) |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) { |
||||
|
if setupSummary.State != types.SpecStatePassed { |
||||
|
testCase := JUnitTestCase{ |
||||
|
Name: name, |
||||
|
ClassName: reporter.testSuiteName, |
||||
|
} |
||||
|
|
||||
|
testCase.FailureMessage = &JUnitFailureMessage{ |
||||
|
Type: reporter.failureTypeForState(setupSummary.State), |
||||
|
Message: failureMessage(setupSummary.Failure), |
||||
|
} |
||||
|
testCase.SystemOut = setupSummary.CapturedOutput |
||||
|
testCase.Time = setupSummary.RunTime.Seconds() |
||||
|
reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) SpecDidComplete(specSummary *types.SpecSummary) { |
||||
|
testCase := JUnitTestCase{ |
||||
|
Name: strings.Join(specSummary.ComponentTexts[1:], " "), |
||||
|
ClassName: reporter.testSuiteName, |
||||
|
} |
||||
|
if reporter.ReporterConfig.ReportPassed && specSummary.State == types.SpecStatePassed { |
||||
|
testCase.SystemOut = specSummary.CapturedOutput |
||||
|
} |
||||
|
if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked { |
||||
|
testCase.FailureMessage = &JUnitFailureMessage{ |
||||
|
Type: reporter.failureTypeForState(specSummary.State), |
||||
|
Message: failureMessage(specSummary.Failure), |
||||
|
} |
||||
|
if specSummary.State == types.SpecStatePanicked { |
||||
|
testCase.FailureMessage.Message += fmt.Sprintf("\n\nPanic: %s\n\nFull stack:\n%s", |
||||
|
specSummary.Failure.ForwardedPanic, |
||||
|
specSummary.Failure.Location.FullStackTrace) |
||||
|
} |
||||
|
testCase.SystemOut = specSummary.CapturedOutput |
||||
|
} |
||||
|
if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending { |
||||
|
testCase.Skipped = &JUnitSkipped{} |
||||
|
if specSummary.Failure.Message != "" { |
||||
|
testCase.Skipped.Message = failureMessage(specSummary.Failure) |
||||
|
} |
||||
|
} |
||||
|
testCase.Time = specSummary.RunTime.Seconds() |
||||
|
reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { |
||||
|
reporter.suite.Tests = summary.NumberOfSpecsThatWillBeRun |
||||
|
reporter.suite.Time = math.Trunc(summary.RunTime.Seconds()*1000) / 1000 |
||||
|
reporter.suite.Failures = summary.NumberOfFailedSpecs |
||||
|
reporter.suite.Errors = 0 |
||||
|
if reporter.ReporterConfig.ReportFile != "" { |
||||
|
reporter.filename = reporter.ReporterConfig.ReportFile |
||||
|
fmt.Printf("\nJUnit path was configured: %s\n", reporter.filename) |
||||
|
} |
||||
|
filePath, _ := filepath.Abs(reporter.filename) |
||||
|
dirPath := filepath.Dir(filePath) |
||||
|
err := os.MkdirAll(dirPath, os.ModePerm) |
||||
|
if err != nil { |
||||
|
fmt.Printf("\nFailed to create JUnit directory: %s\n\t%s", filePath, err.Error()) |
||||
|
} |
||||
|
file, err := os.Create(filePath) |
||||
|
if err != nil { |
||||
|
fmt.Fprintf(os.Stderr, "Failed to create JUnit report file: %s\n\t%s", filePath, err.Error()) |
||||
|
} |
||||
|
defer file.Close() |
||||
|
file.WriteString(xml.Header) |
||||
|
encoder := xml.NewEncoder(file) |
||||
|
encoder.Indent(" ", " ") |
||||
|
err = encoder.Encode(reporter.suite) |
||||
|
if err == nil { |
||||
|
fmt.Fprintf(os.Stdout, "\nJUnit report was created: %s\n", filePath) |
||||
|
} else { |
||||
|
fmt.Fprintf(os.Stderr,"\nFailed to generate JUnit report data:\n\t%s", err.Error()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (reporter *JUnitReporter) failureTypeForState(state types.SpecState) string { |
||||
|
switch state { |
||||
|
case types.SpecStateFailed: |
||||
|
return "Failure" |
||||
|
case types.SpecStateTimedOut: |
||||
|
return "Timeout" |
||||
|
case types.SpecStatePanicked: |
||||
|
return "Panic" |
||||
|
default: |
||||
|
return "" |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package reporters |
||||
|
|
||||
|
import ( |
||||
|
"github.com/onsi/ginkgo/config" |
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
type Reporter interface { |
||||
|
SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) |
||||
|
BeforeSuiteDidRun(setupSummary *types.SetupSummary) |
||||
|
SpecWillRun(specSummary *types.SpecSummary) |
||||
|
SpecDidComplete(specSummary *types.SpecSummary) |
||||
|
AfterSuiteDidRun(setupSummary *types.SetupSummary) |
||||
|
SpecSuiteDidEnd(summary *types.SuiteSummary) |
||||
|
} |
@ -0,0 +1,64 @@ |
|||||
|
package stenographer |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
func (s *consoleStenographer) colorize(colorCode string, format string, args ...interface{}) string { |
||||
|
var out string |
||||
|
|
||||
|
if len(args) > 0 { |
||||
|
out = fmt.Sprintf(format, args...) |
||||
|
} else { |
||||
|
out = format |
||||
|
} |
||||
|
|
||||
|
if s.color { |
||||
|
return fmt.Sprintf("%s%s%s", colorCode, out, defaultStyle) |
||||
|
} else { |
||||
|
return out |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printBanner(text string, bannerCharacter string) { |
||||
|
fmt.Fprintln(s.w, text) |
||||
|
fmt.Fprintln(s.w, strings.Repeat(bannerCharacter, len(text))) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printNewLine() { |
||||
|
fmt.Fprintln(s.w, "") |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printDelimiter() { |
||||
|
fmt.Fprintln(s.w, s.colorize(grayColor, "%s", strings.Repeat("-", 30))) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) print(indentation int, format string, args ...interface{}) { |
||||
|
fmt.Fprint(s.w, s.indent(indentation, format, args...)) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) println(indentation int, format string, args ...interface{}) { |
||||
|
fmt.Fprintln(s.w, s.indent(indentation, format, args...)) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) indent(indentation int, format string, args ...interface{}) string { |
||||
|
var text string |
||||
|
|
||||
|
if len(args) > 0 { |
||||
|
text = fmt.Sprintf(format, args...) |
||||
|
} else { |
||||
|
text = format |
||||
|
} |
||||
|
|
||||
|
stringArray := strings.Split(text, "\n") |
||||
|
padding := "" |
||||
|
if indentation >= 0 { |
||||
|
padding = strings.Repeat(" ", indentation) |
||||
|
} |
||||
|
for i, s := range stringArray { |
||||
|
stringArray[i] = fmt.Sprintf("%s%s", padding, s) |
||||
|
} |
||||
|
|
||||
|
return strings.Join(stringArray, "\n") |
||||
|
} |
@ -0,0 +1,142 @@ |
|||||
|
package stenographer |
||||
|
|
||||
|
import ( |
||||
|
"sync" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
func NewFakeStenographerCall(method string, args ...interface{}) FakeStenographerCall { |
||||
|
return FakeStenographerCall{ |
||||
|
Method: method, |
||||
|
Args: args, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type FakeStenographer struct { |
||||
|
calls []FakeStenographerCall |
||||
|
lock *sync.Mutex |
||||
|
} |
||||
|
|
||||
|
type FakeStenographerCall struct { |
||||
|
Method string |
||||
|
Args []interface{} |
||||
|
} |
||||
|
|
||||
|
func NewFakeStenographer() *FakeStenographer { |
||||
|
stenographer := &FakeStenographer{ |
||||
|
lock: &sync.Mutex{}, |
||||
|
} |
||||
|
stenographer.Reset() |
||||
|
return stenographer |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) Calls() []FakeStenographerCall { |
||||
|
stenographer.lock.Lock() |
||||
|
defer stenographer.lock.Unlock() |
||||
|
|
||||
|
return stenographer.calls |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) Reset() { |
||||
|
stenographer.lock.Lock() |
||||
|
defer stenographer.lock.Unlock() |
||||
|
|
||||
|
stenographer.calls = make([]FakeStenographerCall, 0) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) CallsTo(method string) []FakeStenographerCall { |
||||
|
stenographer.lock.Lock() |
||||
|
defer stenographer.lock.Unlock() |
||||
|
|
||||
|
results := make([]FakeStenographerCall, 0) |
||||
|
for _, call := range stenographer.calls { |
||||
|
if call.Method == method { |
||||
|
results = append(results, call) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return results |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) registerCall(method string, args ...interface{}) { |
||||
|
stenographer.lock.Lock() |
||||
|
defer stenographer.lock.Unlock() |
||||
|
|
||||
|
stenographer.calls = append(stenographer.calls, NewFakeStenographerCall(method, args...)) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceSuite", description, randomSeed, randomizingAll, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceAggregatedParallelRun", nodes, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceParallelRun", node, nodes, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceNumberOfSpecs", specsToRun, total, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceTotalNumberOfSpecs", total, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceSpecRunCompletion", summary, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { |
||||
|
stenographer.registerCall("AnnounceSpecWillRun", spec) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceBeforeSuiteFailure", summary, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceAfterSuiteFailure", summary, succinct, fullTrace) |
||||
|
} |
||||
|
func (stenographer *FakeStenographer) AnnounceCapturedOutput(output string) { |
||||
|
stenographer.registerCall("AnnounceCapturedOutput", output) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSuccessfulSpec(spec *types.SpecSummary) { |
||||
|
stenographer.registerCall("AnnounceSuccessfulSpec", spec) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceSuccessfulSlowSpec", spec, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) { |
||||
|
stenographer.registerCall("AnnounceSuccessfulMeasurement", spec, succinct) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { |
||||
|
stenographer.registerCall("AnnouncePendingSpec", spec, noisy) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceSkippedSpec", spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceSpecTimedOut", spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceSpecPanicked", spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
stenographer.registerCall("AnnounceSpecFailed", spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (stenographer *FakeStenographer) SummarizeFailures(summaries []*types.SpecSummary) { |
||||
|
stenographer.registerCall("SummarizeFailures", summaries) |
||||
|
} |
@ -0,0 +1,572 @@ |
|||||
|
/* |
||||
|
The stenographer is used by Ginkgo's reporters to generate output. |
||||
|
|
||||
|
Move along, nothing to see here. |
||||
|
*/ |
||||
|
|
||||
|
package stenographer |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"runtime" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/types" |
||||
|
) |
||||
|
|
||||
|
const defaultStyle = "\x1b[0m" |
||||
|
const boldStyle = "\x1b[1m" |
||||
|
const redColor = "\x1b[91m" |
||||
|
const greenColor = "\x1b[32m" |
||||
|
const yellowColor = "\x1b[33m" |
||||
|
const cyanColor = "\x1b[36m" |
||||
|
const grayColor = "\x1b[90m" |
||||
|
const lightGrayColor = "\x1b[37m" |
||||
|
|
||||
|
type cursorStateType int |
||||
|
|
||||
|
const ( |
||||
|
cursorStateTop cursorStateType = iota |
||||
|
cursorStateStreaming |
||||
|
cursorStateMidBlock |
||||
|
cursorStateEndBlock |
||||
|
) |
||||
|
|
||||
|
type Stenographer interface { |
||||
|
AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) |
||||
|
AnnounceAggregatedParallelRun(nodes int, succinct bool) |
||||
|
AnnounceParallelRun(node int, nodes int, succinct bool) |
||||
|
AnnounceTotalNumberOfSpecs(total int, succinct bool) |
||||
|
AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) |
||||
|
AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) |
||||
|
|
||||
|
AnnounceSpecWillRun(spec *types.SpecSummary) |
||||
|
AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) |
||||
|
AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) |
||||
|
|
||||
|
AnnounceCapturedOutput(output string) |
||||
|
|
||||
|
AnnounceSuccessfulSpec(spec *types.SpecSummary) |
||||
|
AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) |
||||
|
AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) |
||||
|
|
||||
|
AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) |
||||
|
AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) |
||||
|
|
||||
|
AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) |
||||
|
AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) |
||||
|
AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) |
||||
|
|
||||
|
SummarizeFailures(summaries []*types.SpecSummary) |
||||
|
} |
||||
|
|
||||
|
func New(color bool, enableFlakes bool, writer io.Writer) Stenographer { |
||||
|
denoter := "•" |
||||
|
if runtime.GOOS == "windows" { |
||||
|
denoter = "+" |
||||
|
} |
||||
|
return &consoleStenographer{ |
||||
|
color: color, |
||||
|
denoter: denoter, |
||||
|
cursorState: cursorStateTop, |
||||
|
enableFlakes: enableFlakes, |
||||
|
w: writer, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type consoleStenographer struct { |
||||
|
color bool |
||||
|
denoter string |
||||
|
cursorState cursorStateType |
||||
|
enableFlakes bool |
||||
|
w io.Writer |
||||
|
} |
||||
|
|
||||
|
var alternatingColors = []string{defaultStyle, grayColor} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { |
||||
|
if succinct { |
||||
|
s.print(0, "[%d] %s ", randomSeed, s.colorize(boldStyle, description)) |
||||
|
return |
||||
|
} |
||||
|
s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=") |
||||
|
s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed)) |
||||
|
if randomizingAll { |
||||
|
s.print(0, " - Will randomize all specs") |
||||
|
} |
||||
|
s.printNewLine() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) { |
||||
|
if succinct { |
||||
|
s.print(0, "- node #%d ", node) |
||||
|
return |
||||
|
} |
||||
|
s.println(0, |
||||
|
"Parallel test node %s/%s.", |
||||
|
s.colorize(boldStyle, "%d", node), |
||||
|
s.colorize(boldStyle, "%d", nodes), |
||||
|
) |
||||
|
s.printNewLine() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { |
||||
|
if succinct { |
||||
|
s.print(0, "- %d nodes ", nodes) |
||||
|
return |
||||
|
} |
||||
|
s.println(0, |
||||
|
"Running in parallel across %s nodes", |
||||
|
s.colorize(boldStyle, "%d", nodes), |
||||
|
) |
||||
|
s.printNewLine() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { |
||||
|
if succinct { |
||||
|
s.print(0, "- %d/%d specs ", specsToRun, total) |
||||
|
s.stream() |
||||
|
return |
||||
|
} |
||||
|
s.println(0, |
||||
|
"Will run %s of %s specs", |
||||
|
s.colorize(boldStyle, "%d", specsToRun), |
||||
|
s.colorize(boldStyle, "%d", total), |
||||
|
) |
||||
|
|
||||
|
s.printNewLine() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) { |
||||
|
if succinct { |
||||
|
s.print(0, "- %d specs ", total) |
||||
|
s.stream() |
||||
|
return |
||||
|
} |
||||
|
s.println(0, |
||||
|
"Will run %s specs", |
||||
|
s.colorize(boldStyle, "%d", total), |
||||
|
) |
||||
|
|
||||
|
s.printNewLine() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { |
||||
|
if succinct && summary.SuiteSucceeded { |
||||
|
s.print(0, " %s %s ", s.colorize(greenColor, "SUCCESS!"), summary.RunTime) |
||||
|
return |
||||
|
} |
||||
|
s.printNewLine() |
||||
|
color := greenColor |
||||
|
if !summary.SuiteSucceeded { |
||||
|
color = redColor |
||||
|
} |
||||
|
s.println(0, s.colorize(boldStyle+color, "Ran %d of %d Specs in %.3f seconds", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, summary.RunTime.Seconds())) |
||||
|
|
||||
|
status := "" |
||||
|
if summary.SuiteSucceeded { |
||||
|
status = s.colorize(boldStyle+greenColor, "SUCCESS!") |
||||
|
} else { |
||||
|
status = s.colorize(boldStyle+redColor, "FAIL!") |
||||
|
} |
||||
|
|
||||
|
flakes := "" |
||||
|
if s.enableFlakes { |
||||
|
flakes = " | " + s.colorize(yellowColor+boldStyle, "%d Flaked", summary.NumberOfFlakedSpecs) |
||||
|
} |
||||
|
|
||||
|
s.print(0, |
||||
|
"%s -- %s | %s | %s | %s\n", |
||||
|
status, |
||||
|
s.colorize(greenColor+boldStyle, "%d Passed", summary.NumberOfPassedSpecs), |
||||
|
s.colorize(redColor+boldStyle, "%d Failed", summary.NumberOfFailedSpecs)+flakes, |
||||
|
s.colorize(yellowColor+boldStyle, "%d Pending", summary.NumberOfPendingSpecs), |
||||
|
s.colorize(cyanColor+boldStyle, "%d Skipped", summary.NumberOfSkippedSpecs), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { |
||||
|
s.startBlock() |
||||
|
for i, text := range spec.ComponentTexts[1 : len(spec.ComponentTexts)-1] { |
||||
|
s.print(0, s.colorize(alternatingColors[i%2], text)+" ") |
||||
|
} |
||||
|
|
||||
|
indentation := 0 |
||||
|
if len(spec.ComponentTexts) > 2 { |
||||
|
indentation = 1 |
||||
|
s.printNewLine() |
||||
|
} |
||||
|
index := len(spec.ComponentTexts) - 1 |
||||
|
s.print(indentation, s.colorize(boldStyle, spec.ComponentTexts[index])) |
||||
|
s.printNewLine() |
||||
|
s.print(indentation, s.colorize(lightGrayColor, spec.ComponentCodeLocations[index].String())) |
||||
|
s.printNewLine() |
||||
|
s.midBlock() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { |
||||
|
s.announceSetupFailure("BeforeSuite", summary, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { |
||||
|
s.announceSetupFailure("AfterSuite", summary, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) announceSetupFailure(name string, summary *types.SetupSummary, succinct bool, fullTrace bool) { |
||||
|
s.startBlock() |
||||
|
var message string |
||||
|
switch summary.State { |
||||
|
case types.SpecStateFailed: |
||||
|
message = "Failure" |
||||
|
case types.SpecStatePanicked: |
||||
|
message = "Panic" |
||||
|
case types.SpecStateTimedOut: |
||||
|
message = "Timeout" |
||||
|
} |
||||
|
|
||||
|
s.println(0, s.colorize(redColor+boldStyle, "%s [%.3f seconds]", message, summary.RunTime.Seconds())) |
||||
|
|
||||
|
indentation := s.printCodeLocationBlock([]string{name}, []types.CodeLocation{summary.CodeLocation}, summary.ComponentType, 0, summary.State, true) |
||||
|
|
||||
|
s.printNewLine() |
||||
|
s.printFailure(indentation, summary.State, summary.Failure, fullTrace) |
||||
|
|
||||
|
s.endBlock() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceCapturedOutput(output string) { |
||||
|
if output == "" { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s.startBlock() |
||||
|
s.println(0, output) |
||||
|
s.midBlock() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSuccessfulSpec(spec *types.SpecSummary) { |
||||
|
s.print(0, s.colorize(greenColor, s.denoter)) |
||||
|
s.stream() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) { |
||||
|
s.printBlockWithMessage( |
||||
|
s.colorize(greenColor, "%s [SLOW TEST:%.3f seconds]", s.denoter, spec.RunTime.Seconds()), |
||||
|
"", |
||||
|
spec, |
||||
|
succinct, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) { |
||||
|
s.printBlockWithMessage( |
||||
|
s.colorize(greenColor, "%s [MEASUREMENT]", s.denoter), |
||||
|
s.measurementReport(spec, succinct), |
||||
|
spec, |
||||
|
succinct, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { |
||||
|
if noisy { |
||||
|
s.printBlockWithMessage( |
||||
|
s.colorize(yellowColor, "P [PENDING]"), |
||||
|
"", |
||||
|
spec, |
||||
|
false, |
||||
|
) |
||||
|
} else { |
||||
|
s.print(0, s.colorize(yellowColor, "P")) |
||||
|
s.stream() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
// Skips at runtime will have a non-empty spec.Failure. All others should be succinct.
|
||||
|
if succinct || spec.Failure == (types.SpecFailure{}) { |
||||
|
s.print(0, s.colorize(cyanColor, "S")) |
||||
|
s.stream() |
||||
|
} else { |
||||
|
s.startBlock() |
||||
|
s.println(0, s.colorize(cyanColor+boldStyle, "S [SKIPPING]%s [%.3f seconds]", s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) |
||||
|
|
||||
|
indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) |
||||
|
|
||||
|
s.printNewLine() |
||||
|
s.printSkip(indentation, spec.Failure) |
||||
|
s.endBlock() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
s.printSpecFailure(fmt.Sprintf("%s... Timeout", s.denoter), spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
s.printSpecFailure(fmt.Sprintf("%s! Panic", s.denoter), spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
s.printSpecFailure(fmt.Sprintf("%s Failure", s.denoter), spec, succinct, fullTrace) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) SummarizeFailures(summaries []*types.SpecSummary) { |
||||
|
failingSpecs := []*types.SpecSummary{} |
||||
|
|
||||
|
for _, summary := range summaries { |
||||
|
if summary.HasFailureState() { |
||||
|
failingSpecs = append(failingSpecs, summary) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if len(failingSpecs) == 0 { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s.printNewLine() |
||||
|
s.printNewLine() |
||||
|
plural := "s" |
||||
|
if len(failingSpecs) == 1 { |
||||
|
plural = "" |
||||
|
} |
||||
|
s.println(0, s.colorize(redColor+boldStyle, "Summarizing %d Failure%s:", len(failingSpecs), plural)) |
||||
|
for _, summary := range failingSpecs { |
||||
|
s.printNewLine() |
||||
|
if summary.HasFailureState() { |
||||
|
if summary.TimedOut() { |
||||
|
s.print(0, s.colorize(redColor+boldStyle, "[Timeout...] ")) |
||||
|
} else if summary.Panicked() { |
||||
|
s.print(0, s.colorize(redColor+boldStyle, "[Panic!] ")) |
||||
|
} else if summary.Failed() { |
||||
|
s.print(0, s.colorize(redColor+boldStyle, "[Fail] ")) |
||||
|
} |
||||
|
s.printSpecContext(summary.ComponentTexts, summary.ComponentCodeLocations, summary.Failure.ComponentType, summary.Failure.ComponentIndex, summary.State, true) |
||||
|
s.printNewLine() |
||||
|
s.println(0, s.colorize(lightGrayColor, summary.Failure.Location.String())) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) startBlock() { |
||||
|
if s.cursorState == cursorStateStreaming { |
||||
|
s.printNewLine() |
||||
|
s.printDelimiter() |
||||
|
} else if s.cursorState == cursorStateMidBlock { |
||||
|
s.printNewLine() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) midBlock() { |
||||
|
s.cursorState = cursorStateMidBlock |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) endBlock() { |
||||
|
s.printDelimiter() |
||||
|
s.cursorState = cursorStateEndBlock |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) stream() { |
||||
|
s.cursorState = cursorStateStreaming |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printBlockWithMessage(header string, message string, spec *types.SpecSummary, succinct bool) { |
||||
|
s.startBlock() |
||||
|
s.println(0, header) |
||||
|
|
||||
|
indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, types.SpecComponentTypeInvalid, 0, spec.State, succinct) |
||||
|
|
||||
|
if message != "" { |
||||
|
s.printNewLine() |
||||
|
s.println(indentation, message) |
||||
|
} |
||||
|
|
||||
|
s.endBlock() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printSpecFailure(message string, spec *types.SpecSummary, succinct bool, fullTrace bool) { |
||||
|
s.startBlock() |
||||
|
s.println(0, s.colorize(redColor+boldStyle, "%s%s [%.3f seconds]", message, s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) |
||||
|
|
||||
|
indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) |
||||
|
|
||||
|
s.printNewLine() |
||||
|
s.printFailure(indentation, spec.State, spec.Failure, fullTrace) |
||||
|
s.endBlock() |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) failureContext(failedComponentType types.SpecComponentType) string { |
||||
|
switch failedComponentType { |
||||
|
case types.SpecComponentTypeBeforeSuite: |
||||
|
return " in Suite Setup (BeforeSuite)" |
||||
|
case types.SpecComponentTypeAfterSuite: |
||||
|
return " in Suite Teardown (AfterSuite)" |
||||
|
case types.SpecComponentTypeBeforeEach: |
||||
|
return " in Spec Setup (BeforeEach)" |
||||
|
case types.SpecComponentTypeJustBeforeEach: |
||||
|
return " in Spec Setup (JustBeforeEach)" |
||||
|
case types.SpecComponentTypeAfterEach: |
||||
|
return " in Spec Teardown (AfterEach)" |
||||
|
} |
||||
|
|
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printSkip(indentation int, spec types.SpecFailure) { |
||||
|
s.println(indentation, s.colorize(cyanColor, spec.Message)) |
||||
|
s.printNewLine() |
||||
|
s.println(indentation, spec.Location.String()) |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printFailure(indentation int, state types.SpecState, failure types.SpecFailure, fullTrace bool) { |
||||
|
if state == types.SpecStatePanicked { |
||||
|
s.println(indentation, s.colorize(redColor+boldStyle, failure.Message)) |
||||
|
s.println(indentation, s.colorize(redColor, failure.ForwardedPanic)) |
||||
|
s.println(indentation, failure.Location.String()) |
||||
|
s.printNewLine() |
||||
|
s.println(indentation, s.colorize(redColor, "Full Stack Trace")) |
||||
|
s.println(indentation, failure.Location.FullStackTrace) |
||||
|
} else { |
||||
|
s.println(indentation, s.colorize(redColor, failure.Message)) |
||||
|
s.printNewLine() |
||||
|
s.println(indentation, failure.Location.String()) |
||||
|
if fullTrace { |
||||
|
s.printNewLine() |
||||
|
s.println(indentation, s.colorize(redColor, "Full Stack Trace")) |
||||
|
s.println(indentation, failure.Location.FullStackTrace) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printSpecContext(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { |
||||
|
startIndex := 1 |
||||
|
indentation := 0 |
||||
|
|
||||
|
if len(componentTexts) == 1 { |
||||
|
startIndex = 0 |
||||
|
} |
||||
|
|
||||
|
for i := startIndex; i < len(componentTexts); i++ { |
||||
|
if (state.IsFailure() || state == types.SpecStateSkipped) && i == failedComponentIndex { |
||||
|
color := redColor |
||||
|
if state == types.SpecStateSkipped { |
||||
|
color = cyanColor |
||||
|
} |
||||
|
blockType := "" |
||||
|
switch failedComponentType { |
||||
|
case types.SpecComponentTypeBeforeSuite: |
||||
|
blockType = "BeforeSuite" |
||||
|
case types.SpecComponentTypeAfterSuite: |
||||
|
blockType = "AfterSuite" |
||||
|
case types.SpecComponentTypeBeforeEach: |
||||
|
blockType = "BeforeEach" |
||||
|
case types.SpecComponentTypeJustBeforeEach: |
||||
|
blockType = "JustBeforeEach" |
||||
|
case types.SpecComponentTypeAfterEach: |
||||
|
blockType = "AfterEach" |
||||
|
case types.SpecComponentTypeIt: |
||||
|
blockType = "It" |
||||
|
case types.SpecComponentTypeMeasure: |
||||
|
blockType = "Measurement" |
||||
|
} |
||||
|
if succinct { |
||||
|
s.print(0, s.colorize(color+boldStyle, "[%s] %s ", blockType, componentTexts[i])) |
||||
|
} else { |
||||
|
s.println(indentation, s.colorize(color+boldStyle, "%s [%s]", componentTexts[i], blockType)) |
||||
|
s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) |
||||
|
} |
||||
|
} else { |
||||
|
if succinct { |
||||
|
s.print(0, s.colorize(alternatingColors[i%2], "%s ", componentTexts[i])) |
||||
|
} else { |
||||
|
s.println(indentation, componentTexts[i]) |
||||
|
s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) |
||||
|
} |
||||
|
} |
||||
|
indentation++ |
||||
|
} |
||||
|
|
||||
|
return indentation |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) printCodeLocationBlock(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { |
||||
|
indentation := s.printSpecContext(componentTexts, componentCodeLocations, failedComponentType, failedComponentIndex, state, succinct) |
||||
|
|
||||
|
if succinct { |
||||
|
if len(componentTexts) > 0 { |
||||
|
s.printNewLine() |
||||
|
s.print(0, s.colorize(lightGrayColor, "%s", componentCodeLocations[len(componentCodeLocations)-1])) |
||||
|
} |
||||
|
s.printNewLine() |
||||
|
indentation = 1 |
||||
|
} else { |
||||
|
indentation-- |
||||
|
} |
||||
|
|
||||
|
return indentation |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) orderedMeasurementKeys(measurements map[string]*types.SpecMeasurement) []string { |
||||
|
orderedKeys := make([]string, len(measurements)) |
||||
|
for key, measurement := range measurements { |
||||
|
orderedKeys[measurement.Order] = key |
||||
|
} |
||||
|
return orderedKeys |
||||
|
} |
||||
|
|
||||
|
func (s *consoleStenographer) measurementReport(spec *types.SpecSummary, succinct bool) string { |
||||
|
if len(spec.Measurements) == 0 { |
||||
|
return "Found no measurements" |
||||
|
} |
||||
|
|
||||
|
message := []string{} |
||||
|
orderedKeys := s.orderedMeasurementKeys(spec.Measurements) |
||||
|
|
||||
|
if succinct { |
||||
|
message = append(message, fmt.Sprintf("%s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) |
||||
|
for _, key := range orderedKeys { |
||||
|
measurement := spec.Measurements[key] |
||||
|
message = append(message, fmt.Sprintf(" %s - %s: %s%s, %s: %s%s ± %s%s, %s: %s%s", |
||||
|
s.colorize(boldStyle, "%s", measurement.Name), |
||||
|
measurement.SmallestLabel, |
||||
|
s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), |
||||
|
measurement.Units, |
||||
|
measurement.AverageLabel, |
||||
|
s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), |
||||
|
measurement.Units, |
||||
|
s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), |
||||
|
measurement.Units, |
||||
|
measurement.LargestLabel, |
||||
|
s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), |
||||
|
measurement.Units, |
||||
|
)) |
||||
|
} |
||||
|
} else { |
||||
|
message = append(message, fmt.Sprintf("Ran %s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) |
||||
|
for _, key := range orderedKeys { |
||||
|
measurement := spec.Measurements[key] |
||||
|
info := "" |
||||
|
if measurement.Info != nil { |
||||
|
message = append(message, fmt.Sprintf("%v", measurement.Info)) |
||||
|
} |
||||
|
|
||||
|
message = append(message, fmt.Sprintf("%s:\n%s %s: %s%s\n %s: %s%s\n %s: %s%s ± %s%s", |
||||
|
s.colorize(boldStyle, "%s", measurement.Name), |
||||
|
info, |
||||
|
measurement.SmallestLabel, |
||||
|
s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), |
||||
|
measurement.Units, |
||||
|
measurement.LargestLabel, |
||||
|
s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), |
||||
|
measurement.Units, |
||||
|
measurement.AverageLabel, |
||||
|
s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), |
||||
|
measurement.Units, |
||||
|
s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), |
||||
|
measurement.Units, |
||||
|
)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return strings.Join(message, "\n") |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2016 Yasuhiro Matsumoto |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
@ -0,0 +1,43 @@ |
|||||
|
# go-colorable |
||||
|
|
||||
|
Colorable writer for windows. |
||||
|
|
||||
|
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.) |
||||
|
This package is possible to handle escape sequence for ansi color on windows. |
||||
|
|
||||
|
## Too Bad! |
||||
|
|
||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png) |
||||
|
|
||||
|
|
||||
|
## So Good! |
||||
|
|
||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```go |
||||
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) |
||||
|
logrus.SetOutput(colorable.NewColorableStdout()) |
||||
|
|
||||
|
logrus.Info("succeeded") |
||||
|
logrus.Warn("not correct") |
||||
|
logrus.Error("something error") |
||||
|
logrus.Fatal("panic") |
||||
|
``` |
||||
|
|
||||
|
You can compile above code on non-windows OSs. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
``` |
||||
|
$ go get github.com/mattn/go-colorable |
||||
|
``` |
||||
|
|
||||
|
# License |
||||
|
|
||||
|
MIT |
||||
|
|
||||
|
# Author |
||||
|
|
||||
|
Yasuhiro Matsumoto (a.k.a mattn) |
@ -0,0 +1,24 @@ |
|||||
|
// +build !windows
|
||||
|
|
||||
|
package colorable |
||||
|
|
||||
|
import ( |
||||
|
"io" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func NewColorable(file *os.File) io.Writer { |
||||
|
if file == nil { |
||||
|
panic("nil passed instead of *os.File to NewColorable()") |
||||
|
} |
||||
|
|
||||
|
return file |
||||
|
} |
||||
|
|
||||
|
func NewColorableStdout() io.Writer { |
||||
|
return os.Stdout |
||||
|
} |
||||
|
|
||||
|
func NewColorableStderr() io.Writer { |
||||
|
return os.Stderr |
||||
|
} |
@ -0,0 +1,783 @@ |
|||||
|
package colorable |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"math" |
||||
|
"os" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
|
||||
|
"github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
foregroundBlue = 0x1 |
||||
|
foregroundGreen = 0x2 |
||||
|
foregroundRed = 0x4 |
||||
|
foregroundIntensity = 0x8 |
||||
|
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) |
||||
|
backgroundBlue = 0x10 |
||||
|
backgroundGreen = 0x20 |
||||
|
backgroundRed = 0x40 |
||||
|
backgroundIntensity = 0x80 |
||||
|
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) |
||||
|
) |
||||
|
|
||||
|
type wchar uint16 |
||||
|
type short int16 |
||||
|
type dword uint32 |
||||
|
type word uint16 |
||||
|
|
||||
|
type coord struct { |
||||
|
x short |
||||
|
y short |
||||
|
} |
||||
|
|
||||
|
type smallRect struct { |
||||
|
left short |
||||
|
top short |
||||
|
right short |
||||
|
bottom short |
||||
|
} |
||||
|
|
||||
|
type consoleScreenBufferInfo struct { |
||||
|
size coord |
||||
|
cursorPosition coord |
||||
|
attributes word |
||||
|
window smallRect |
||||
|
maximumWindowSize coord |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll") |
||||
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") |
||||
|
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") |
||||
|
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") |
||||
|
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") |
||||
|
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") |
||||
|
) |
||||
|
|
||||
|
type Writer struct { |
||||
|
out io.Writer |
||||
|
handle syscall.Handle |
||||
|
lastbuf bytes.Buffer |
||||
|
oldattr word |
||||
|
} |
||||
|
|
||||
|
func NewColorable(file *os.File) io.Writer { |
||||
|
if file == nil { |
||||
|
panic("nil passed instead of *os.File to NewColorable()") |
||||
|
} |
||||
|
|
||||
|
if isatty.IsTerminal(file.Fd()) { |
||||
|
var csbi consoleScreenBufferInfo |
||||
|
handle := syscall.Handle(file.Fd()) |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
return &Writer{out: file, handle: handle, oldattr: csbi.attributes} |
||||
|
} else { |
||||
|
return file |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func NewColorableStdout() io.Writer { |
||||
|
return NewColorable(os.Stdout) |
||||
|
} |
||||
|
|
||||
|
func NewColorableStderr() io.Writer { |
||||
|
return NewColorable(os.Stderr) |
||||
|
} |
||||
|
|
||||
|
var color256 = map[int]int{ |
||||
|
0: 0x000000, |
||||
|
1: 0x800000, |
||||
|
2: 0x008000, |
||||
|
3: 0x808000, |
||||
|
4: 0x000080, |
||||
|
5: 0x800080, |
||||
|
6: 0x008080, |
||||
|
7: 0xc0c0c0, |
||||
|
8: 0x808080, |
||||
|
9: 0xff0000, |
||||
|
10: 0x00ff00, |
||||
|
11: 0xffff00, |
||||
|
12: 0x0000ff, |
||||
|
13: 0xff00ff, |
||||
|
14: 0x00ffff, |
||||
|
15: 0xffffff, |
||||
|
16: 0x000000, |
||||
|
17: 0x00005f, |
||||
|
18: 0x000087, |
||||
|
19: 0x0000af, |
||||
|
20: 0x0000d7, |
||||
|
21: 0x0000ff, |
||||
|
22: 0x005f00, |
||||
|
23: 0x005f5f, |
||||
|
24: 0x005f87, |
||||
|
25: 0x005faf, |
||||
|
26: 0x005fd7, |
||||
|
27: 0x005fff, |
||||
|
28: 0x008700, |
||||
|
29: 0x00875f, |
||||
|
30: 0x008787, |
||||
|
31: 0x0087af, |
||||
|
32: 0x0087d7, |
||||
|
33: 0x0087ff, |
||||
|
34: 0x00af00, |
||||
|
35: 0x00af5f, |
||||
|
36: 0x00af87, |
||||
|
37: 0x00afaf, |
||||
|
38: 0x00afd7, |
||||
|
39: 0x00afff, |
||||
|
40: 0x00d700, |
||||
|
41: 0x00d75f, |
||||
|
42: 0x00d787, |
||||
|
43: 0x00d7af, |
||||
|
44: 0x00d7d7, |
||||
|
45: 0x00d7ff, |
||||
|
46: 0x00ff00, |
||||
|
47: 0x00ff5f, |
||||
|
48: 0x00ff87, |
||||
|
49: 0x00ffaf, |
||||
|
50: 0x00ffd7, |
||||
|
51: 0x00ffff, |
||||
|
52: 0x5f0000, |
||||
|
53: 0x5f005f, |
||||
|
54: 0x5f0087, |
||||
|
55: 0x5f00af, |
||||
|
56: 0x5f00d7, |
||||
|
57: 0x5f00ff, |
||||
|
58: 0x5f5f00, |
||||
|
59: 0x5f5f5f, |
||||
|
60: 0x5f5f87, |
||||
|
61: 0x5f5faf, |
||||
|
62: 0x5f5fd7, |
||||
|
63: 0x5f5fff, |
||||
|
64: 0x5f8700, |
||||
|
65: 0x5f875f, |
||||
|
66: 0x5f8787, |
||||
|
67: 0x5f87af, |
||||
|
68: 0x5f87d7, |
||||
|
69: 0x5f87ff, |
||||
|
70: 0x5faf00, |
||||
|
71: 0x5faf5f, |
||||
|
72: 0x5faf87, |
||||
|
73: 0x5fafaf, |
||||
|
74: 0x5fafd7, |
||||
|
75: 0x5fafff, |
||||
|
76: 0x5fd700, |
||||
|
77: 0x5fd75f, |
||||
|
78: 0x5fd787, |
||||
|
79: 0x5fd7af, |
||||
|
80: 0x5fd7d7, |
||||
|
81: 0x5fd7ff, |
||||
|
82: 0x5fff00, |
||||
|
83: 0x5fff5f, |
||||
|
84: 0x5fff87, |
||||
|
85: 0x5fffaf, |
||||
|
86: 0x5fffd7, |
||||
|
87: 0x5fffff, |
||||
|
88: 0x870000, |
||||
|
89: 0x87005f, |
||||
|
90: 0x870087, |
||||
|
91: 0x8700af, |
||||
|
92: 0x8700d7, |
||||
|
93: 0x8700ff, |
||||
|
94: 0x875f00, |
||||
|
95: 0x875f5f, |
||||
|
96: 0x875f87, |
||||
|
97: 0x875faf, |
||||
|
98: 0x875fd7, |
||||
|
99: 0x875fff, |
||||
|
100: 0x878700, |
||||
|
101: 0x87875f, |
||||
|
102: 0x878787, |
||||
|
103: 0x8787af, |
||||
|
104: 0x8787d7, |
||||
|
105: 0x8787ff, |
||||
|
106: 0x87af00, |
||||
|
107: 0x87af5f, |
||||
|
108: 0x87af87, |
||||
|
109: 0x87afaf, |
||||
|
110: 0x87afd7, |
||||
|
111: 0x87afff, |
||||
|
112: 0x87d700, |
||||
|
113: 0x87d75f, |
||||
|
114: 0x87d787, |
||||
|
115: 0x87d7af, |
||||
|
116: 0x87d7d7, |
||||
|
117: 0x87d7ff, |
||||
|
118: 0x87ff00, |
||||
|
119: 0x87ff5f, |
||||
|
120: 0x87ff87, |
||||
|
121: 0x87ffaf, |
||||
|
122: 0x87ffd7, |
||||
|
123: 0x87ffff, |
||||
|
124: 0xaf0000, |
||||
|
125: 0xaf005f, |
||||
|
126: 0xaf0087, |
||||
|
127: 0xaf00af, |
||||
|
128: 0xaf00d7, |
||||
|
129: 0xaf00ff, |
||||
|
130: 0xaf5f00, |
||||
|
131: 0xaf5f5f, |
||||
|
132: 0xaf5f87, |
||||
|
133: 0xaf5faf, |
||||
|
134: 0xaf5fd7, |
||||
|
135: 0xaf5fff, |
||||
|
136: 0xaf8700, |
||||
|
137: 0xaf875f, |
||||
|
138: 0xaf8787, |
||||
|
139: 0xaf87af, |
||||
|
140: 0xaf87d7, |
||||
|
141: 0xaf87ff, |
||||
|
142: 0xafaf00, |
||||
|
143: 0xafaf5f, |
||||
|
144: 0xafaf87, |
||||
|
145: 0xafafaf, |
||||
|
146: 0xafafd7, |
||||
|
147: 0xafafff, |
||||
|
148: 0xafd700, |
||||
|
149: 0xafd75f, |
||||
|
150: 0xafd787, |
||||
|
151: 0xafd7af, |
||||
|
152: 0xafd7d7, |
||||
|
153: 0xafd7ff, |
||||
|
154: 0xafff00, |
||||
|
155: 0xafff5f, |
||||
|
156: 0xafff87, |
||||
|
157: 0xafffaf, |
||||
|
158: 0xafffd7, |
||||
|
159: 0xafffff, |
||||
|
160: 0xd70000, |
||||
|
161: 0xd7005f, |
||||
|
162: 0xd70087, |
||||
|
163: 0xd700af, |
||||
|
164: 0xd700d7, |
||||
|
165: 0xd700ff, |
||||
|
166: 0xd75f00, |
||||
|
167: 0xd75f5f, |
||||
|
168: 0xd75f87, |
||||
|
169: 0xd75faf, |
||||
|
170: 0xd75fd7, |
||||
|
171: 0xd75fff, |
||||
|
172: 0xd78700, |
||||
|
173: 0xd7875f, |
||||
|
174: 0xd78787, |
||||
|
175: 0xd787af, |
||||
|
176: 0xd787d7, |
||||
|
177: 0xd787ff, |
||||
|
178: 0xd7af00, |
||||
|
179: 0xd7af5f, |
||||
|
180: 0xd7af87, |
||||
|
181: 0xd7afaf, |
||||
|
182: 0xd7afd7, |
||||
|
183: 0xd7afff, |
||||
|
184: 0xd7d700, |
||||
|
185: 0xd7d75f, |
||||
|
186: 0xd7d787, |
||||
|
187: 0xd7d7af, |
||||
|
188: 0xd7d7d7, |
||||
|
189: 0xd7d7ff, |
||||
|
190: 0xd7ff00, |
||||
|
191: 0xd7ff5f, |
||||
|
192: 0xd7ff87, |
||||
|
193: 0xd7ffaf, |
||||
|
194: 0xd7ffd7, |
||||
|
195: 0xd7ffff, |
||||
|
196: 0xff0000, |
||||
|
197: 0xff005f, |
||||
|
198: 0xff0087, |
||||
|
199: 0xff00af, |
||||
|
200: 0xff00d7, |
||||
|
201: 0xff00ff, |
||||
|
202: 0xff5f00, |
||||
|
203: 0xff5f5f, |
||||
|
204: 0xff5f87, |
||||
|
205: 0xff5faf, |
||||
|
206: 0xff5fd7, |
||||
|
207: 0xff5fff, |
||||
|
208: 0xff8700, |
||||
|
209: 0xff875f, |
||||
|
210: 0xff8787, |
||||
|
211: 0xff87af, |
||||
|
212: 0xff87d7, |
||||
|
213: 0xff87ff, |
||||
|
214: 0xffaf00, |
||||
|
215: 0xffaf5f, |
||||
|
216: 0xffaf87, |
||||
|
217: 0xffafaf, |
||||
|
218: 0xffafd7, |
||||
|
219: 0xffafff, |
||||
|
220: 0xffd700, |
||||
|
221: 0xffd75f, |
||||
|
222: 0xffd787, |
||||
|
223: 0xffd7af, |
||||
|
224: 0xffd7d7, |
||||
|
225: 0xffd7ff, |
||||
|
226: 0xffff00, |
||||
|
227: 0xffff5f, |
||||
|
228: 0xffff87, |
||||
|
229: 0xffffaf, |
||||
|
230: 0xffffd7, |
||||
|
231: 0xffffff, |
||||
|
232: 0x080808, |
||||
|
233: 0x121212, |
||||
|
234: 0x1c1c1c, |
||||
|
235: 0x262626, |
||||
|
236: 0x303030, |
||||
|
237: 0x3a3a3a, |
||||
|
238: 0x444444, |
||||
|
239: 0x4e4e4e, |
||||
|
240: 0x585858, |
||||
|
241: 0x626262, |
||||
|
242: 0x6c6c6c, |
||||
|
243: 0x767676, |
||||
|
244: 0x808080, |
||||
|
245: 0x8a8a8a, |
||||
|
246: 0x949494, |
||||
|
247: 0x9e9e9e, |
||||
|
248: 0xa8a8a8, |
||||
|
249: 0xb2b2b2, |
||||
|
250: 0xbcbcbc, |
||||
|
251: 0xc6c6c6, |
||||
|
252: 0xd0d0d0, |
||||
|
253: 0xdadada, |
||||
|
254: 0xe4e4e4, |
||||
|
255: 0xeeeeee, |
||||
|
} |
||||
|
|
||||
|
func (w *Writer) Write(data []byte) (n int, err error) { |
||||
|
var csbi consoleScreenBufferInfo |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
|
||||
|
er := bytes.NewBuffer(data) |
||||
|
loop: |
||||
|
for { |
||||
|
r1, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
if r1 == 0 { |
||||
|
break loop |
||||
|
} |
||||
|
|
||||
|
c1, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
break loop |
||||
|
} |
||||
|
if c1 != 0x1b { |
||||
|
fmt.Fprint(w.out, string(c1)) |
||||
|
continue |
||||
|
} |
||||
|
c2, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
break loop |
||||
|
} |
||||
|
if c2 != 0x5b { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
w.lastbuf.WriteRune(c2) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
var m rune |
||||
|
for { |
||||
|
c, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
w.lastbuf.WriteRune(c2) |
||||
|
w.lastbuf.Write(buf.Bytes()) |
||||
|
break loop |
||||
|
} |
||||
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { |
||||
|
m = c |
||||
|
break |
||||
|
} |
||||
|
buf.Write([]byte(string(c))) |
||||
|
} |
||||
|
|
||||
|
var csbi consoleScreenBufferInfo |
||||
|
switch m { |
||||
|
case 'A': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.y -= short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'B': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.y += short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'C': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.x -= short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'D': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
if n, err = strconv.Atoi(buf.String()); err == nil { |
||||
|
var csbi consoleScreenBufferInfo |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.x += short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
} |
||||
|
case 'E': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.x = 0 |
||||
|
csbi.cursorPosition.y += short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'F': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.x = 0 |
||||
|
csbi.cursorPosition.y -= short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'G': |
||||
|
n, err = strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) |
||||
|
csbi.cursorPosition.x = short(n) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'H': |
||||
|
token := strings.Split(buf.String(), ";") |
||||
|
if len(token) != 2 { |
||||
|
continue |
||||
|
} |
||||
|
n1, err := strconv.Atoi(token[0]) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
n2, err := strconv.Atoi(token[1]) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
csbi.cursorPosition.x = short(n2) |
||||
|
csbi.cursorPosition.x = short(n1) |
||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) |
||||
|
case 'J': |
||||
|
n, err := strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
var cursor coord |
||||
|
switch n { |
||||
|
case 0: |
||||
|
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} |
||||
|
case 1: |
||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top} |
||||
|
case 2: |
||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top} |
||||
|
} |
||||
|
var count, written dword |
||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) |
||||
|
procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) |
||||
|
procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) |
||||
|
case 'K': |
||||
|
n, err := strconv.Atoi(buf.String()) |
||||
|
if err != nil { |
||||
|
continue |
||||
|
} |
||||
|
var cursor coord |
||||
|
switch n { |
||||
|
case 0: |
||||
|
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} |
||||
|
case 1: |
||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} |
||||
|
case 2: |
||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} |
||||
|
} |
||||
|
var count, written dword |
||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x) |
||||
|
procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) |
||||
|
procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) |
||||
|
case 'm': |
||||
|
attr := csbi.attributes |
||||
|
cs := buf.String() |
||||
|
if cs == "" { |
||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.oldattr)) |
||||
|
continue |
||||
|
} |
||||
|
token := strings.Split(cs, ";") |
||||
|
for i := 0; i < len(token); i += 1 { |
||||
|
ns := token[i] |
||||
|
if n, err = strconv.Atoi(ns); err == nil { |
||||
|
switch { |
||||
|
case n == 0 || n == 100: |
||||
|
attr = w.oldattr |
||||
|
case 1 <= n && n <= 5: |
||||
|
attr |= foregroundIntensity |
||||
|
case n == 7: |
||||
|
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) |
||||
|
case 22 == n || n == 25 || n == 25: |
||||
|
attr |= foregroundIntensity |
||||
|
case n == 27: |
||||
|
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) |
||||
|
case 30 <= n && n <= 37: |
||||
|
attr = (attr & backgroundMask) |
||||
|
if (n-30)&1 != 0 { |
||||
|
attr |= foregroundRed |
||||
|
} |
||||
|
if (n-30)&2 != 0 { |
||||
|
attr |= foregroundGreen |
||||
|
} |
||||
|
if (n-30)&4 != 0 { |
||||
|
attr |= foregroundBlue |
||||
|
} |
||||
|
case n == 38: // set foreground color.
|
||||
|
if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") { |
||||
|
if n256, err := strconv.Atoi(token[i+2]); err == nil { |
||||
|
if n256foreAttr == nil { |
||||
|
n256setup() |
||||
|
} |
||||
|
attr &= backgroundMask |
||||
|
attr |= n256foreAttr[n256] |
||||
|
i += 2 |
||||
|
} |
||||
|
} else { |
||||
|
attr = attr & (w.oldattr & backgroundMask) |
||||
|
} |
||||
|
case n == 39: // reset foreground color.
|
||||
|
attr &= backgroundMask |
||||
|
attr |= w.oldattr & foregroundMask |
||||
|
case 40 <= n && n <= 47: |
||||
|
attr = (attr & foregroundMask) |
||||
|
if (n-40)&1 != 0 { |
||||
|
attr |= backgroundRed |
||||
|
} |
||||
|
if (n-40)&2 != 0 { |
||||
|
attr |= backgroundGreen |
||||
|
} |
||||
|
if (n-40)&4 != 0 { |
||||
|
attr |= backgroundBlue |
||||
|
} |
||||
|
case n == 48: // set background color.
|
||||
|
if i < len(token)-2 && token[i+1] == "5" { |
||||
|
if n256, err := strconv.Atoi(token[i+2]); err == nil { |
||||
|
if n256backAttr == nil { |
||||
|
n256setup() |
||||
|
} |
||||
|
attr &= foregroundMask |
||||
|
attr |= n256backAttr[n256] |
||||
|
i += 2 |
||||
|
} |
||||
|
} else { |
||||
|
attr = attr & (w.oldattr & foregroundMask) |
||||
|
} |
||||
|
case n == 49: // reset foreground color.
|
||||
|
attr &= foregroundMask |
||||
|
attr |= w.oldattr & backgroundMask |
||||
|
case 90 <= n && n <= 97: |
||||
|
attr = (attr & backgroundMask) |
||||
|
attr |= foregroundIntensity |
||||
|
if (n-90)&1 != 0 { |
||||
|
attr |= foregroundRed |
||||
|
} |
||||
|
if (n-90)&2 != 0 { |
||||
|
attr |= foregroundGreen |
||||
|
} |
||||
|
if (n-90)&4 != 0 { |
||||
|
attr |= foregroundBlue |
||||
|
} |
||||
|
case 100 <= n && n <= 107: |
||||
|
attr = (attr & foregroundMask) |
||||
|
attr |= backgroundIntensity |
||||
|
if (n-100)&1 != 0 { |
||||
|
attr |= backgroundRed |
||||
|
} |
||||
|
if (n-100)&2 != 0 { |
||||
|
attr |= backgroundGreen |
||||
|
} |
||||
|
if (n-100)&4 != 0 { |
||||
|
attr |= backgroundBlue |
||||
|
} |
||||
|
} |
||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return len(data) - w.lastbuf.Len(), nil |
||||
|
} |
||||
|
|
||||
|
type consoleColor struct { |
||||
|
rgb int |
||||
|
red bool |
||||
|
green bool |
||||
|
blue bool |
||||
|
intensity bool |
||||
|
} |
||||
|
|
||||
|
func (c consoleColor) foregroundAttr() (attr word) { |
||||
|
if c.red { |
||||
|
attr |= foregroundRed |
||||
|
} |
||||
|
if c.green { |
||||
|
attr |= foregroundGreen |
||||
|
} |
||||
|
if c.blue { |
||||
|
attr |= foregroundBlue |
||||
|
} |
||||
|
if c.intensity { |
||||
|
attr |= foregroundIntensity |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (c consoleColor) backgroundAttr() (attr word) { |
||||
|
if c.red { |
||||
|
attr |= backgroundRed |
||||
|
} |
||||
|
if c.green { |
||||
|
attr |= backgroundGreen |
||||
|
} |
||||
|
if c.blue { |
||||
|
attr |= backgroundBlue |
||||
|
} |
||||
|
if c.intensity { |
||||
|
attr |= backgroundIntensity |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
var color16 = []consoleColor{ |
||||
|
consoleColor{0x000000, false, false, false, false}, |
||||
|
consoleColor{0x000080, false, false, true, false}, |
||||
|
consoleColor{0x008000, false, true, false, false}, |
||||
|
consoleColor{0x008080, false, true, true, false}, |
||||
|
consoleColor{0x800000, true, false, false, false}, |
||||
|
consoleColor{0x800080, true, false, true, false}, |
||||
|
consoleColor{0x808000, true, true, false, false}, |
||||
|
consoleColor{0xc0c0c0, true, true, true, false}, |
||||
|
consoleColor{0x808080, false, false, false, true}, |
||||
|
consoleColor{0x0000ff, false, false, true, true}, |
||||
|
consoleColor{0x00ff00, false, true, false, true}, |
||||
|
consoleColor{0x00ffff, false, true, true, true}, |
||||
|
consoleColor{0xff0000, true, false, false, true}, |
||||
|
consoleColor{0xff00ff, true, false, true, true}, |
||||
|
consoleColor{0xffff00, true, true, false, true}, |
||||
|
consoleColor{0xffffff, true, true, true, true}, |
||||
|
} |
||||
|
|
||||
|
type hsv struct { |
||||
|
h, s, v float32 |
||||
|
} |
||||
|
|
||||
|
func (a hsv) dist(b hsv) float32 { |
||||
|
dh := a.h - b.h |
||||
|
switch { |
||||
|
case dh > 0.5: |
||||
|
dh = 1 - dh |
||||
|
case dh < -0.5: |
||||
|
dh = -1 - dh |
||||
|
} |
||||
|
ds := a.s - b.s |
||||
|
dv := a.v - b.v |
||||
|
return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv))) |
||||
|
} |
||||
|
|
||||
|
func toHSV(rgb int) hsv { |
||||
|
r, g, b := float32((rgb&0xFF0000)>>16)/256.0, |
||||
|
float32((rgb&0x00FF00)>>8)/256.0, |
||||
|
float32(rgb&0x0000FF)/256.0 |
||||
|
min, max := minmax3f(r, g, b) |
||||
|
h := max - min |
||||
|
if h > 0 { |
||||
|
if max == r { |
||||
|
h = (g - b) / h |
||||
|
if h < 0 { |
||||
|
h += 6 |
||||
|
} |
||||
|
} else if max == g { |
||||
|
h = 2 + (b-r)/h |
||||
|
} else { |
||||
|
h = 4 + (r-g)/h |
||||
|
} |
||||
|
} |
||||
|
h /= 6.0 |
||||
|
s := max - min |
||||
|
if max != 0 { |
||||
|
s /= max |
||||
|
} |
||||
|
v := max |
||||
|
return hsv{h: h, s: s, v: v} |
||||
|
} |
||||
|
|
||||
|
type hsvTable []hsv |
||||
|
|
||||
|
func toHSVTable(rgbTable []consoleColor) hsvTable { |
||||
|
t := make(hsvTable, len(rgbTable)) |
||||
|
for i, c := range rgbTable { |
||||
|
t[i] = toHSV(c.rgb) |
||||
|
} |
||||
|
return t |
||||
|
} |
||||
|
|
||||
|
func (t hsvTable) find(rgb int) consoleColor { |
||||
|
hsv := toHSV(rgb) |
||||
|
n := 7 |
||||
|
l := float32(5.0) |
||||
|
for i, p := range t { |
||||
|
d := hsv.dist(p) |
||||
|
if d < l { |
||||
|
l, n = d, i |
||||
|
} |
||||
|
} |
||||
|
return color16[n] |
||||
|
} |
||||
|
|
||||
|
func minmax3f(a, b, c float32) (min, max float32) { |
||||
|
if a < b { |
||||
|
if b < c { |
||||
|
return a, c |
||||
|
} else if a < c { |
||||
|
return a, b |
||||
|
} else { |
||||
|
return c, b |
||||
|
} |
||||
|
} else { |
||||
|
if a < c { |
||||
|
return b, c |
||||
|
} else if b < c { |
||||
|
return b, a |
||||
|
} else { |
||||
|
return c, a |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var n256foreAttr []word |
||||
|
var n256backAttr []word |
||||
|
|
||||
|
func n256setup() { |
||||
|
n256foreAttr = make([]word, 256) |
||||
|
n256backAttr = make([]word, 256) |
||||
|
t := toHSVTable(color16) |
||||
|
for i, rgb := range color256 { |
||||
|
c := t.find(rgb) |
||||
|
n256foreAttr[i] = c.foregroundAttr() |
||||
|
n256backAttr[i] = c.backgroundAttr() |
||||
|
} |
||||
|
} |
@ -0,0 +1,57 @@ |
|||||
|
package colorable |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
) |
||||
|
|
||||
|
type NonColorable struct { |
||||
|
out io.Writer |
||||
|
lastbuf bytes.Buffer |
||||
|
} |
||||
|
|
||||
|
func NewNonColorable(w io.Writer) io.Writer { |
||||
|
return &NonColorable{out: w} |
||||
|
} |
||||
|
|
||||
|
func (w *NonColorable) Write(data []byte) (n int, err error) { |
||||
|
er := bytes.NewBuffer(data) |
||||
|
loop: |
||||
|
for { |
||||
|
c1, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
break loop |
||||
|
} |
||||
|
if c1 != 0x1b { |
||||
|
fmt.Fprint(w.out, string(c1)) |
||||
|
continue |
||||
|
} |
||||
|
c2, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
break loop |
||||
|
} |
||||
|
if c2 != 0x5b { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
w.lastbuf.WriteRune(c2) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
for { |
||||
|
c, _, err := er.ReadRune() |
||||
|
if err != nil { |
||||
|
w.lastbuf.WriteRune(c1) |
||||
|
w.lastbuf.WriteRune(c2) |
||||
|
w.lastbuf.Write(buf.Bytes()) |
||||
|
break loop |
||||
|
} |
||||
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { |
||||
|
break |
||||
|
} |
||||
|
buf.Write([]byte(string(c))) |
||||
|
} |
||||
|
} |
||||
|
return len(data) - w.lastbuf.Len(), nil |
||||
|
} |