├── .all-contributorsrc ├── .dockerignore ├── .editorconfig ├── .gitpod.yml ├── .golangci.yml ├── .goreleaser.yml ├── .releaserc.js ├── AUTHORS ├── COPYRIGHT ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── SECURITY.md ├── depaware.txt ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── internal └── tools │ └── tools_test.go ├── package.json ├── progress.go ├── progress_test.go └── rules.mk /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg)](#contributors)", 8 | "contributors": [ 9 | { 10 | "login": "moul", 11 | "name": "Manfred Touron", 12 | "avatar_url": "https://avatars1.githubusercontent.com/u/94029?v=4", 13 | "profile": "http://manfred.life", 14 | "contributions": [ 15 | "maintenance", 16 | "doc", 17 | "test", 18 | "code" 19 | ] 20 | }, 21 | { 22 | "login": "moul-bot", 23 | "name": "moul-bot", 24 | "avatar_url": "https://avatars1.githubusercontent.com/u/41326314?v=4", 25 | "profile": "https://manfred.life/moul-bot", 26 | "contributions": [ 27 | "maintenance" 28 | ] 29 | } 30 | ], 31 | "contributorsPerLine": 7, 32 | "projectName": "progress", 33 | "projectOwner": "moul", 34 | "repoType": "github", 35 | "repoHost": "https://github.com", 36 | "skipCi": true 37 | } 38 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ## 2 | ## Specific to .dockerignore 3 | ## 4 | 5 | .git/ 6 | Dockerfile 7 | contrib/ 8 | 9 | ## 10 | ## Common with .gitignore 11 | ## 12 | 13 | # Temporary files 14 | *~ 15 | *# 16 | .#* 17 | 18 | # Vendors 19 | node_modules/ 20 | vendor/ 21 | 22 | # Binaries for programs and plugins 23 | dist/ 24 | gin-bin 25 | *.exe 26 | *.exe~ 27 | *.dll 28 | *.so 29 | *.dylib 30 | 31 | # Test binary, build with `go test -c` 32 | *.test 33 | 34 | # Output of the go coverage tool, specifically when used with LiteIDE 35 | *.out 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.mod] 14 | indent_style = tab 15 | 16 | [{Makefile,**.mk}] 17 | indent_style = tab 18 | 19 | [*.go] 20 | indent_style = tab 21 | 22 | [*.css] 23 | indent_size = 2 24 | 25 | [*.proto] 26 | indent_size = 2 27 | 28 | [*.ftl] 29 | indent_size = 2 30 | 31 | [*.toml] 32 | indent_size = 2 33 | 34 | [*.swift] 35 | indent_size = 4 36 | 37 | [*.tmpl] 38 | indent_size = 2 39 | 40 | [*.js] 41 | indent_size = 2 42 | block_comment_start = /* 43 | block_comment_end = */ 44 | 45 | [*.{html,htm}] 46 | indent_size = 2 47 | 48 | [*.bat] 49 | end_of_line = crlf 50 | 51 | [*.{yml,yaml}] 52 | indent_size = 2 53 | 54 | [*.json] 55 | indent_size = 2 56 | 57 | [.{babelrc,eslintrc,prettierrc}] 58 | indent_size = 2 59 | 60 | [{Fastfile,.buckconfig,BUCK}] 61 | indent_size = 2 62 | 63 | [*.diff] 64 | indent_size = 1 65 | 66 | [*.m] 67 | indent_size = 1 68 | indent_style = space 69 | block_comment_start = /** 70 | block_comment = * 71 | block_comment_end = */ 72 | 73 | [*.java] 74 | indent_size = 4 75 | indent_style = space 76 | block_comment_start = /** 77 | block_comment = * 78 | block_comment_end = */ 79 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go get && go build ./... && go test ./... 3 | command: go run 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 1m 3 | tests: false 4 | skip-files: 5 | - "testing.go" 6 | - ".*\\.pb\\.go" 7 | - ".*\\.gen\\.go" 8 | 9 | linters-settings: 10 | golint: 11 | min-confidence: 0 12 | maligned: 13 | suggest-new: true 14 | goconst: 15 | min-len: 5 16 | min-occurrences: 4 17 | misspell: 18 | locale: US 19 | 20 | linters: 21 | disable-all: true 22 | enable: 23 | - asciicheck 24 | - bodyclose 25 | - deadcode 26 | - depguard 27 | - dogsled 28 | - dupl 29 | - errcheck 30 | - exhaustive 31 | - exportloopref 32 | - gci 33 | - gochecknoinits 34 | - goconst 35 | - gocritic 36 | - gocyclo 37 | - godot 38 | - goerr113 39 | - gofmt 40 | - gofumpt 41 | - goimports 42 | - golint 43 | - gomnd 44 | - gomodguard 45 | - gosec 46 | - gosimple 47 | - govet 48 | - ineffassign 49 | - interfacer 50 | - maligned 51 | - misspell 52 | - nakedret 53 | - nestif 54 | - noctx 55 | - nolintlint 56 | - prealloc 57 | - scopelint 58 | - sqlclosecheck 59 | - staticcheck 60 | - structcheck 61 | - stylecheck 62 | - typecheck 63 | - unconvert 64 | - unparam 65 | - unused 66 | - varcheck 67 | - whitespace 68 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - GOPROXY=https://proxy.golang.org 4 | before: 5 | hooks: 6 | - go mod download 7 | builds: 8 | - 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | goarch: 16 | - 386 17 | - amd64 18 | - arm 19 | - arm64 20 | ignore: 21 | - 22 | goos: darwin 23 | goarch: 386 24 | flags: 25 | - "-a" 26 | ldflags: 27 | - '-extldflags "-static"' 28 | checksum: 29 | name_template: '{{.ProjectName}}_checksums.txt' 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | - Merge pull request 37 | - Merge branch 38 | archives: 39 | - 40 | name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 41 | replacements: 42 | darwin: Darwin 43 | linux: Linux 44 | windows: Windows 45 | 386: i386 46 | amd64: x86_64 47 | format_overrides: 48 | - 49 | goos: windows 50 | format: zip 51 | wrap_in_directory: true 52 | brews: 53 | - 54 | name: progress 55 | # github: 56 | # owner: moul 57 | # name: homebrew-moul 58 | commit_author: 59 | name: moul-bot 60 | email: "bot@moul.io" 61 | homepage: https://github.com/moul/progress 62 | description: "progress" 63 | nfpms: 64 | - 65 | file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 66 | homepage: https://github.com/moul/progress 67 | description: "progress" 68 | maintainer: "Manfred Touron " 69 | license: "Apache-2.0 OR MIT" 70 | vendor: moul 71 | formats: 72 | - deb 73 | - rpm 74 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branch: 'master', 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/github', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This file lists all individuals having contributed content to the repository. 2 | # For how it is generated, see 'https://github.com/moul/rules.mk' 3 | 4 | Manfred Touron <94029+moul@users.noreply.github.com> 5 | Renovate Bot 6 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2020 Manfred Touron and other progress Developers. 2 | 3 | Intellectual Property Notice 4 | ---------------------------- 5 | 6 | progress is licensed under the Apache License, Version 2.0 7 | (see LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or 8 | the MIT license (see LICENSE-MIT or http://opensource.org/licenses/MIT), 9 | at your option. 10 | 11 | Copyrights and patents in the progress project are retained 12 | by contributors. 13 | No copyright assignment is required to contribute to progress. 14 | 15 | SPDX-License-Identifier: (Apache-2.0 OR MIT) 16 | 17 | SPDX usage 18 | ---------- 19 | 20 | Individual files may contain SPDX tags instead of the full license text. 21 | This enables machine processing of license information based on the SPDX 22 | License Identifiers that are available here: https://spdx.org/licenses/ 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # dynamic config 2 | ARG BUILD_DATE 3 | ARG VCS_REF 4 | ARG VERSION 5 | 6 | # build 7 | FROM golang:1.18.1-alpine as builder 8 | RUN apk add --no-cache git gcc musl-dev make 9 | ENV GO111MODULE=on 10 | WORKDIR /go/src/moul.io/progress 11 | COPY go.* ./ 12 | RUN go mod download 13 | COPY . ./ 14 | RUN make install 15 | 16 | # minimalist runtime 17 | FROM alpine:3.16.0 18 | LABEL org.label-schema.build-date=$BUILD_DATE \ 19 | org.label-schema.name="progress" \ 20 | org.label-schema.description="" \ 21 | org.label-schema.url="https://moul.io/progress/" \ 22 | org.label-schema.vcs-ref=$VCS_REF \ 23 | org.label-schema.vcs-url="https://github.com/moul/progress" \ 24 | org.label-schema.vendor="Manfred Touron" \ 25 | org.label-schema.version=$VERSION \ 26 | org.label-schema.schema-version="1.0" \ 27 | org.label-schema.cmd="docker run -i -t --rm moul/progress" \ 28 | org.label-schema.help="docker exec -it $CONTAINER progress --help" 29 | COPY --from=builder /go/bin/progress /bin/ 30 | ENTRYPOINT ["/bin/progress"] 31 | #CMD [] 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Manfred Touron (manfred.life) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Manfred Touron (manfred.life) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPKG ?= moul.io/progress 2 | DOCKER_IMAGE ?= moul/progress 3 | GOBINS ?= . 4 | NPM_PACKAGES ?= . 5 | 6 | include rules.mk 7 | 8 | generate: install 9 | GO111MODULE=off go get github.com/campoy/embedmd 10 | mkdir -p .tmp 11 | go doc -all > .tmp/godoc.txt 12 | embedmd -w README.md 13 | rm -rf .tmp 14 | .PHONY: generate 15 | 16 | lint: 17 | cd tool/lint; make 18 | .PHONY: lint 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # progress 2 | 3 | :smile: progress 4 | 5 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/moul.io/progress) 6 | [![License](https://img.shields.io/badge/license-Apache--2.0%20%2F%20MIT-%2397ca00.svg)](https://github.com/moul/progress/blob/master/COPYRIGHT) 7 | [![GitHub release](https://img.shields.io/github/release/moul/progress.svg)](https://github.com/moul/progress/releases) 8 | [![Made by Manfred Touron](https://img.shields.io/badge/made%20by-Manfred%20Touron-blue.svg?style=flat)](https://manfred.life/) 9 | 10 | [![Go](https://github.com/moul/progress/workflows/Go/badge.svg)](https://github.com/moul/progress/actions?query=workflow%3AGo) 11 | [![PR](https://github.com/moul/progress/workflows/PR/badge.svg)](https://github.com/moul/progress/actions?query=workflow%3APR) 12 | [![GolangCI](https://golangci.com/badges/github.com/moul/progress.svg)](https://golangci.com/r/github.com/moul/progress) 13 | [![codecov](https://codecov.io/gh/moul/progress/branch/master/graph/badge.svg)](https://codecov.io/gh/moul/progress) 14 | [![Go Report Card](https://goreportcard.com/badge/moul.io/progress)](https://goreportcard.com/report/moul.io/progress) 15 | [![CodeFactor](https://www.codefactor.io/repository/github/moul/progress/badge)](https://www.codefactor.io/repository/github/moul/progress) 16 | 17 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/moul/progress) 18 | 19 | ## Usage 20 | 21 | [embedmd]:# (.tmp/godoc.txt txt /TYPES/ $) 22 | ```txt 23 | TYPES 24 | 25 | type Progress struct { 26 | Steps []*Step `json:"steps,omitempty"` 27 | CreatedAt time.Time `json:"created_at,omitempty"` 28 | 29 | // Has unexported fields. 30 | } 31 | Progress is the top-level object of the 'progress' library. 32 | 33 | func New() *Progress 34 | New creates and returns a new Progress. 35 | 36 | func (p *Progress) AddStep(id string) *Step 37 | AddStep creates and returns a new Step with the provided 'id'. A non-empty, 38 | unique 'id' is required, else it will panic. 39 | 40 | func (p *Progress) Get(id string) *Step 41 | Get retrieves a Step by its 'id'. A non-empty 'id' is required, else it will 42 | panic. If 'id' does not match an existing step, nil is returned. 43 | 44 | func (p *Progress) MarshalJSON() ([]byte, error) 45 | MarshalJSON is a custom JSON marshaler that automatically computes and 46 | append the current snapshot. 47 | 48 | func (p *Progress) Percent() float64 49 | Percent returns the current completion percentage, it's a faster alternative 50 | to Progress.Snapshot().Percent. 51 | 52 | func (p *Progress) SafeAddStep(id string) (*Step, error) 53 | SafeAddStep is equivalent to AddStep with but returns error instead of 54 | panicking. 55 | 56 | func (p *Progress) Snapshot() Snapshot 57 | Snapshot computes and returns the current stats of the Progress. 58 | 59 | func (p *Progress) Subscribe(subscriber chan *Step) 60 | Subscribe register a provided chan as a target called each time a step is 61 | changed. 62 | 63 | type Snapshot struct { 64 | State State `json:"state,omitempty"` 65 | Doing string `json:"doing,omitempty"` 66 | NotStarted int `json:"not_started,omitempty"` 67 | InProgress int `json:"in_progress,omitempty"` 68 | Completed int `json:"completed,omitempty"` 69 | Total int `json:"total,omitempty"` 70 | Percent float64 `json:"percent,omitempty"` 71 | TotalDuration time.Duration `json:"total_duration,omitempty"` 72 | StepDuration time.Duration `json:"step_duration,omitempty"` 73 | CompletionEstimate time.Duration `json:"completion_estimate,omitempty"` 74 | DoneAt *time.Time `json:"done_at,omitempty"` 75 | StartedAt *time.Time `json:"started_at,omitempty"` 76 | } 77 | Snapshot represents info and stats about a progress at a given time. 78 | 79 | type State string 80 | 81 | const ( 82 | StateNotStarted State = "not started" 83 | StateInProgress State = "in progress" 84 | StateDone State = "done" 85 | ) 86 | type Step struct { 87 | ID string `json:"id,omitempty"` 88 | Description string `json:"description,omitempty"` 89 | StartedAt *time.Time `json:"started_at,omitempty"` 90 | DoneAt *time.Time `json:"done_at,omitempty"` 91 | State State `json:"state,omitempty"` 92 | Data interface{} `json:"data,omitempty"` 93 | 94 | // Has unexported fields. 95 | } 96 | Step represents a progress step. It always have an 'id' and can be 97 | customized using helpers. 98 | 99 | func (s *Step) Done() 100 | Done marks a step as done. If the step was already done, it panics. 101 | 102 | func (s *Step) Duration() time.Duration 103 | Duration computes the step duration. 104 | 105 | func (s *Step) MarshalJSON() ([]byte, error) 106 | MarshalJSON is a custom JSON marshaler that automatically computes and 107 | append some runtime metadata. 108 | 109 | func (s *Step) SetAsCurrent() 110 | SetAsCurrent stops all in-progress steps and start this one. 111 | 112 | func (s *Step) SetData(data interface{}) *Step 113 | SetData sets a custom step data. It returns itself (*Step) for chaining. 114 | 115 | func (s *Step) SetDescription(desc string) *Step 116 | SetDescription sets a custom step description. It returns itself (*Step) for 117 | chaining. 118 | 119 | func (s *Step) Start() 120 | Start marks a step as started. If a step was already InProgress or Done, it 121 | panics. 122 | 123 | ``` 124 | 125 | ## Example 126 | 127 | [embedmd]:# (example_test.go /import\ / $) 128 | ```go 129 | import ( 130 | "fmt" 131 | "time" 132 | 133 | "moul.io/progress" 134 | "moul.io/u" 135 | ) 136 | 137 | func Example() { 138 | // initialize a new progress.Progress 139 | prog := progress.New() 140 | prog.AddStep("init").SetDescription("initialize") 141 | prog.AddStep("step1").SetDescription("step 1") 142 | prog.AddStep("step2").SetData([]string{"hello", "world"}).SetDescription("step 2") 143 | prog.AddStep("step3") 144 | prog.AddStep("finish") 145 | 146 | // automatically mark the last step as done when the function quit 147 | defer prog.Get("finish").Done() 148 | 149 | // mark init as Done 150 | prog.Get("init").Done() 151 | 152 | // mark step1 as started 153 | prog.Get("step1").SetData(42).Start() 154 | 155 | // then, mark it as done + attach custom data 156 | prog.Get("step1").SetData(1337).Done() 157 | 158 | // mark step2 as started 159 | prog.Get("step2").Start() 160 | 161 | fmt.Println(u.PrettyJSON(prog)) 162 | 163 | // outputs something like this: 164 | // { 165 | // "steps": [ 166 | // { 167 | // "id": "init", 168 | // "description": "initialize", 169 | // "started_at": "2020-12-22T20:26:05.717427484+01:00", 170 | // "done_at": "2020-12-22T20:26:05.717427484+01:00", 171 | // "state": "done" 172 | // }, 173 | // { 174 | // "id": "step1", 175 | // "description": "step 1", 176 | // "started_at": "2020-12-22T20:26:05.71742797+01:00", 177 | // "done_at": "2020-12-22T20:26:05.717428258+01:00", 178 | // "state": "done", 179 | // "data": 1337, 180 | // "duration": 286 181 | // }, 182 | // { 183 | // "id": "step2", 184 | // "description": "step 2", 185 | // "started_at": "2020-12-22T20:26:05.71742865+01:00", 186 | // "state": "in progress", 187 | // "data": [ 188 | // "hello", 189 | // "world" 190 | // ], 191 | // "duration": 496251 192 | // }, 193 | // { 194 | // "id": "step3" 195 | // }, 196 | // { 197 | // "id": "finish" 198 | // } 199 | // ], 200 | // "created_at": "2020-12-22T20:26:05.717423018+01:00", 201 | // "snapshot": { 202 | // "state": "in progress", 203 | // "doing": "step 2", 204 | // "not_started": 2, 205 | // "in_progress": 1, 206 | // "completed": 2, 207 | // "total": 5, 208 | // "percent": 50, 209 | // "total_duration": 25935, 210 | // "started_at": "2020-12-22T20:26:05.717427484+01:00" 211 | // } 212 | //} 213 | } 214 | 215 | func ExampleProgressSubscribe() { 216 | prog := progress.New() 217 | done := make(chan bool) 218 | ch := make(chan *progress.Step, 0) 219 | prog.Subscribe(ch) 220 | go func() { 221 | idx := 0 222 | for step := range ch { 223 | if step == nil { 224 | break 225 | } 226 | fmt.Println(idx, step.ID, step.State) 227 | idx++ 228 | } 229 | done <- true 230 | }() 231 | time.Sleep(10 * time.Millisecond) 232 | prog.AddStep("step1").SetDescription("hello") 233 | prog.AddStep("step2") 234 | prog.Get("step1").Start() 235 | prog.Get("step2").Done() 236 | prog.AddStep("step3") 237 | prog.Get("step3").Start() 238 | prog.Get("step1").Done() 239 | prog.Get("step3").Done() 240 | // fmt.Println(u.PrettyJSON(prog)) 241 | <-done 242 | close(ch) 243 | 244 | // Output: 245 | // 0 step1 not started 246 | // 1 step1 not started 247 | // 2 step2 not started 248 | // 3 step1 in progress 249 | // 4 step2 done 250 | // 5 step3 not started 251 | // 6 step3 in progress 252 | // 7 step1 done 253 | // 8 step3 done 254 | } 255 | ``` 256 | 257 | ## Install 258 | 259 | ### Using go 260 | 261 | ```sh 262 | go get moul.io/progress 263 | ``` 264 | 265 | ## Contribute 266 | 267 | ![Contribute <3](https://raw.githubusercontent.com/moul/moul/master/contribute.gif) 268 | 269 | I really welcome contributions. 270 | Your input is the most precious material. 271 | I'm well aware of that and I thank you in advance. 272 | Everyone is encouraged to look at what they can do on their own scale; 273 | no effort is too small. 274 | 275 | Everything on contribution is sum up here: [CONTRIBUTING.md](./CONTRIBUTING.md) 276 | 277 | ### Contributors ✨ 278 | 279 | 280 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg)](#contributors) 281 | 282 | 283 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |

Manfred Touron

🚧 📖 ⚠️ 💻

moul-bot

🚧
294 | 295 | 296 | 297 | 298 | 299 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) 300 | specification. Contributions of any kind welcome! 301 | 302 | ### Stargazers over time 303 | 304 | [![Stargazers over time](https://starchart.cc/moul/progress.svg)](https://starchart.cc/moul/progress) 305 | 306 | ## License 307 | 308 | © 2020 [Manfred Touron](https://manfred.life) 309 | 310 | Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 311 | ([`LICENSE-APACHE`](LICENSE-APACHE)) or the [MIT license](https://opensource.org/licenses/MIT) 312 | ([`LICENSE-MIT`](LICENSE-MIT)), at your option. 313 | See the [`COPYRIGHT`](COPYRIGHT) file for more details. 314 | 315 | `SPDX-License-Identifier: (Apache-2.0 OR MIT)` 316 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues 2 | 3 | I take security seriously. If you discover a security issue, 4 | please bring it to my attention right away! 5 | 6 | ## Reporting a Vulnerability 7 | 8 | Please **DO NOT** file a public issue, 9 | instead send your report privately to security@moul.io. 10 | 11 | Security reports are greatly appreciated and I will publicly thank you for it, 12 | although I keep your name confidential if you request it. 13 | -------------------------------------------------------------------------------- /depaware.txt: -------------------------------------------------------------------------------- 1 | moul.io/progress dependencies: (generated by github.com/tailscale/depaware) 2 | 3 | go.uber.org/atomic from go.uber.org/multierr 4 | go.uber.org/multierr from moul.io/u 5 | moul.io/u from moul.io/progress 6 | archive/zip from moul.io/u 7 | bufio from archive/zip+ 8 | bytes from bufio+ 9 | compress/flate from archive/zip 10 | context from moul.io/u+ 11 | crypto from crypto/sha1 12 | crypto/sha1 from moul.io/u 13 | encoding from encoding/json 14 | encoding/base64 from encoding/json+ 15 | encoding/binary from archive/zip+ 16 | encoding/json from go.uber.org/atomic+ 17 | errors from archive/zip+ 18 | fmt from compress/flate+ 19 | hash from archive/zip+ 20 | hash/crc32 from archive/zip 21 | io from archive/zip+ 22 | io/ioutil from archive/zip+ 23 | math from compress/flate+ 24 | math/bits from compress/flate+ 25 | os from archive/zip+ 26 | os/exec from moul.io/u 27 | os/signal from moul.io/u 28 | os/user from moul.io/u 29 | path from archive/zip 30 | path/filepath from io/ioutil+ 31 | reflect from encoding/binary+ 32 | sort from compress/flate+ 33 | strconv from compress/flate+ 34 | strings from archive/zip+ 35 | sync from archive/zip+ 36 | sync/atomic from context+ 37 | syscall from internal/poll+ 38 | time from archive/zip+ 39 | unicode from bytes+ 40 | unicode/utf16 from encoding/json+ 41 | unicode/utf8 from archive/zip+ 42 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Manfred Touron 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // message from the author: 5 | // +--------------------------------------------------------------+ 6 | // | * * * ░░░░░░░░░░░░░░░░░░░░ Hello ░░░░░░░░░░░░░░░░░░░░░░░░░░| 7 | // +--------------------------------------------------------------+ 8 | // | | 9 | // | ++ ______________________________________ | 10 | // | ++++ / \ | 11 | // | ++++ | | | 12 | // | ++++++++++ | Feel free to contribute to this | | 13 | // | +++ | | project or contact me on | | 14 | // | ++ | | manfred.life if you like this | | 15 | // | + -== ==| | project! | | 16 | // | ( <*> <*> | | | 17 | // | | | /| :) | | 18 | // | | _) / | | | 19 | // | | +++ / \______________________________________/ | 20 | // | \ =+ / | 21 | // | \ + | 22 | // | |\++++++ | 23 | // | | ++++ ||// | 24 | // | ___| |___ _||/__ __| 25 | // | / --- \ \| ||| __ _ ___ __ __/ /| 26 | // |/ | | \ \ / / ' \/ _ \/ // / / | 27 | // || | | | | | /_/_/_/\___/\_,_/_/ | 28 | // +--------------------------------------------------------------+ 29 | package progress // import "moul.io/progress" 30 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package progress_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "moul.io/progress" 8 | "moul.io/u" 9 | ) 10 | 11 | func Example() { 12 | // initialize a new progress.Progress 13 | prog := progress.New() 14 | prog.AddStep("init").SetDescription("initialize") 15 | prog.AddStep("step1").SetDescription("step 1") 16 | prog.AddStep("step2").SetData([]string{"hello", "world"}).SetDescription("step 2") 17 | prog.AddStep("step3") 18 | prog.AddStep("finish") 19 | 20 | // automatically mark the last step as done when the function quit 21 | defer prog.Get("finish").Done() 22 | 23 | // mark init as Done 24 | prog.Get("init").Done() 25 | 26 | // mark step1 as started 27 | prog.Get("step1").SetData(42).Start() 28 | 29 | // then, mark it as done + attach custom data 30 | prog.Get("step1").SetData(1337).Done() 31 | 32 | // mark step2 as started 33 | prog.Get("step2").Start() 34 | 35 | fmt.Println(u.PrettyJSON(prog)) 36 | 37 | // outputs something like this: 38 | // { 39 | // "steps": [ 40 | // { 41 | // "id": "init", 42 | // "description": "initialize", 43 | // "started_at": "2020-12-22T20:26:05.717427484+01:00", 44 | // "done_at": "2020-12-22T20:26:05.717427484+01:00", 45 | // "state": "done" 46 | // }, 47 | // { 48 | // "id": "step1", 49 | // "description": "step 1", 50 | // "started_at": "2020-12-22T20:26:05.71742797+01:00", 51 | // "done_at": "2020-12-22T20:26:05.717428258+01:00", 52 | // "state": "done", 53 | // "data": 1337, 54 | // "duration": 286 55 | // }, 56 | // { 57 | // "id": "step2", 58 | // "description": "step 2", 59 | // "started_at": "2020-12-22T20:26:05.71742865+01:00", 60 | // "state": "in progress", 61 | // "data": [ 62 | // "hello", 63 | // "world" 64 | // ], 65 | // "duration": 496251 66 | // }, 67 | // { 68 | // "id": "step3" 69 | // }, 70 | // { 71 | // "id": "finish" 72 | // } 73 | // ], 74 | // "created_at": "2020-12-22T20:26:05.717423018+01:00", 75 | // "snapshot": { 76 | // "state": "in progress", 77 | // "doing": "step 2", 78 | // "not_started": 2, 79 | // "in_progress": 1, 80 | // "completed": 2, 81 | // "total": 5, 82 | // "percent": 50, 83 | // "total_duration": 25935, 84 | // "started_at": "2020-12-22T20:26:05.717427484+01:00" 85 | // } 86 | //} 87 | } 88 | 89 | func ExampleProgressSubscribe() { 90 | prog := progress.New() 91 | defer prog.Close() 92 | done := make(chan bool) 93 | ch := prog.Subscribe() 94 | 95 | go func() { 96 | idx := 0 97 | for step := range ch { 98 | if step == nil { 99 | break 100 | } 101 | fmt.Println(idx, step.ID, step.State) 102 | idx++ 103 | } 104 | done <- true 105 | }() 106 | time.Sleep(10 * time.Millisecond) 107 | prog.AddStep("step1").SetDescription("hello") 108 | prog.AddStep("step2") 109 | prog.Get("step1").Start() 110 | prog.Get("step2").Done() 111 | prog.AddStep("step3") 112 | prog.Get("step3").Start() 113 | prog.Get("step1").Done() 114 | prog.AddStep("step4") 115 | prog.Get("step3").Done() 116 | prog.Get("step4").SetAsCurrent() 117 | prog.Get("step4").Done() 118 | // fmt.Println(u.PrettyJSON(prog)) 119 | <-done 120 | 121 | // Output: 122 | // 0 step1 not started 123 | // 1 step1 not started 124 | // 2 step2 not started 125 | // 3 step1 in progress 126 | // 4 step2 done 127 | // 5 step3 not started 128 | // 6 step3 in progress 129 | // 7 step1 done 130 | // 8 step4 not started 131 | // 9 step3 done 132 | // 10 step4 in progress 133 | // 11 step4 done 134 | } 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module moul.io/progress 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/stretchr/testify v1.6.1 7 | github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 8 | moul.io/u v1.20.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8= 10 | github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 14 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 17 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 18 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 19 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw= 21 | github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= 22 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 23 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 24 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 25 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 26 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 27 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 28 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 29 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 30 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 31 | golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= 32 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 33 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 34 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 35 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 36 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 45 | golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag= 46 | golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 47 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 50 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 53 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 56 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | moul.io/u v1.20.0 h1:VSGDnrFDggJeTqxEHB06OrPE8VeJq+xYsgjWTu8VTP4= 58 | moul.io/u v1.20.0/go.mod h1:wbu/e7QOYvmXQW4P3W2494MZU2y9Zh+H1hcr6HBxftE= 59 | -------------------------------------------------------------------------------- /internal/tools/tools_test.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/tailscale/depaware" // required by rules.mk 7 | ) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "this project is not a node.js one, package.json is just used to define some metadata", 3 | "name": "@moul.io/progress", 4 | "version": "0.0.1", 5 | "author": "Manfred Touron (https://manfred.life)", 6 | "contributors": [ 7 | "Manfred Touron (https://manfred.life)" 8 | ], 9 | "license": "(Apache-2.0 OR MIT)", 10 | "scripts": { 11 | "start": "progress", 12 | "install": "make install", 13 | "test": "make test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/moul/progress.git" 18 | }, 19 | "bugs": "https://github.com/moul/progress/issues", 20 | "homepage": "https://moul.io/progress" 21 | } 22 | -------------------------------------------------------------------------------- /progress.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "moul.io/u" 12 | ) 13 | 14 | // Progress is the top-level object of the 'progress' library. 15 | type Progress struct { 16 | Steps []*Step `json:"steps,omitempty"` 17 | CreatedAt time.Time `json:"created_at,omitempty"` 18 | 19 | mainMutex sync.RWMutex 20 | subscribers map[chan *Step]struct{} 21 | } 22 | 23 | type State string 24 | 25 | const ( 26 | StateNotStarted State = "not started" 27 | StateInProgress State = "in progress" 28 | StateDone State = "done" 29 | StateStopped State = "stopped" 30 | ) 31 | 32 | const ( 33 | notStartedProgress = 0.0 34 | defaultStartProgress = 0.5 35 | doneProgress = 1.0 36 | publishTimeout = 1000 * time.Millisecond 37 | // based on the average usage of this library, we can't have a small number like "1" or "2". 38 | // by refactoring the project, we may find a solution to update the locking strategy so we can reduce this number. 39 | defaultSubscriberChanLength = 42 40 | ) 41 | 42 | // New creates and returns a new Progress. 43 | func New() *Progress { 44 | return &Progress{ 45 | CreatedAt: time.Now(), 46 | } 47 | } 48 | 49 | // AddStep creates and returns a new Step with the provided 'id'. 50 | // A non-empty, unique 'id' is required, else it will panic. 51 | func (p *Progress) AddStep(id string) *Step { 52 | step, err := p.SafeAddStep(id) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return step 57 | } 58 | 59 | // SafeAddStep is equivalent to AddStep with but returns error instead of panicking. 60 | func (p *Progress) SafeAddStep(id string) (*Step, error) { 61 | if id == "" { 62 | return nil, ErrStepRequiresID 63 | } 64 | step := &Step{ 65 | ID: id, 66 | State: StateNotStarted, 67 | Progress: notStartedProgress, 68 | parent: p, 69 | } 70 | 71 | p.mainMutex.Lock() 72 | defer p.mainMutex.Unlock() 73 | if p.Steps == nil { 74 | p.Steps = make([]*Step, 0) 75 | } 76 | 77 | for _, step := range p.Steps { 78 | if step.ID == id { 79 | return nil, ErrStepIDShouldBeUnique 80 | } 81 | } 82 | 83 | p.Steps = append(p.Steps, step) 84 | p.publishStep(step) 85 | return step, nil 86 | } 87 | 88 | // publishStep iterates over subscribers and try to append a step. 89 | func (p *Progress) publishStep(step *Step) { 90 | if len(p.subscribers) == 0 { 91 | return 92 | } 93 | 94 | var stepCopyPtr *Step 95 | if step != nil { 96 | stepCopy := *step 97 | stepCopyPtr = &stepCopy 98 | } 99 | 100 | for subscriber := range p.subscribers { 101 | select { 102 | case subscriber <- stepCopyPtr: 103 | case <-time.After(publishTimeout): 104 | // debug: fmt.Println("************** DROP **************") 105 | } 106 | } 107 | } 108 | 109 | // Subscribe registers the provided chan as a target called each time a step is changed. 110 | func (p *Progress) Subscribe() chan *Step { 111 | p.mainMutex.Lock() 112 | subscriber := make(chan *Step, defaultSubscriberChanLength) 113 | if p.subscribers == nil { 114 | p.subscribers = make(map[chan *Step]struct{}) 115 | } 116 | p.subscribers[subscriber] = struct{}{} 117 | p.mainMutex.Unlock() 118 | return subscriber 119 | } 120 | 121 | // Close cleans up the allocated ressources. 122 | func (p *Progress) Close() { 123 | p.closeSubscribers() 124 | } 125 | 126 | func (p *Progress) closeSubscribers() { 127 | for sub := range p.subscribers { 128 | close(sub) 129 | delete(p.subscribers, sub) 130 | } 131 | } 132 | 133 | // Get retrieves a Step by its 'id'. 134 | // A non-empty 'id' is required, else it will panic. 135 | // If 'id' does not match an existing step, nil is returned. 136 | func (p *Progress) Get(id string) *Step { 137 | if id == "" { 138 | panic("progress.Get requires a non-empty ID as argument.") 139 | } 140 | 141 | p.mainMutex.RLock() 142 | defer p.mainMutex.RUnlock() 143 | 144 | for _, step := range p.Steps { 145 | if step.ID == id { 146 | return step 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | 153 | // Snapshot represents info and stats about a progress at a given time. 154 | type Snapshot struct { 155 | State State `json:"state,omitempty"` 156 | Doing string `json:"doing,omitempty"` 157 | NotStarted int `json:"not_started,omitempty"` 158 | InProgress int `json:"in_progress,omitempty"` 159 | Completed int `json:"completed,omitempty"` 160 | Total int `json:"total,omitempty"` 161 | Progress float64 `json:"progress,omitempty"` 162 | TotalDuration time.Duration `json:"total_duration,omitempty"` 163 | StepDuration time.Duration `json:"step_duration,omitempty"` 164 | CompletionEstimate time.Duration `json:"completion_estimate,omitempty"` 165 | DoneAt *time.Time `json:"done_at,omitempty"` 166 | StartedAt *time.Time `json:"started_at,omitempty"` 167 | } 168 | 169 | // Snapshot computes and returns the current stats of the Progress. 170 | func (p *Progress) Snapshot() Snapshot { 171 | p.mainMutex.RLock() 172 | defer p.mainMutex.RUnlock() 173 | if len(p.Steps) == 0 { 174 | return Snapshot{ 175 | State: StateNotStarted, 176 | } 177 | } 178 | 179 | snapshot := Snapshot{ 180 | Total: len(p.Steps), 181 | Progress: 0, 182 | } 183 | 184 | doing := []string{} 185 | for _, step := range p.Steps { 186 | switch step.State { 187 | case StateNotStarted: 188 | snapshot.NotStarted++ 189 | case StateInProgress: 190 | snapshot.InProgress++ 191 | doing = append(doing, step.title()) 192 | case StateDone: 193 | snapshot.Completed++ 194 | case StateStopped: 195 | panic(fmt.Sprintf("step cannot be in stopped state (yet!): %s", u.JSON(step))) 196 | default: 197 | panic(fmt.Sprintf("step is in an unexpected state: %s", u.JSON(step))) 198 | } 199 | 200 | // compute the oldest step.StartedAt 201 | if step.StartedAt != nil { 202 | if snapshot.StartedAt == nil { 203 | snapshot.StartedAt = step.StartedAt 204 | } else if step.StartedAt.Before(*snapshot.StartedAt) { 205 | snapshot.StartedAt = step.StartedAt 206 | } 207 | } 208 | 209 | // compute the most recent step.DoneAt 210 | if step.DoneAt != nil { 211 | if snapshot.DoneAt == nil { 212 | snapshot.DoneAt = step.DoneAt 213 | } else if step.DoneAt.After(*snapshot.DoneAt) { 214 | snapshot.DoneAt = step.DoneAt 215 | } 216 | } 217 | } 218 | 219 | snapshot.Progress = p.Progress() 220 | 221 | // compute top-level aggregates 222 | { 223 | snapshot.Doing = strings.Join(doing, ", ") 224 | var ( 225 | isDone = snapshot.Completed > 0 && snapshot.InProgress == 0 && snapshot.NotStarted == 0 226 | isInProgress = snapshot.Completed < snapshot.Total && snapshot.InProgress > 0 227 | isNotStarted = snapshot.Completed == 0 && snapshot.InProgress == 0 228 | isStopped = snapshot.Completed > 0 && snapshot.InProgress == 0 && snapshot.NotStarted > 0 229 | ) 230 | switch { 231 | case isDone: 232 | snapshot.State = StateDone 233 | if snapshot.Completed != snapshot.Total { 234 | panic(fmt.Sprintf("snapshot has a strange state: %s", u.JSON(snapshot))) 235 | } 236 | snapshot.Progress = 1 // avoid having 0.99999999999 by adding floats together 237 | snapshot.TotalDuration = snapshot.DoneAt.Sub(*snapshot.StartedAt) 238 | case isInProgress: 239 | snapshot.State = StateInProgress 240 | snapshot.DoneAt = nil 241 | snapshot.TotalDuration = time.Since(*snapshot.StartedAt) 242 | case isNotStarted: 243 | snapshot.State = StateNotStarted 244 | snapshot.DoneAt = nil 245 | case isStopped: 246 | snapshot.State = StateStopped 247 | snapshot.DoneAt = nil 248 | snapshot.TotalDuration = time.Since(*snapshot.StartedAt) 249 | default: 250 | panic(fmt.Sprintf("snapshot has a strange state: %s", u.JSON(snapshot))) 251 | } 252 | } 253 | 254 | return snapshot 255 | } 256 | 257 | // MarshalJSON is a custom JSON marshaler that automatically computes and append the current snapshot. 258 | func (p *Progress) MarshalJSON() ([]byte, error) { 259 | type alias Progress 260 | type enriched struct { 261 | *alias 262 | Snapshot Snapshot `json:"snapshot"` 263 | } 264 | return json.Marshal(&enriched{ 265 | alias: (*alias)(p), 266 | Snapshot: p.Snapshot(), 267 | }) 268 | } 269 | 270 | // Progress returns the current completion rate, it's a faster alternative to Progress.Snapshot().Progress. 271 | // The returned value is between 0.0 and 1.0. 272 | func (p *Progress) Progress() float64 { 273 | total := len(p.Steps) 274 | progress := notStartedProgress 275 | for _, step := range p.Steps { 276 | switch step.State { 277 | case StateNotStarted: 278 | // noop 279 | case StateInProgress: 280 | // in-progress task count as partially done 281 | progress += (step.Progress / float64(total)) 282 | // FIXME: support per-task progress 283 | case StateDone: 284 | progress += (doneProgress / float64(total)) 285 | case StateStopped: 286 | panic(fmt.Sprintf("step cannot be in stopped state (yet!): %s", u.JSON(step))) 287 | default: 288 | panic(fmt.Sprintf("step is in an unexpected state: %s", u.JSON(step))) 289 | } 290 | } 291 | return progress 292 | } 293 | 294 | func (p *Progress) isDone() bool { 295 | if len(p.Steps) == 0 { 296 | return false 297 | } 298 | for _, step := range p.Steps { 299 | if step.State != StateDone { 300 | return false 301 | } 302 | } 303 | return true 304 | } 305 | 306 | // Step represents a progress step. 307 | // It always have an 'id' and can be customized using helpers. 308 | type Step struct { 309 | ID string `json:"id,omitempty"` 310 | Description string `json:"description,omitempty"` 311 | StartedAt *time.Time `json:"started_at,omitempty"` 312 | DoneAt *time.Time `json:"done_at,omitempty"` 313 | State State `json:"state,omitempty"` 314 | Data interface{} `json:"data,omitempty"` 315 | Progress float64 `json:"progress,omitempty"` 316 | 317 | parent *Progress 318 | } 319 | 320 | // SetProgress sets the current step progress rate. 321 | // It may also update the current Step.State depending on the passed progress. 322 | // The value should be something between 0.0 and 1.0. 323 | func (s *Step) SetProgress(progress float64) *Step { 324 | if progress == doneProgress { 325 | return s.Done() 326 | } 327 | 328 | s.parent.mainMutex.Lock() 329 | defer s.parent.mainMutex.Unlock() 330 | s.Progress = progress 331 | if progress == notStartedProgress { 332 | s.State = StateNotStarted 333 | } else { 334 | s.State = StateInProgress 335 | if s.StartedAt == nil { 336 | now := time.Now() 337 | s.StartedAt = &now 338 | } 339 | } 340 | s.parent.publishStep(s) 341 | return s 342 | } 343 | 344 | // SetDescription sets a custom step description. 345 | // It returns itself (*Step) for chaining. 346 | func (s *Step) SetDescription(desc string) *Step { 347 | s.Description = desc 348 | s.parent.publishStep(s) 349 | return s 350 | } 351 | 352 | // SetData sets a custom step data. 353 | // It returns itself (*Step) for chaining. 354 | func (s *Step) SetData(data interface{}) *Step { 355 | s.Data = data 356 | s.parent.publishStep(s) 357 | return s 358 | } 359 | 360 | // Start marks a step as started. 361 | // If a step was already InProgress or Done, it panics. 362 | func (s *Step) Start() *Step { 363 | s.parent.mainMutex.Lock() 364 | defer s.parent.mainMutex.Unlock() 365 | if s.State == StateInProgress { 366 | panic("cannot Step.Start() an already in-progress step.") 367 | } 368 | if s.State == StateDone { 369 | panic("cannot Step.Start() an already done step.") 370 | } 371 | s.State = StateInProgress 372 | now := time.Now() 373 | s.StartedAt = &now 374 | s.Progress = defaultStartProgress 375 | s.parent.publishStep(s) 376 | return s 377 | } 378 | 379 | // SetAsCurrent stops all in-progress steps and start this one. 380 | func (s *Step) SetAsCurrent() *Step { 381 | s.parent.mainMutex.Lock() 382 | defer s.parent.mainMutex.Unlock() 383 | if s.State == StateInProgress { 384 | panic("cannot Step.Start() an already in-progress step.") 385 | } 386 | if s.State == StateDone { 387 | panic("cannot Step.Start() an already done step.") 388 | } 389 | now := time.Now() 390 | for _, step := range s.parent.Steps { 391 | if step.State == StateInProgress { 392 | step.State = StateDone 393 | step.DoneAt = &now 394 | s.parent.publishStep(step) 395 | } 396 | } 397 | s.Progress = defaultStartProgress 398 | s.State = StateInProgress 399 | s.StartedAt = &now 400 | s.parent.publishStep(s) 401 | return s 402 | } 403 | 404 | // Done marks a step as done. 405 | // If the step was already done, it panics. 406 | func (s *Step) Done() *Step { 407 | s.parent.mainMutex.Lock() 408 | defer s.parent.mainMutex.Unlock() 409 | if s.State == StateDone { 410 | panic("cannot Step.Done() an already done step.") 411 | } 412 | s.State = StateDone 413 | now := time.Now() 414 | if s.StartedAt == nil { 415 | s.StartedAt = &now 416 | } 417 | s.DoneAt = &now 418 | s.parent.publishStep(s) 419 | if s.parent.isDone() { 420 | s.parent.closeSubscribers() 421 | } 422 | return s 423 | } 424 | 425 | // MarshalJSON is a custom JSON marshaler that automatically computes and append some runtime metadata. 426 | func (s *Step) MarshalJSON() ([]byte, error) { 427 | type alias Step 428 | type enriched struct { 429 | alias 430 | Duration time.Duration `json:"duration,omitempty"` 431 | } 432 | return json.Marshal(&enriched{ 433 | alias: (alias)(*s), 434 | Duration: s.Duration(), 435 | }) 436 | } 437 | 438 | // Duration computes the step duration. 439 | func (s *Step) Duration() time.Duration { 440 | var ret time.Duration 441 | switch s.State { 442 | case StateInProgress: 443 | ret = time.Since(*s.StartedAt) 444 | case StateDone: 445 | ret = s.DoneAt.Sub(*s.StartedAt) 446 | case StateNotStarted: 447 | // noop 448 | case StateStopped: 449 | panic(fmt.Sprintf("step cannot be in stopped state (yet!): %s", u.JSON(s))) 450 | default: 451 | // noop 452 | } 453 | return ret 454 | } 455 | 456 | func (s *Step) title() string { 457 | if s.Description != "" { 458 | return s.Description 459 | } 460 | return s.ID 461 | } 462 | 463 | var ( 464 | ErrStepRequiresID = errors.New("progress.AddStep requires a non-empty ID as argument") 465 | ErrStepIDShouldBeUnique = errors.New("progress.AddStep requires a unique ID as argument") 466 | ) 467 | -------------------------------------------------------------------------------- /progress_test.go: -------------------------------------------------------------------------------- 1 | package progress_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | "moul.io/progress" 10 | "moul.io/u" 11 | ) 12 | 13 | func TestFlow(t *testing.T) { 14 | // initialize a new progress 15 | prog := progress.New() 16 | { 17 | require.NotEmpty(t, prog) 18 | require.Empty(t, prog.Steps) 19 | require.NotZero(t, prog.CreatedAt) 20 | require.True(t, prog.CreatedAt.Before(time.Now())) 21 | snapshot := prog.Snapshot() 22 | require.Equal(t, progress.StateNotStarted, snapshot.State) 23 | require.Equal(t, 0, snapshot.Total) 24 | require.Equal(t, 0, snapshot.Completed) 25 | require.Equal(t, 0, snapshot.NotStarted) 26 | require.Equal(t, 0, snapshot.InProgress) 27 | require.Equal(t, float64(0), snapshot.Progress) 28 | require.Equal(t, snapshot.Progress, prog.Progress()) 29 | require.Nil(t, prog.Get("step1")) 30 | } 31 | 32 | // add a first step 33 | { 34 | prog.AddStep("step1") 35 | require.NotEmpty(t, prog.Steps) 36 | require.Len(t, prog.Steps, 1) 37 | require.True(t, prog.CreatedAt.Before(time.Now())) 38 | 39 | snapshot := prog.Snapshot() 40 | require.Equal(t, "", snapshot.Doing) 41 | require.Equal(t, progress.StateNotStarted, snapshot.State) 42 | require.Equal(t, 1, snapshot.Total) 43 | require.Equal(t, 0, snapshot.Completed) 44 | require.Equal(t, 1, snapshot.NotStarted) 45 | require.Equal(t, 0, snapshot.InProgress) 46 | require.NotNil(t, prog.Get("step1")) 47 | require.Equal(t, float64(0), snapshot.Progress) 48 | require.Equal(t, snapshot.Progress, prog.Progress()) 49 | 50 | step1 := prog.Get("step1") 51 | require.NotNil(t, step1) 52 | require.Equal(t, step1.State, progress.StateNotStarted) 53 | require.Empty(t, step1.Description) 54 | step1.SetDescription("hello") 55 | require.Equal(t, "hello", step1.Description) 56 | } 57 | 58 | // add a second step 59 | { 60 | prog.AddStep("step2") 61 | require.NotEmpty(t, prog.Steps) 62 | require.Len(t, prog.Steps, 2) 63 | 64 | snapshot := prog.Snapshot() 65 | require.Equal(t, "", snapshot.Doing) 66 | require.Equal(t, progress.StateNotStarted, snapshot.State) 67 | require.Equal(t, 2, snapshot.Total) 68 | require.Equal(t, 0, snapshot.Completed) 69 | require.Equal(t, 2, snapshot.NotStarted) 70 | require.Equal(t, 0, snapshot.InProgress) 71 | require.NotNil(t, prog.Get("step2")) 72 | require.Equal(t, float64(0), snapshot.Progress) 73 | require.Equal(t, snapshot.Progress, prog.Progress()) 74 | } 75 | 76 | // start the first step 77 | { 78 | step1 := prog.Get("step1") 79 | step1.Start() 80 | 81 | require.NotEmpty(t, prog.Steps) 82 | require.Len(t, prog.Steps, 2) 83 | 84 | snapshot := prog.Snapshot() 85 | require.Equal(t, "hello", snapshot.Doing) 86 | require.Equal(t, progress.StateInProgress, snapshot.State) 87 | require.Equal(t, 2, snapshot.Total) 88 | require.Equal(t, 0, snapshot.Completed) 89 | require.Equal(t, 1, snapshot.NotStarted) 90 | require.Equal(t, 1, snapshot.InProgress) 91 | require.Equal(t, float64(0.25), snapshot.Progress) 92 | require.Equal(t, snapshot.Progress, prog.Progress()) 93 | } 94 | 95 | // mark the first step as done 96 | { 97 | time.Sleep(200 * time.Millisecond) 98 | step1 := prog.Get("step1") 99 | step1.Done() 100 | require.Equal(t, progress.StateDone, step1.State) 101 | 102 | require.NotEmpty(t, prog.Steps) 103 | require.Len(t, prog.Steps, 2) 104 | 105 | snapshot := prog.Snapshot() 106 | require.Equal(t, "", snapshot.Doing) 107 | require.Equal(t, progress.StateStopped, snapshot.State) 108 | require.Equal(t, 2, snapshot.Total) 109 | require.Equal(t, 1, snapshot.Completed) 110 | require.Equal(t, 1, snapshot.NotStarted) 111 | require.Equal(t, 0, snapshot.InProgress) 112 | require.Equal(t, float64(0.5), snapshot.Progress) 113 | require.Equal(t, snapshot.Progress, prog.Progress()) 114 | } 115 | 116 | // mark the second step as done without starting it first 117 | { 118 | step2 := prog.Get("step2") 119 | step2.Done() 120 | require.Equal(t, progress.StateDone, step2.State) 121 | 122 | require.NotEmpty(t, prog.Steps) 123 | require.Len(t, prog.Steps, 2) 124 | 125 | snapshot := prog.Snapshot() 126 | require.Equal(t, "", snapshot.Doing) 127 | require.Equal(t, progress.StateDone, snapshot.State) 128 | require.Equal(t, 2, snapshot.Total) 129 | require.Equal(t, 2, snapshot.Completed) 130 | require.Equal(t, 0, snapshot.NotStarted) 131 | require.Equal(t, 0, snapshot.InProgress) 132 | require.Equal(t, float64(1), snapshot.Progress) 133 | require.Equal(t, snapshot.Progress, prog.Progress()) 134 | } 135 | 136 | // add a third step 137 | { 138 | prog.AddStep("step3") 139 | require.NotEmpty(t, prog.Steps) 140 | require.Len(t, prog.Steps, 3) 141 | require.NotNil(t, prog.Get("step3")) 142 | 143 | snapshot := prog.Snapshot() 144 | require.Equal(t, "", snapshot.Doing) 145 | require.Equal(t, progress.StateStopped, snapshot.State) 146 | require.Equal(t, 3, snapshot.Total) 147 | require.Equal(t, 2, snapshot.Completed) 148 | require.Equal(t, 1, snapshot.NotStarted) 149 | require.Equal(t, 0, snapshot.InProgress) 150 | require.Equal(t, 66, int(snapshot.Progress*100)) 151 | require.Equal(t, snapshot.Progress, prog.Progress()) 152 | } 153 | 154 | // add a fourth step 155 | { 156 | prog.AddStep("step4") 157 | require.NotEmpty(t, prog.Steps) 158 | require.Len(t, prog.Steps, 4) 159 | require.NotNil(t, prog.Get("step4")) 160 | 161 | snapshot := prog.Snapshot() 162 | require.Equal(t, "", snapshot.Doing) 163 | require.Equal(t, progress.StateStopped, snapshot.State) 164 | require.Equal(t, 4, snapshot.Total) 165 | require.Equal(t, 2, snapshot.Completed) 166 | require.Equal(t, 2, snapshot.NotStarted) 167 | require.Equal(t, 0, snapshot.InProgress) 168 | require.Equal(t, float64(0.5), snapshot.Progress) 169 | require.Equal(t, snapshot.Progress, prog.Progress()) 170 | } 171 | 172 | // start step3 and step4 at the same time 173 | { 174 | step3 := prog.Get("step3") 175 | step4 := prog.Get("step4") 176 | step3.Start() 177 | step4.Start() 178 | 179 | snapshot := prog.Snapshot() 180 | require.Equal(t, "step3, step4", snapshot.Doing) 181 | require.Equal(t, progress.StateInProgress, snapshot.State) 182 | require.Equal(t, 4, snapshot.Total) 183 | require.Equal(t, 2, snapshot.Completed) 184 | require.Equal(t, 0, snapshot.NotStarted) 185 | require.Equal(t, 2, snapshot.InProgress) 186 | require.Equal(t, float64(0.75), snapshot.Progress) 187 | require.Equal(t, snapshot.Progress, prog.Progress()) 188 | } 189 | 190 | // mark step3 and step4 as done at the same time 191 | { 192 | time.Sleep(200 * time.Millisecond) 193 | step1 := prog.Get("step1") 194 | step2 := prog.Get("step2") 195 | step3 := prog.Get("step3") 196 | step4 := prog.Get("step4") 197 | step3.Done() 198 | step4.Done() 199 | 200 | snapshot := prog.Snapshot() 201 | require.Equal(t, "", snapshot.Doing) 202 | require.Equal(t, progress.StateDone, snapshot.State) 203 | require.Equal(t, 4, snapshot.Total) 204 | require.Equal(t, 4, snapshot.Completed) 205 | require.Equal(t, 0, snapshot.NotStarted) 206 | require.Equal(t, 0, snapshot.InProgress) 207 | require.Equal(t, float64(1), snapshot.Progress) 208 | require.Equal(t, snapshot.Progress, prog.Progress()) 209 | 210 | require.True(t, step1.Duration() > 200*time.Millisecond && step1.Duration() < 400*time.Millisecond) 211 | require.Zero(t, step2.Duration()) 212 | require.True(t, step3.Duration() > 200*time.Millisecond && step3.Duration() < 400*time.Millisecond) 213 | require.True(t, step4.Duration() > 200*time.Millisecond && step4.Duration() < 400*time.Millisecond) 214 | require.True(t, snapshot.TotalDuration > 400*time.Millisecond && snapshot.TotalDuration < 600*time.Millisecond) 215 | } 216 | 217 | // create a new step and use SetProgress instead of Start 218 | { 219 | prog.AddStep("step5") 220 | require.NotEmpty(t, prog.Steps) 221 | require.Len(t, prog.Steps, 5) 222 | require.NotNil(t, prog.Get("step5")) 223 | 224 | snapshot := prog.Snapshot() 225 | require.Equal(t, "", snapshot.Doing) 226 | require.Equal(t, progress.StateStopped, snapshot.State) 227 | require.Equal(t, 5, snapshot.Total) 228 | require.Equal(t, 4, snapshot.Completed) 229 | require.Equal(t, 1, snapshot.NotStarted) 230 | require.Equal(t, 0, snapshot.InProgress) 231 | require.Equal(t, float64(0.8), snapshot.Progress) 232 | require.Equal(t, snapshot.Progress, prog.Progress()) 233 | 234 | prog.Get("step5").SetProgress(0) 235 | snapshot = prog.Snapshot() 236 | require.Equal(t, "", snapshot.Doing) 237 | require.Equal(t, progress.StateStopped, snapshot.State) 238 | require.Equal(t, 5, snapshot.Total) 239 | require.Equal(t, 4, snapshot.Completed) 240 | require.Equal(t, 1, snapshot.NotStarted) 241 | require.Equal(t, 0, snapshot.InProgress) 242 | require.Equal(t, float64(0.8), snapshot.Progress) 243 | require.Equal(t, snapshot.Progress, prog.Progress()) 244 | 245 | prog.Get("step5").SetProgress(0.2) 246 | snapshot = prog.Snapshot() 247 | require.Equal(t, "step5", snapshot.Doing) 248 | require.Equal(t, progress.StateInProgress, snapshot.State) 249 | require.Equal(t, 5, snapshot.Total) 250 | require.Equal(t, 4, snapshot.Completed) 251 | require.Equal(t, 0, snapshot.NotStarted) 252 | require.Equal(t, 1, snapshot.InProgress) 253 | require.Equal(t, 84, int(snapshot.Progress*100)) 254 | require.Equal(t, snapshot.Progress, prog.Progress()) 255 | 256 | prog.Get("step5").SetProgress(0.8) 257 | snapshot = prog.Snapshot() 258 | require.Equal(t, "step5", snapshot.Doing) 259 | require.Equal(t, progress.StateInProgress, snapshot.State) 260 | require.Equal(t, 5, snapshot.Total) 261 | require.Equal(t, 4, snapshot.Completed) 262 | require.Equal(t, 0, snapshot.NotStarted) 263 | require.Equal(t, 1, snapshot.InProgress) 264 | require.Equal(t, 96, int(snapshot.Progress*100)) 265 | require.Equal(t, snapshot.Progress, prog.Progress()) 266 | 267 | prog.Get("step5").SetProgress(1.0) 268 | snapshot = prog.Snapshot() 269 | require.Equal(t, "", snapshot.Doing) 270 | require.Equal(t, progress.StateDone, snapshot.State) 271 | require.Equal(t, 5, snapshot.Total) 272 | require.Equal(t, 5, snapshot.Completed) 273 | require.Equal(t, 0, snapshot.NotStarted) 274 | require.Equal(t, 0, snapshot.InProgress) 275 | require.Equal(t, 1.0, snapshot.Progress) 276 | require.Equal(t, snapshot.Progress, prog.Progress()) 277 | } 278 | 279 | // create 3 new steps to test the Step.SetAsCurrent() helper 280 | { 281 | prog.AddStep("step10") 282 | prog.AddStep("step11") 283 | prog.AddStep("step12") 284 | require.Equal(t, "", prog.Snapshot().Doing) 285 | prog.Get("step11").SetAsCurrent() 286 | require.Equal(t, "step11", prog.Snapshot().Doing) 287 | prog.Get("step10").SetAsCurrent() 288 | require.Equal(t, "step10", prog.Snapshot().Doing) 289 | prog.Get("step12").SetAsCurrent() 290 | require.Equal(t, "step12", prog.Snapshot().Doing) 291 | prog.Get("step12").Done() 292 | require.Equal(t, "", prog.Snapshot().Doing) 293 | } 294 | 295 | // debug 296 | // fmt.Println(u.PrettyJSON(prog)) 297 | } 298 | 299 | func TestSubscribe(t *testing.T) { 300 | prog := progress.New() 301 | defer prog.Close() 302 | done := make(chan bool) 303 | ch := prog.Subscribe() 304 | 305 | seen := 0 306 | go func() { 307 | for step := range ch { 308 | _ = step 309 | seen++ 310 | } 311 | done <- true 312 | }() 313 | 314 | prog.AddStep("step1").SetDescription("hello") 315 | prog.AddStep("step2") 316 | prog.Get("step1").Start() 317 | prog.Get("step2").Done() 318 | prog.AddStep("step3") 319 | prog.Get("step3").Start() 320 | prog.Get("step1").Done() 321 | prog.Get("step3").Done() 322 | // fmt.Println(u.PrettyJSON(prog)) 323 | 324 | <-done 325 | require.Equal(t, 9, seen) 326 | } 327 | 328 | func TestSubscribe_withConcurrency(t *testing.T) { 329 | prog := progress.New() 330 | defer prog.Close() 331 | done := make(chan bool) 332 | ch := prog.Subscribe() 333 | 334 | seen := 0 335 | go func() { 336 | for step := range ch { 337 | _ = fmt.Sprintf("step: %v", step) 338 | // get snapshot which is a command that locks the prog object 339 | snapshot := prog.Snapshot() 340 | _ = fmt.Sprintf("snapshot: %v", snapshot) 341 | if step == nil { 342 | break 343 | } 344 | seen++ 345 | } 346 | done <- true 347 | }() 348 | 349 | prog.AddStep("step1").SetDescription("hello") 350 | prog.AddStep("step2") 351 | prog.AddStep("step3") 352 | prog.AddStep("step4") 353 | prog.AddStep("step5") 354 | prog.AddStep("step6") 355 | prog.AddStep("step7") 356 | prog.AddStep("step8") 357 | prog.AddStep("step9") 358 | prog.AddStep("step10") 359 | prog.Get("step1").Start() 360 | prog.Get("step2").Done() 361 | prog.Get("step3").Done() 362 | prog.Get("step4").SetAsCurrent() 363 | prog.Get("step5").SetAsCurrent() 364 | prog.Get("step6").Start() 365 | prog.Get("step7").Start() 366 | prog.Get("step8").SetAsCurrent() 367 | prog.Get("step9").SetAsCurrent() 368 | prog.Get("step10").Start() 369 | prog.AddStep("step11") 370 | prog.Get("step11").Start() 371 | prog.Get("step11").Done() 372 | prog.Get("step10").Done() 373 | prog.Get("step9").Done() 374 | _ = fmt.Sprintf("result: %v", u.PrettyJSON(prog)) 375 | 376 | <-done 377 | // require.Equal(t, 9, seen) 378 | require.True(t, seen > 1) 379 | } 380 | 381 | func TestClose(t *testing.T) { 382 | prog := progress.New() 383 | prog.Close() 384 | prog.Close() 385 | require.True(t, true) // should not fail before this line 386 | } 387 | 388 | func TestSubcribe_closeReopen(t *testing.T) { 389 | prog := progress.New() 390 | defer prog.Close() 391 | 392 | // add a first step, start it, done it; then, the chan should be closed 393 | ch1 := prog.Subscribe() 394 | prog.AddStep("step1") 395 | require.NotNil(t, <-ch1) 396 | prog.Get("step1").Start() 397 | require.NotNil(t, <-ch1) 398 | prog.Get("step1").Done() 399 | require.NotNil(t, <-ch1) 400 | require.Nil(t, <-ch1) 401 | 402 | // add a new step, the previous chan should still be closed 403 | prog.AddStep("step2") 404 | require.Nil(t, <-ch1) 405 | prog.Get("step2").Start() 406 | require.Nil(t, <-ch1) 407 | prog.Get("step2").Done() 408 | require.Nil(t, <-ch1) 409 | 410 | // start a new subscriber, add a new step, only the new subcriber will get the info 411 | ch2 := prog.Subscribe() 412 | prog.AddStep("step3") 413 | require.NotNil(t, <-ch2) 414 | prog.Get("step3").Start() 415 | require.NotNil(t, <-ch2) 416 | prog.Get("step3").Done() 417 | require.NotNil(t, <-ch2) 418 | require.Nil(t, <-ch2) 419 | require.Nil(t, <-ch1) 420 | } 421 | -------------------------------------------------------------------------------- /rules.mk: -------------------------------------------------------------------------------- 1 | # +--------------------------------------------------------------+ 2 | # | * * * moul.io/rules.mk | 3 | # +--------------------------------------------------------------+ 4 | # | | 5 | # | ++ ______________________________________ | 6 | # | ++++ / \ | 7 | # | ++++ | | | 8 | # | ++++++++++ | https://moul.io/rules.mk is a set | | 9 | # | +++ | | of common Makefile rules that can | | 10 | # | ++ | | be configured from the Makefile | | 11 | # | + -== ==| | or with environment variables. | | 12 | # | ( <*> <*> | | | 13 | # | | | /| Manfred Touron | | 14 | # | | _) / | manfred.life | | 15 | # | | +++ / \______________________________________/ | 16 | # | \ =+ / | 17 | # | \ + | 18 | # | |\++++++ | 19 | # | | ++++ ||// | 20 | # | ___| |___ _||/__ __| 21 | # | / --- \ \| ||| __ _ ___ __ __/ /| 22 | # |/ | | \ \ / / ' \/ _ \/ // / / | 23 | # || | | | | | /_/_/_/\___/\_,_/_/ | 24 | # +--------------------------------------------------------------+ 25 | 26 | .PHONY: _default_entrypoint 27 | _default_entrypoint: help 28 | 29 | ## 30 | ## Common helpers 31 | ## 32 | 33 | rwildcard = $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d)) 34 | check-program = $(foreach exec,$(1),$(if $(shell PATH="$(PATH)" which $(exec)),,$(error "No $(exec) in PATH"))) 35 | my-filter-out = $(foreach v,$(2),$(if $(findstring $(1),$(v)),,$(v))) 36 | novendor = $(call my-filter-out,vendor/,$(1)) 37 | 38 | ## 39 | ## rules.mk 40 | ## 41 | ifneq ($(wildcard rules.mk),) 42 | .PHONY: rulesmk.bumpdeps 43 | rulesmk.bumpdeps: 44 | wget -O rules.mk https://raw.githubusercontent.com/moul/rules.mk/master/rules.mk 45 | BUMPDEPS_STEPS += rulesmk.bumpdeps 46 | endif 47 | 48 | ## 49 | ## Maintainer 50 | ## 51 | 52 | ifneq ($(wildcard .git/HEAD),) 53 | .PHONY: generate.authors 54 | generate.authors: AUTHORS 55 | AUTHORS: .git/ 56 | echo "# This file lists all individuals having contributed content to the repository." > AUTHORS 57 | echo "# For how it is generated, see 'https://github.com/moul/rules.mk'" >> AUTHORS 58 | echo >> AUTHORS 59 | git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf >> AUTHORS 60 | GENERATE_STEPS += generate.authors 61 | endif 62 | 63 | ## 64 | ## Golang 65 | ## 66 | 67 | ifndef GOPKG 68 | ifneq ($(wildcard go.mod),) 69 | GOPKG = $(shell sed '/module/!d;s/^omdule\ //' go.mod) 70 | endif 71 | endif 72 | ifdef GOPKG 73 | GO ?= go 74 | GOPATH ?= $(HOME)/go 75 | GO_INSTALL_OPTS ?= 76 | GO_TEST_OPTS ?= -test.timeout=30s 77 | GOMOD_DIRS ?= $(sort $(call novendor,$(dir $(call rwildcard,*,*/go.mod go.mod)))) 78 | GOCOVERAGE_FILE ?= ./coverage.txt 79 | GOTESTJSON_FILE ?= ./go-test.json 80 | GOBUILDLOG_FILE ?= ./go-build.log 81 | GOINSTALLLOG_FILE ?= ./go-install.log 82 | 83 | ifdef GOBINS 84 | .PHONY: go.install 85 | go.install: 86 | ifeq ($(CI),true) 87 | @rm -f /tmp/goinstall.log 88 | @set -e; for dir in $(GOBINS); do ( set -xe; \ 89 | cd $$dir; \ 90 | $(GO) install -v $(GO_INSTALL_OPTS) .; \ 91 | ); done 2>&1 | tee $(GOINSTALLLOG_FILE) 92 | 93 | else 94 | @set -e; for dir in $(GOBINS); do ( set -xe; \ 95 | cd $$dir; \ 96 | $(GO) install $(GO_INSTALL_OPTS) .; \ 97 | ); done 98 | endif 99 | INSTALL_STEPS += go.install 100 | 101 | .PHONY: go.release 102 | go.release: 103 | $(call check-program, goreleaser) 104 | goreleaser --snapshot --skip-publish --rm-dist 105 | @echo -n "Do you want to release? [y/N] " && read ans && \ 106 | if [ $${ans:-N} = y ]; then set -xe; goreleaser --rm-dist; fi 107 | RELEASE_STEPS += go.release 108 | endif 109 | 110 | .PHONY: go.unittest 111 | go.unittest: 112 | ifeq ($(CI),true) 113 | @echo "mode: atomic" > /tmp/gocoverage 114 | @rm -f $(GOTESTJSON_FILE) 115 | @set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -euf pipefail; \ 116 | cd $$dir; \ 117 | (($(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race -json && touch $@.ok) | tee -a $(GOTESTJSON_FILE) 3>&1 1>&2 2>&3 | tee -a $(GOBUILDLOG_FILE); \ 118 | ); \ 119 | rm $@.ok 2>/dev/null || exit 1; \ 120 | if [ -f /tmp/profile.out ]; then \ 121 | cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \ 122 | rm -f /tmp/profile.out; \ 123 | fi)); done 124 | @mv /tmp/gocoverage $(GOCOVERAGE_FILE) 125 | else 126 | @echo "mode: atomic" > /tmp/gocoverage 127 | @set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -xe; \ 128 | cd $$dir; \ 129 | $(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race); \ 130 | if [ -f /tmp/profile.out ]; then \ 131 | cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \ 132 | rm -f /tmp/profile.out; \ 133 | fi); done 134 | @mv /tmp/gocoverage $(GOCOVERAGE_FILE) 135 | endif 136 | 137 | .PHONY: go.checkdoc 138 | go.checkdoc: 139 | go doc $(first $(GOMOD_DIRS)) 140 | 141 | .PHONY: go.coverfunc 142 | go.coverfunc: go.unittest 143 | go tool cover -func=$(GOCOVERAGE_FILE) | grep -v .pb.go: | grep -v .pb.gw.go: 144 | 145 | .PHONY: go.lint 146 | go.lint: 147 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 148 | cd $$dir; \ 149 | golangci-lint run --verbose ./...; \ 150 | ); done 151 | 152 | .PHONY: go.tidy 153 | go.tidy: 154 | @# tidy dirs with go.mod files 155 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 156 | cd $$dir; \ 157 | $(GO) mod tidy; \ 158 | ); done 159 | 160 | .PHONY: go.depaware-update 161 | go.depaware-update: go.tidy 162 | @# gen depaware for bins 163 | @set -e; for dir in $(GOBINS); do ( set -xe; \ 164 | cd $$dir; \ 165 | $(GO) run github.com/tailscale/depaware --update .; \ 166 | ); done 167 | @# tidy unused depaware deps if not in a tools_test.go file 168 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 169 | cd $$dir; \ 170 | $(GO) mod tidy; \ 171 | ); done 172 | 173 | .PHONY: go.depaware-check 174 | go.depaware-check: go.tidy 175 | @# gen depaware for bins 176 | @set -e; for dir in $(GOBINS); do ( set -xe; \ 177 | cd $$dir; \ 178 | $(GO) run github.com/tailscale/depaware --check .; \ 179 | ); done 180 | 181 | 182 | .PHONY: go.build 183 | go.build: 184 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 185 | cd $$dir; \ 186 | $(GO) build ./...; \ 187 | ); done 188 | 189 | .PHONY: go.bump-deps 190 | go.bumpdeps: 191 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 192 | cd $$dir; \ 193 | $(GO) get -u ./...; \ 194 | ); done 195 | 196 | .PHONY: go.bump-deps 197 | go.fmt: 198 | @set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \ 199 | cd $$dir; \ 200 | $(GO) run golang.org/x/tools/cmd/goimports -w `go list -f '{{.Dir}}' ./...` \ 201 | ); done 202 | 203 | VERIFY_STEPS += go.depaware-check 204 | BUILD_STEPS += go.build 205 | BUMPDEPS_STEPS += go.bumpdeps go.depaware-update 206 | TIDY_STEPS += go.tidy 207 | LINT_STEPS += go.lint 208 | UNITTEST_STEPS += go.unittest 209 | FMT_STEPS += go.fmt 210 | 211 | # FIXME: disabled, because currently slow 212 | # new rule that is manually run sometimes, i.e. `make pre-release` or `make maintenance`. 213 | # alternative: run it each time the go.mod is changed 214 | #GENERATE_STEPS += go.depaware-update 215 | endif 216 | 217 | ## 218 | ## Gitattributes 219 | ## 220 | 221 | ifneq ($(wildcard .gitattributes),) 222 | .PHONY: _linguist-ignored 223 | _linguist-kept: 224 | @git check-attr linguist-vendored $(shell git check-attr linguist-generated $(shell find . -type f | grep -v .git/) | grep unspecified | cut -d: -f1) | grep unspecified | cut -d: -f1 | sort 225 | 226 | .PHONY: _linguist-kept 227 | _linguist-ignored: 228 | @git check-attr linguist-vendored linguist-ignored `find . -not -path './.git/*' -type f` | grep '\ set$$' | cut -d: -f1 | sort -u 229 | endif 230 | 231 | ## 232 | ## Node 233 | ## 234 | 235 | ifndef NPM_PACKAGES 236 | ifneq ($(wildcard package.json),) 237 | NPM_PACKAGES = . 238 | endif 239 | endif 240 | ifdef NPM_PACKAGES 241 | .PHONY: npm.publish 242 | npm.publish: 243 | @echo -n "Do you want to npm publish? [y/N] " && read ans && \ 244 | @if [ $${ans:-N} = y ]; then \ 245 | set -e; for dir in $(NPM_PACKAGES); do ( set -xe; \ 246 | cd $$dir; \ 247 | npm publish --access=public; \ 248 | ); done; \ 249 | fi 250 | RELEASE_STEPS += npm.publish 251 | endif 252 | 253 | ## 254 | ## Docker 255 | ## 256 | 257 | docker_build = docker build \ 258 | --build-arg VCS_REF=`git rev-parse --short HEAD` \ 259 | --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ 260 | --build-arg VERSION=`git describe --tags --always` \ 261 | -t "$2" -f "$1" "$(dir $1)" 262 | 263 | ifndef DOCKERFILE_PATH 264 | DOCKERFILE_PATH = ./Dockerfile 265 | endif 266 | ifndef DOCKER_IMAGE 267 | ifneq ($(wildcard Dockerfile),) 268 | DOCKER_IMAGE = $(notdir $(PWD)) 269 | endif 270 | endif 271 | ifdef DOCKER_IMAGE 272 | ifneq ($(DOCKER_IMAGE),none) 273 | .PHONY: docker.build 274 | docker.build: 275 | $(call check-program, docker) 276 | $(call docker_build,$(DOCKERFILE_PATH),$(DOCKER_IMAGE)) 277 | 278 | BUILD_STEPS += docker.build 279 | endif 280 | endif 281 | 282 | ## 283 | ## Common 284 | ## 285 | 286 | TEST_STEPS += $(UNITTEST_STEPS) 287 | TEST_STEPS += $(LINT_STEPS) 288 | TEST_STEPS += $(TIDY_STEPS) 289 | 290 | ifneq ($(strip $(TEST_STEPS)),) 291 | .PHONY: test 292 | test: $(PRE_TEST_STEPS) $(TEST_STEPS) 293 | endif 294 | 295 | ifdef INSTALL_STEPS 296 | .PHONY: install 297 | install: $(PRE_INSTALL_STEPS) $(INSTALL_STEPS) 298 | endif 299 | 300 | ifdef UNITTEST_STEPS 301 | .PHONY: unittest 302 | unittest: $(PRE_UNITTEST_STEPS) $(UNITTEST_STEPS) 303 | endif 304 | 305 | ifdef LINT_STEPS 306 | .PHONY: lint 307 | lint: $(PRE_LINT_STEPS) $(FMT_STEPS) $(LINT_STEPS) 308 | endif 309 | 310 | ifdef TIDY_STEPS 311 | .PHONY: tidy 312 | tidy: $(PRE_TIDY_STEPS) $(TIDY_STEPS) 313 | endif 314 | 315 | ifdef BUILD_STEPS 316 | .PHONY: build 317 | build: $(PRE_BUILD_STEPS) $(BUILD_STEPS) 318 | endif 319 | 320 | ifdef VERIFY_STEPS 321 | .PHONY: verify 322 | verify: $(PRE_VERIFY_STEPS) $(VERIFY_STEPS) 323 | endif 324 | 325 | ifdef RELEASE_STEPS 326 | .PHONY: release 327 | release: $(PRE_RELEASE_STEPS) $(RELEASE_STEPS) 328 | endif 329 | 330 | ifdef BUMPDEPS_STEPS 331 | .PHONY: bumpdeps 332 | bumpdeps: $(PRE_BUMDEPS_STEPS) $(BUMPDEPS_STEPS) 333 | endif 334 | 335 | ifdef FMT_STEPS 336 | .PHONY: fmt 337 | fmt: $(PRE_FMT_STEPS) $(FMT_STEPS) 338 | endif 339 | 340 | ifdef GENERATE_STEPS 341 | .PHONY: generate 342 | generate: $(PRE_GENERATE_STEPS) $(GENERATE_STEPS) 343 | endif 344 | 345 | .PHONY: help 346 | help:: 347 | @echo "General commands:" 348 | @[ "$(BUILD_STEPS)" != "" ] && echo " build" || true 349 | @[ "$(BUMPDEPS_STEPS)" != "" ] && echo " bumpdeps" || true 350 | @[ "$(FMT_STEPS)" != "" ] && echo " fmt" || true 351 | @[ "$(GENERATE_STEPS)" != "" ] && echo " generate" || true 352 | @[ "$(INSTALL_STEPS)" != "" ] && echo " install" || true 353 | @[ "$(LINT_STEPS)" != "" ] && echo " lint" || true 354 | @[ "$(RELEASE_STEPS)" != "" ] && echo " release" || true 355 | @[ "$(TEST_STEPS)" != "" ] && echo " test" || true 356 | @[ "$(TIDY_STEPS)" != "" ] && echo " tidy" || true 357 | @[ "$(UNITTEST_STEPS)" != "" ] && echo " unittest" || true 358 | @[ "$(VERIFY_STEPS)" != "" ] && echo " verify" || true 359 | @# FIXME: list other commands 360 | 361 | print-% : ; $(info $* is a $(flavor $*) variable set to [$($*)]) @true 362 | --------------------------------------------------------------------------------