├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml └── workflows │ ├── assign.yml │ ├── release.yml │ ├── review.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── config ├── color.go ├── color_name.go ├── color_test.go ├── color_type.go ├── concat.go ├── concat_test.go ├── config.go ├── default.go ├── label_type.go ├── label_type_test.go ├── load.go ├── load_test.go ├── style.go └── style_test.go ├── editor ├── editor.go ├── editor_test.go ├── parrot.go ├── parrot_test.go ├── regexp_flow.go ├── regexp_flow_test.go ├── test │ ├── e2e_test.go │ ├── label_set.go │ ├── label_set_test.go │ ├── statik │ │ └── statik.go │ └── test.go ├── tty.go └── tty_test.go ├── go.mod ├── go.sum ├── main.go ├── richgo.json └── sample ├── buildfail └── buildfail_test.go ├── cover05 ├── cover05.go └── cover05_test.go ├── coverall ├── coverall.go └── coverall_test.go ├── emptytest ├── dummy.go └── dummy_test.go ├── nocover ├── nocover.go └── nocover_test.go ├── notest └── notest.go ├── out_colored.txt ├── out_raw.txt ├── run.sh ├── sample_ng_test.go ├── sample_ok_test.go ├── sample_skip_test.go └── sample_timeout_test.go /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: 'bug:' 5 | labels: bug 6 | assignees: kyoh86 7 | 8 | --- 9 | 10 | ## Summary 11 | A clear and concise description of what the bug is. 12 | 13 | ## Details 14 | 15 | ### To reproduce 16 | 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | ### Expected behavior 24 | 25 | A clear and concise description of what you expected to happen. 26 | 27 | ### Screenshots 28 | 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | ### Plain `go test` output 32 | 33 | Paste a raw output text of the plain `go test`. 34 | 35 | ## Context 36 | 37 | - OS: [e.g. iOS] 38 | - Shell 39 | - Terminal application 40 | - go version 41 | 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | on: 3 | issues: 4 | types: [opened] 5 | jobs: 6 | auto-assign: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | steps: 11 | - name: 'Auto-assign issue' 12 | uses: pozil/auto-assign-issue@v1 13 | with: 14 | assignees: kyoh86 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to the GitHub Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | method: 6 | description: | 7 | Which number to increment in the semantic versioning. 8 | required: true 9 | type: choice 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check Actor 20 | if: github.actor != 'kyoh86' 21 | run: exit 1 22 | - name: Check Branch 23 | if: github.ref != 'refs/heads/main' 24 | run: exit 1 25 | - name: Wait Tests 26 | id: test_result 27 | uses: Sibz/await-status-action@v1.0.1 28 | with: 29 | contexts: test-status 30 | authToken: ${{ secrets.GITHUB_TOKEN }} 31 | timeout: 30 32 | - name: Check Test Result 33 | if: steps.test_result.outputs.result != 'success' 34 | run: | 35 | echo "feiled ${{ steps.test_result.outputs.failedCheckNames }}" 36 | echo "status ${{ steps.test_result.outputs.failedCheckStates }}" 37 | exit 1 38 | - name: Checkout Sources 39 | uses: actions/checkout@v3 40 | - name: Bump-up Semantic Version 41 | uses: kyoh86/git-vertag-action@v1 42 | with: 43 | # method: "major", "minor" or "patch" to update tag with semver 44 | method: "${{ github.event.inputs.method }}" 45 | - name: Setup Go 46 | uses: actions/setup-go@v3 47 | with: 48 | go-version: 1.19 49 | - name: Run GoReleaser 50 | uses: goreleaser/goreleaser-action@v3 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} 53 | with: 54 | args: release --rm-dist 55 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review 2 | on: [pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: reviewdog/action-golangci-lint@v1 9 | with: 10 | level: info 11 | github_token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | jobs: 7 | test: 8 | name: Test local sources 9 | strategy: 10 | fail-fast: false 11 | max-parallel: 3 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout Sources 17 | uses: actions/checkout@v3 18 | - name: Setup Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | - name: Test Go 23 | run: go test -v --race ./... 24 | test-release: 25 | name: Test releases 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout Sources 29 | uses: actions/checkout@v3 30 | - name: Setup Go 31 | uses: actions/setup-go@v3 32 | with: 33 | go-version: 1.19 34 | - name: Try Bump-up Semantic Version 35 | uses: kyoh86/git-vertag-action@v1 36 | with: 37 | method: "patch" 38 | - name: Run GoReleaser (dry-run) 39 | uses: goreleaser/goreleaser-action@v3 40 | with: 41 | args: release --rm-dist --skip-publish --snapshot 42 | test-others: 43 | name: Test others 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout Sources 47 | uses: actions/checkout@v3 48 | - name: Setup Go 49 | uses: actions/setup-go@v3 50 | with: 51 | go-version: 1.19 52 | - name: Search diagnostics 53 | uses: golangci/golangci-lint-action@v3 54 | with: 55 | version: v1.50.1 56 | test-status: 57 | name: Test status 58 | runs-on: ubuntu-latest 59 | needs: 60 | - test 61 | - test-others 62 | - test-release 63 | steps: 64 | - name: Set Check Status Success 65 | uses: Sibz/github-status-action@v1.1.6 66 | with: 67 | context: test-status 68 | authToken: ${{ secrets.GITHUB_TOKEN }} 69 | state: success 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for goreleaser output with .goreleaser.yml 2 | /dist 3 | 4 | # for man file 5 | /richgo.1 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, build with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - unparam 4 | - exportloopref 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | 3 | project_name: richgo 4 | builds: 5 | - id: default 6 | goos: 7 | - linux 8 | - darwin 9 | - windows 10 | goarch: 11 | - amd64 12 | - arm64 13 | - "386" 14 | main: . 15 | binary: richgo 16 | brews: 17 | - install: | 18 | bin.install "richgo" 19 | tap: 20 | owner: kyoh86 21 | name: homebrew-tap 22 | folder: Formula 23 | homepage: https://github.com/kyoh86/richgo 24 | description: Rich-Go will enrich `go test` outputs with text decorations 25 | license: MIT 26 | nfpms: 27 | - builds: 28 | - default 29 | maintainer: kyoh86 30 | homepage: https://github.com/kyoh86/richgo 31 | description: Rich-Go will enrich `go test` outputs with text decorations 32 | license: MIT 33 | formats: 34 | - apk 35 | - deb 36 | - rpm 37 | archives: 38 | - id: gzip 39 | format: tar.gz 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | files: 44 | - licence* 45 | - LICENCE* 46 | - license* 47 | - LICENSE* 48 | - readme* 49 | - README* 50 | - changelog* 51 | - CHANGELOG* 52 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at chili.pepper86@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 kyoh86 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: gen lint test install man sample 2 | 3 | VERSION := $(shell git describe --tags --abbrev=0) 4 | COMMIT := $(shell git rev-parse HEAD) 5 | 6 | gen: 7 | go run github.com/rakyll/statik -src=./sample -dest editor/test -include='*.txt' -f 8 | 9 | lint: gen 10 | golangci-lint run 11 | 12 | test: lint 13 | go test -v --race ./... 14 | 15 | install: test 16 | go install -a -ldflags "-X=main.version=$(VERSION) -X=main.commit=$(COMMIT)" ./... 17 | 18 | sample: 19 | sample/run.sh 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # richgo 2 | 3 | Rich-Go will enrich `go test` outputs with text decorations 4 | 5 | [![PkgGoDev](https://pkg.go.dev/badge/kyoh86/richgo)](https://pkg.go.dev/kyoh86/richgo) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/kyoh86/richgo)](https://goreportcard.com/report/github.com/kyoh86/richgo) 7 | [![Coverage Status](https://img.shields.io/codecov/c/github/kyoh86/richgo.svg)](https://codecov.io/gh/kyoh86/richgo) 8 | [![Release](https://github.com/kyoh86/richgo/workflows/Release/badge.svg)](https://github.com/kyoh86/richgo/releases) 9 | 10 | [![asciicast](https://asciinema.org/a/99810.png)](https://asciinema.org/a/99810) 11 | 12 | # NOTICE (what I think about `richgo`) 13 | 14 | For some years, I've not been using `richgo`. 15 | Now I don't feel much effect that a little bit of tweaking to the appearance of the test output. 16 | And It is poor method that `richgo` parses and adjusts the standard output of `go test`. 17 | So now I recommend you that you do NOT use `richgo`, get use to pure `go test`, train an ability to find the error from the output and contribute to improve official `go test` if you needed. 18 | Some may think that I have too much faith in pure Go, but this is my honest feeling. 19 | 20 | If you want a good suggestion for alternative tools for this one, you may find it in the [issue #57](https://github.com/kyoh86/richgo/issues/57). 21 | 22 | 23 | # Installation 24 | 25 | (go get): 26 | 27 | ``` 28 | go get -u github.com/kyoh86/richgo 29 | ``` 30 | 31 | (homebrew): 32 | 33 | ``` 34 | brew install kyoh86/tap/richgo 35 | ``` 36 | 37 | (asdf): 38 | 39 | ``` 40 | asdf plugin add richgo 41 | asdf install richgo 0.3.6 42 | ``` 43 | 44 | # Usage 45 | 46 | ## Basic 47 | 48 | ```sh 49 | richgo test ./... 50 | ``` 51 | 52 | ## In an existing pipeline 53 | 54 | If your build scripts expect to interact with the standard output format of `go 55 | test` (for instance, if you're using 56 | [go-junit-report](https://github.com/jstemmer/go-junit-report)), you'll need to 57 | use the `testfilter` subcommand of `richgo`. 58 | 59 | For example: 60 | 61 | ```sh 62 | go test ./... | tee >(richgo testfilter) | go-junit-report 63 | ``` 64 | 65 | This will "tee" the output of the standard `go test` run into a `richgo 66 | testfilter` process as well as passing the original output to 67 | `go-junit-report`. 68 | 69 | Note that at some point this recommendation may change, as the "go test" tool 70 | may learn how to produce a standard output format 71 | [golang/go#2981](https://github.com/golang/go/issues/2981) that both this tool 72 | and others could rely on. 73 | 74 | ## alias 75 | 76 | You can define alias so that `go test` prints rich outputs: 77 | 78 | * bash: `~/.bashrc` 79 | * zsh: `~/.zshrc` 80 | 81 | ``` 82 | alias go=richgo 83 | ``` 84 | 85 | ## Configure 86 | 87 | ### Configuration file paths 88 | 89 | It's possible to change styles with the preference file. 90 | Rich-Go loads preferences from the files in the following order. 91 | 92 | * `${CWD}/.richstyle` 93 | * `${CWD}/.richstyle.yaml` 94 | * `${CWD}/.richstyle.yml` 95 | * `${GOPATH}/.richstyle` 96 | * `${GOPATH}/.richstyle.yaml` 97 | * `${GOPATH}/.richstyle.yml` 98 | * `${GOROOT}/.richstyle` 99 | * `${GOROOT}/.richstyle.yaml` 100 | * `${GOROOT}/.richstyle.yml` 101 | * `${HOME}/.richstyle` 102 | * `${HOME}/.richstyle.yaml` 103 | * `${HOME}/.richstyle.yml` 104 | 105 | Setting the environment variable `RICHGO_LOCAL` to 1, Rich-Go loads only `${CWD}/.richstyle*`. 106 | 107 | ### Configuration file format 108 | 109 | **Now Rich-Go supports only YAML formatted.** 110 | 111 | ```yaml 112 | # Type of the label that notes a kind of each lines. 113 | labelType: (long | short | none) 114 | 115 | # Style of "Build" lines. 116 | buildStyle: 117 | # Hide lines 118 | hide: (true | false) 119 | # Bold or increased intensity. 120 | bold: (true | false) 121 | faint: (true | false) 122 | italic: (true | false) 123 | underline: (true | false) 124 | blinkSlow: (true | false) 125 | blinkRapid: (true | false) 126 | # Swap the foreground color and background color. 127 | inverse: (true | false) 128 | conceal: (true | false) 129 | crossOut: (true | false) 130 | frame: (true | false) 131 | encircle: (true | false) 132 | overline: (true | false) 133 | # Fore-color of text 134 | foreground: ("#xxxxxx" | rgb(0-256,0-256,0-256) | rgb(0x00-0xFF,0x00-0xFF,0x00-0xFF) | (name of colors)) 135 | # Back-color of text 136 | background: # Same format as `foreground` 137 | 138 | # Style of the "Start" lines. 139 | startStyle: 140 | # Same format as `buildStyle` 141 | 142 | # Style of the "Pass" lines. 143 | passStyle: 144 | # Same format as `buildStyle` 145 | 146 | # Style of the "Fail" lines. 147 | failStyle: 148 | # Same format as `buildStyle` 149 | 150 | # Style of the "Skip" lines. 151 | skipStyle: 152 | # Same format as `buildStyle` 153 | 154 | # Style of the "File" lines. 155 | fileStyle: 156 | # Same format as `buildStyle` 157 | 158 | # Style of the "Line" lines. 159 | lineStyle: 160 | # Same format as `buildStyle` 161 | 162 | # Style of the "Pass" package lines. 163 | passPackageStyle: 164 | # Same format as `buildStyle` 165 | 166 | # Style of the "Fail" package lines. 167 | failPackageStyle: 168 | # Same format as `buildStyle` 169 | 170 | # A threashold of the coverage 171 | coverThreshold: (0-100) 172 | 173 | # Style of the "Cover" lines with the coverage that is higher than coverThreshold. 174 | coveredStyle: 175 | # Same format as `buildStyle` 176 | 177 | # Style of the "Cover" lines with the coverage that is lower than coverThreshold. 178 | uncoveredStyle: 179 | # Same format as `buildStyle` 180 | 181 | # If you want to delete lines, write the regular expressions. 182 | removals: 183 | - (regexp) 184 | # If you want to leave `Test` prefixes, set it "true". 185 | leaveTestPrefix: (true | false) 186 | ``` 187 | 188 | ### Line categories 189 | 190 | Rich-Go separate the output-lines in following categories. 191 | 192 | * Build: 193 | When the Go fails to build, it prints errors like this: 194 | 195 |
# github.com/kyoh86/richgo/sample/buildfail
196 |   sample/buildfail/buildfail_test.go:6: t.Foo undefined (type testing.T has no field or method Foo)
197 | 198 | * Start: 199 | In the top of test, Go prints that name like this: 200 | 201 |
=== RUN   TestSampleOK/SubtestOK
202 | 203 | * Pass: 204 | When a test is successed, Go prints that name like this: 205 | 206 |
    ---PASS: TestSampleOK/SubtestOK
207 | 208 | * Fail: 209 | When a test is failed, Go prints that name like this: 210 | 211 |
--- FAIL: TestSampleNG (0.00s)
212 |   sample_ng_test.go:9: It's not OK... :(
213 | 214 | * Skip: 215 | If there is no test files in directory or a test is skipped, Go prints that path or the name like this: 216 | 217 |
--- SKIP: TestSampleSkip (0.00s)
218 |   sample_skip_test.go:6:
219 | ?     github.com/kyoh86/richgo/sample/notest  [no test files]
220 | 221 | * PassPackage: 222 | When tests in package are successed, Go prints just: 223 | 224 |
PASS
225 | 226 | * Fail: 227 | When a test in package are failed, Go prints just: 228 | 229 |
FAIL
230 | 231 | * Cover: 232 | If the coverage analysis is enabled, Go prints the coverage like this: 233 | 234 |
=== RUN   TestCover05
235 | --- PASS: TestCover05 (0.00s)
236 | PASS
237 | coverage: 50.0% of statements
238 | ok  	github.com/kyoh86/richgo/sample/cover05	0.012s	coverage: 50.0% of statements
239 | 240 | Each categories can be styled seperately. 241 | 242 | ### Label types 243 | 244 | * Long: 245 | * Build: "BUILD" 246 | * Start: "START" 247 | * Pass: "PASS" 248 | * Fail: "FAIL" 249 | * Skip: "SKIP" 250 | * Cover: "COVER" 251 | 252 | * Short: 253 | * Build: "!!" 254 | * Start: ">" 255 | * Pass: "o" 256 | * Fail: "x" 257 | * Skip: "-" 258 | * Cover: "%" 259 | 260 | * None: 261 | Rich-Go will never output labels. 262 | 263 | ### Default 264 | 265 | ```yaml 266 | labelType: long 267 | buildStyle: 268 | bold: true 269 | foreground: yellow 270 | startStyle: 271 | foreground: lightBlack 272 | passStyle: 273 | foreground: green 274 | failStyle: 275 | bold: true 276 | foreground: red 277 | skipStyle: 278 | foreground: lightBlack 279 | passPackageStyle: 280 | foreground: green 281 | hide: true 282 | failPackageStyle: 283 | bold: true 284 | foreground: red 285 | hide: true 286 | coverThreshold: 50 287 | coveredStyle: 288 | foreground: green 289 | uncoveredStyle: 290 | bold: true 291 | foreground: yellow 292 | fileStyle: 293 | foreground: cyan 294 | lineStyle: 295 | foreground: magenta 296 | ``` 297 | 298 | ## Overriding colorization detection 299 | 300 | By default, `richgo` determines whether or not to colorize its output based 301 | on whether it's connected to a TTY or not. This works for most use cases, but 302 | may not behave as expected if you use `richgo` in a pipeline of commands, where 303 | STDOUT is being piped to another command. 304 | 305 | To force colorization, add `RICHGO_FORCE_COLOR=1` to the environment you're 306 | running in. For example: 307 | 308 | ```sh 309 | RICHGO_FORCE_COLOR=1 richgo test ./... | tee test.log 310 | ``` 311 | 312 | ## Configure to resolve a conflict with "Solarized dark" theme 313 | 314 | The bright-black is used for background color in Solarized dark theme. 315 | Richgo uses that color for "startStyle" and "skipStyle", so "START" and "SKIP" lines can not be seen on the screen with Solarized dark theme. 316 | 317 | To resolve that conflict, you can set another color for "startStyle" and "skipStyle" in [.richstyle](#configuration-file-paths) like below. 318 | 319 | ``` 320 | startStyle: 321 | foreground: yellow 322 | 323 | skipStyle: 324 | foreground: lightYellow 325 | ``` 326 | 327 | ## Getting a version of the richgo 328 | 329 | If you want to get a version of the `richgo`, this information is embedded in the binary (since Go 1.18). 330 | You can view it with go version -m, e.g. for richgo 0.3.10: 331 | 332 | ```console 333 | $ go version -m $(command -v richgo) 334 | ./richgo: go1.18 335 | path github.com/kyoh86/richgo 336 | mod github.com/kyoh86/richgo v0.3.10 h1:iSGvcjhtQN2IVrBDhPk0if0R/RMQnCN1E/9OyAW4UUs= 337 | [...] 338 | ``` 339 | 340 | And just a little more advanced way (with POSIX `awk`): 341 | 342 | ```console 343 | $ go version -m $(command -v richgo) | awk '$1 == "mod" && $2 == "github.com/kyoh86/richgo" {print $3;}' 344 | v0.3.10 345 | ``` 346 | 347 | # License 348 | 349 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](http://www.opensource.org/licenses/MIT) 350 | 351 | This is distributed under the [MIT License](http://www.opensource.org/licenses/MIT). 352 | 353 | -------------------------------------------------------------------------------- /config/color.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/morikuni/aec" 11 | ) 12 | 13 | // Color is the color in the ANSI for configuration 14 | type Color struct { 15 | Type ColorType 16 | 17 | Value8 uint8 18 | 19 | ValueR uint8 20 | ValueG uint8 21 | ValueB uint8 22 | 23 | Name ColorName 24 | } 25 | 26 | // MarshalYAML implements Marshaler 27 | func (c Color) MarshalYAML() (interface{}, error) { 28 | switch c.Type { 29 | case ColorTypeNone: 30 | return "", nil 31 | case ColorTypeName: 32 | return c.Name.String(), nil 33 | case ColorType8Bit: 34 | return c.Value8, nil 35 | case ColorType24Bit: 36 | return fmt.Sprintf(`#%x%x%x`, c.ValueR, c.ValueG, c.ValueB), nil 37 | } 38 | return nil, fmt.Errorf("invalid color type %s", c.Type) 39 | } 40 | 41 | // MarshalJSON implements Marshaler 42 | func (c Color) MarshalJSON() ([]byte, error) { 43 | switch c.Type { 44 | case ColorTypeNone: 45 | return []byte(`""`), nil 46 | case ColorTypeName: 47 | return []byte(fmt.Sprintf(`"%s"`, c.Name.String())), nil 48 | case ColorType8Bit: 49 | return []byte(fmt.Sprintf("%d", c.Value8)), nil 50 | case ColorType24Bit: 51 | return []byte(fmt.Sprintf(`"#%x%x%x"`, c.ValueR, c.ValueG, c.ValueB)), nil 52 | } 53 | return nil, fmt.Errorf("invalid color type %s", c.Type) 54 | } 55 | 56 | // UnmarshalYAML implements Unmarshaler 57 | func (c *Color) UnmarshalYAML(unmarshal func(interface{}) error) error { 58 | var s string 59 | if err := unmarshal(&s); err != nil { 60 | return err 61 | } 62 | return c.unmarshal(s, false) 63 | } 64 | 65 | var errInvalidFormat = errors.New("invalid format") 66 | 67 | var reg8Bit = regexp.MustCompile(`(?mi)^(\d{1,3})$`) 68 | 69 | func (c *Color) unmarshalAs8Bit(str string) error { 70 | match := reg8Bit.FindStringSubmatch(str) 71 | if len(match) != 2 { 72 | return errInvalidFormat 73 | } 74 | v, err := atoi(match[1]) 75 | if err != nil { 76 | return errInvalidFormat 77 | } 78 | c.Type = ColorType8Bit 79 | c.Value8 = v 80 | return nil 81 | } 82 | 83 | var reg8BitHex = regexp.MustCompile(`(?mi)^(0[xX][[:xdigit:]]{1,2})$`) 84 | 85 | func (c *Color) unmarshalAs8BitHex(str string) error { 86 | match := reg8BitHex.FindStringSubmatch(str) 87 | if len(match) != 2 { 88 | return errInvalidFormat 89 | } 90 | v, _ := atoi(match[1]) 91 | c.Type = ColorType8Bit 92 | c.Value8 = v 93 | return nil 94 | } 95 | 96 | var regRGB = regexp.MustCompile(`(?mi)^#([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})$`) 97 | 98 | func (c *Color) unmarshalAs24BitRGB(str string) error { 99 | match := regRGB.FindStringSubmatch(str) 100 | if len(match) != 4 { 101 | return errInvalidFormat 102 | } 103 | r, _ := strconv.ParseUint(match[1], 16, 8) 104 | g, _ := strconv.ParseUint(match[2], 16, 8) 105 | b, _ := strconv.ParseUint(match[3], 16, 8) 106 | c.Type = ColorType24Bit 107 | c.ValueR = uint8(r) 108 | c.ValueG = uint8(g) 109 | c.ValueB = uint8(b) 110 | return nil 111 | } 112 | 113 | var regRGBFunc = regexp.MustCompile(`(?mi)^rgb\((0x[[:xdigit:]]{2}|\d{1,3}), *(0x[[:xdigit:]]{2}|\d{1,3}), *(0x[[:xdigit:]]{2}|\d{1,3})\)$`) 114 | 115 | func (c *Color) unmarshalAsRGBFunc(str string) error { 116 | match := regRGBFunc.FindStringSubmatch(str) 117 | if len(match) != 4 { 118 | return errInvalidFormat 119 | } 120 | r, err := atoi(match[1]) 121 | if err != nil { 122 | return errInvalidFormat 123 | } 124 | g, err := atoi(match[2]) 125 | if err != nil { 126 | return errInvalidFormat 127 | } 128 | b, err := atoi(match[3]) 129 | if err != nil { 130 | return errInvalidFormat 131 | } 132 | 133 | c.Type = ColorType24Bit 134 | c.ValueR = r 135 | c.ValueG = g 136 | c.ValueB = b 137 | return nil 138 | } 139 | 140 | // UnmarshalJSON implements Unmarshaler 141 | func (c *Color) UnmarshalJSON(raw []byte) error { 142 | return c.unmarshal(string(raw), true) 143 | } 144 | 145 | func (c *Color) unmarshal(str string, unquote bool) error { 146 | if unquote { 147 | unquoted, err := strconv.Unquote(str) 148 | if err == nil { 149 | str = unquoted 150 | } 151 | } 152 | if str == "" { 153 | c.Type = ColorTypeNone 154 | return nil 155 | } 156 | if err := c.unmarshalAs8Bit(str); err == nil { 157 | return nil 158 | } 159 | if err := c.unmarshalAs8BitHex(str); err == nil { 160 | return nil 161 | } 162 | if err := c.unmarshalAs24BitRGB(str); err == nil { 163 | return nil 164 | } 165 | if err := c.unmarshalAsRGBFunc(str); err == nil { 166 | return nil 167 | } 168 | for _, cn := range ColorNames() { 169 | if cn.String() == str { 170 | c.Type = ColorTypeName 171 | c.Name = cn 172 | return nil 173 | } 174 | } 175 | return errInvalidFormat 176 | } 177 | 178 | func atoi(s string) (uint8, error) { 179 | i, err := atoiCore(s) 180 | if err != nil { 181 | return 0, err 182 | } 183 | return uint8(i), nil 184 | } 185 | 186 | func atoiCore(s string) (uint64, error) { 187 | if strings.HasPrefix(s, "0x") { 188 | return strconv.ParseUint(strings.TrimPrefix(s, "0x"), 16, 8) 189 | } 190 | if strings.HasPrefix(s, "0X") { 191 | return strconv.ParseUint(strings.TrimPrefix(s, "0X"), 16, 8) 192 | } 193 | return strconv.ParseUint(s, 10, 8) 194 | } 195 | 196 | var backColors = map[ColorName]aec.ANSI{ 197 | Black: aec.BlackB, 198 | Red: aec.RedB, 199 | Green: aec.GreenB, 200 | Yellow: aec.YellowB, 201 | Blue: aec.BlueB, 202 | Magenta: aec.MagentaB, 203 | Cyan: aec.CyanB, 204 | White: aec.WhiteB, 205 | LightBlack: aec.LightBlackB, 206 | LightRed: aec.LightRedB, 207 | LightGreen: aec.LightGreenB, 208 | LightYellow: aec.LightYellowB, 209 | LightBlue: aec.LightBlueB, 210 | LightMagenta: aec.LightMagentaB, 211 | LightCyan: aec.LightCyanB, 212 | LightWhite: aec.LightWhiteB, 213 | } 214 | 215 | var emptyColor aec.ANSI 216 | 217 | func init() { 218 | emptyColor = aec.EmptyBuilder.ANSI 219 | } 220 | 221 | // B gets background ANSI color 222 | func (c *Color) B() aec.ANSI { 223 | switch c.Type { 224 | case ColorTypeNone: 225 | return emptyColor 226 | case ColorTypeName: 227 | b, ok := backColors[c.Name] 228 | if ok { 229 | return b 230 | } 231 | case ColorType8Bit: 232 | return aec.Color8BitB(aec.RGB8Bit(c.Value8)) 233 | case ColorType24Bit: 234 | return aec.FullColorB(c.ValueR, c.ValueG, c.ValueB) 235 | } 236 | return emptyColor 237 | } 238 | 239 | var frontColors = map[ColorName]aec.ANSI{ 240 | Black: aec.BlackF, 241 | Red: aec.RedF, 242 | Green: aec.GreenF, 243 | Yellow: aec.YellowF, 244 | Blue: aec.BlueF, 245 | Magenta: aec.MagentaF, 246 | Cyan: aec.CyanF, 247 | White: aec.WhiteF, 248 | LightBlack: aec.LightBlackF, 249 | LightRed: aec.LightRedF, 250 | LightGreen: aec.LightGreenF, 251 | LightYellow: aec.LightYellowF, 252 | LightBlue: aec.LightBlueF, 253 | LightMagenta: aec.LightMagentaF, 254 | LightCyan: aec.LightCyanF, 255 | LightWhite: aec.LightWhiteF, 256 | } 257 | 258 | // F gets foreground ANSI color 259 | func (c *Color) F() aec.ANSI { 260 | switch c.Type { 261 | case ColorTypeNone: 262 | return emptyColor 263 | case ColorTypeName: 264 | f, ok := frontColors[c.Name] 265 | if ok { 266 | return f 267 | } 268 | case ColorType8Bit: 269 | return aec.Color8BitF(aec.RGB8Bit(c.Value8)) 270 | case ColorType24Bit: 271 | return aec.FullColorF(c.ValueR, c.ValueG, c.ValueB) 272 | } 273 | return emptyColor 274 | } 275 | -------------------------------------------------------------------------------- /config/color_name.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // ColorName is the name of ANSI-colors 4 | type ColorName string 5 | 6 | func (n ColorName) String() string { return string(n) } 7 | 8 | // Colors 9 | const ( 10 | DefaultColor = ColorName("default") 11 | 12 | Black = ColorName("black") 13 | Red = ColorName("red") 14 | Green = ColorName("green") 15 | Yellow = ColorName("yellow") 16 | Blue = ColorName("blue") 17 | Magenta = ColorName("magenta") 18 | Cyan = ColorName("cyan") 19 | White = ColorName("white") 20 | 21 | LightBlack = ColorName("lightBlack") 22 | LightRed = ColorName("lightRed") 23 | LightGreen = ColorName("lightGreen") 24 | LightYellow = ColorName("lightYellow") 25 | LightBlue = ColorName("lightBlue") 26 | LightMagenta = ColorName("lightMagenta") 27 | LightCyan = ColorName("lightCyan") 28 | LightWhite = ColorName("lightWhite") 29 | ) 30 | 31 | // ColorNames will get variations of the colors. 32 | func ColorNames() []ColorName { 33 | return []ColorName{ 34 | DefaultColor, 35 | Black, 36 | Red, 37 | Green, 38 | Yellow, 39 | Blue, 40 | Magenta, 41 | Cyan, 42 | White, 43 | LightBlack, 44 | LightRed, 45 | LightGreen, 46 | LightYellow, 47 | LightBlue, 48 | LightMagenta, 49 | LightCyan, 50 | LightWhite, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config/color_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | yaml "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type testStruct struct { 14 | C Color `json:"c" yaml:"c,omitempty"` 15 | } 16 | 17 | func TestMarshalYAML(t *testing.T) { 18 | for exp, color := range map[string]testStruct{ 19 | `{}`: {}, 20 | `c: ""`: {Color{Type: ColorTypeNone}}, 21 | `c: black`: {Color{Type: ColorTypeName, Name: Black}}, 22 | `c: '#ffeedd'`: {Color{Type: ColorType24Bit, ValueR: 0xff, ValueG: 0xee, ValueB: 0xdd}}, 23 | `c: 31`: {Color{Type: ColorType8Bit, Value8: 31}}, 24 | } { 25 | color := color 26 | buf, err := yaml.Marshal(&color) 27 | if err != nil { 28 | t.Errorf("failed to marshal a color %q to yaml with error %q", "black", err) 29 | t.FailNow() 30 | } 31 | exp += "\n" // NOTE: yaml will have trailing newline 32 | act := string(buf) 33 | if exp != act { 34 | t.Errorf("expect that a marshaled color be %q, but %q", exp, act) 35 | } 36 | } 37 | 38 | _, err := yaml.Marshal(testStruct{Color{Type: ColorType("foobar")}}) 39 | if err == nil { 40 | t.Errorf("invalid ColorType %q accepted in marshaling", "foobar") 41 | } 42 | } 43 | 44 | func TestMarshalJSON(t *testing.T) { 45 | for exp, color := range map[string]testStruct{ 46 | `{"c":""}`: {C: Color{Type: ColorTypeNone}}, 47 | `{"c":"black"}`: {C: Color{Type: ColorTypeName, Name: Black}}, 48 | `{"c":"#ffeedd"}`: {C: Color{Type: ColorType24Bit, ValueR: 0xff, ValueG: 0xee, ValueB: 0xdd}}, 49 | `{"c":31}`: {C: Color{Type: ColorType8Bit, Value8: 31}}, 50 | } { 51 | color := color 52 | buf, err := json.Marshal(&color) 53 | if err != nil { 54 | t.Errorf("failed to marshal a color to json with error %q", err) 55 | t.FailNow() 56 | } 57 | act := string(buf) 58 | if exp != act { 59 | t.Errorf("expect that a marshaled color be %q, but %q", exp, act) 60 | } 61 | } 62 | 63 | _, err := json.Marshal(testStruct{Color{Type: ColorType("foobar")}}) 64 | if err == nil { 65 | t.Errorf("invalid ColorType %q accepted in marshaling", "foobar") 66 | } 67 | } 68 | 69 | func TestUnmarshalYAML(t *testing.T) { 70 | const yamlTemplate = "act: %s" 71 | for value, exp := range map[string]Color{ 72 | `""`: {Type: ColorTypeNone}, 73 | `0x1F`: {Type: ColorType8Bit, Value8: 0x1F}, 74 | `"0x1F"`: {Type: ColorType8Bit, Value8: 0x1F}, 75 | `25`: {Type: ColorType8Bit, Value8: 25}, 76 | `"red"`: {Type: ColorTypeName, Name: Red}, 77 | `red`: {Type: ColorTypeName, Name: Red}, 78 | `"#ffeedd"`: {Type: ColorType24Bit, ValueR: 0xff, ValueG: 0xee, ValueB: 0xdd}, 79 | `"rGb(255, 0xEe, 127)"`: {Type: ColorType24Bit, ValueR: 255, ValueG: 0xEE, ValueB: 127}, 80 | `rGb(255, 0XEe, 127)`: {Type: ColorType24Bit, ValueR: 255, ValueG: 0xEE, ValueB: 127}, 81 | } { 82 | var obj struct { 83 | Act Color `yaml:"act"` 84 | } 85 | err := yaml.Unmarshal([]byte(fmt.Sprintf(yamlTemplate, value)), &obj) 86 | if err != nil { 87 | t.Errorf("failed to unmarshal a color %q as yaml with error %q", value, err) 88 | continue 89 | } 90 | if obj.Act != exp { 91 | t.Errorf("expect that a marshaled color be %#v, but %#v", exp, obj.Act) 92 | } 93 | } 94 | 95 | var obj struct { 96 | Act *Color `yaml:"act"` 97 | } 98 | err := yaml.Unmarshal([]byte{}, &obj) 99 | if err != nil { 100 | t.Errorf("failed to unmarshal a empty color as yaml with error %q", err) 101 | } 102 | if obj.Act != nil { 103 | t.Errorf("expect that a marshaled color be nil, but %#v", obj.Act) 104 | } 105 | 106 | for _, value := range []string{ 107 | `{}`, 108 | `[]`, 109 | `256`, 110 | `rgb(256,0,0)`, 111 | `rgb(0,256,0)`, 112 | `rgb(0,0,256)`, 113 | } { 114 | var obj struct { 115 | Act Color `yaml:"act"` 116 | } 117 | err := yaml.Unmarshal([]byte(fmt.Sprintf(yamlTemplate, value)), &obj) 118 | if err == nil { 119 | t.Errorf("invalid color %q accepted in unmarshaling as %#v", value, obj) 120 | } 121 | } 122 | } 123 | 124 | func TestUnmarshalJSON(t *testing.T) { 125 | const jsonTemplate = `{"act":%s}` 126 | for value, exp := range map[string]Color{ 127 | `""`: {Type: ColorTypeNone}, 128 | `"0x1F"`: {Type: ColorType8Bit, Value8: 0x1F}, 129 | `25`: {Type: ColorType8Bit, Value8: 25}, 130 | `"red"`: {Type: ColorTypeName, Name: Red}, 131 | `"#ffeedd"`: {Type: ColorType24Bit, ValueR: 0xff, ValueG: 0xee, ValueB: 0xdd}, 132 | `"rGb(255, 0xEe, 127)"`: {Type: ColorType24Bit, ValueR: 255, ValueG: 0xEE, ValueB: 127}, 133 | } { 134 | var obj struct { 135 | Act Color `json:"act"` 136 | } 137 | err := json.Unmarshal([]byte(fmt.Sprintf(jsonTemplate, value)), &obj) 138 | if err != nil { 139 | t.Errorf("failed to unmarshal a color %q as json with error %q", value, err) 140 | continue 141 | } 142 | if obj.Act != exp { 143 | t.Errorf("expect that a marshaled color be %#v, but %#v", exp, obj.Act) 144 | } 145 | } 146 | for _, value := range []string{ 147 | `{}`, 148 | `[]`, 149 | `256`, 150 | `"rgb(256,0,0)"`, 151 | `"rgb(0,256,0)"`, 152 | `"rgb(0,0,256)"`, 153 | } { 154 | var obj struct { 155 | Act Color `json:"act"` 156 | } 157 | err := json.Unmarshal([]byte(fmt.Sprintf(jsonTemplate, value)), &obj) 158 | if err == nil { 159 | t.Errorf("invalid color %q accepted in unmarshaling as %#v", value, obj) 160 | } 161 | } 162 | } 163 | 164 | func TestB(t *testing.T) { 165 | const esc = "\x1b[" 166 | for _, c := range []Color{ 167 | {Type: ColorTypeName, Name: Black}, 168 | {Type: ColorType8Bit, Value8: 11}, 169 | {Type: ColorType24Bit, ValueR: 33, ValueG: 22, ValueB: 11}, 170 | } { 171 | // B() func must return Back Color (formed ESC+[+3x) 172 | if !strings.HasPrefix(c.B().String(), esc+"4") { 173 | t.Errorf("invalid front color: %s", strings.TrimPrefix(c.B().String(), esc)) 174 | } 175 | } 176 | 177 | for _, c := range []Color{ 178 | {}, 179 | {Type: ColorTypeName, Name: "invalidColor"}, 180 | } { 181 | c := c 182 | assert.Equal(t, "", c.B().String()) 183 | } 184 | } 185 | 186 | func TestF(t *testing.T) { 187 | const esc = "\x1b[" 188 | for _, c := range []Color{ 189 | {Type: ColorTypeName, Name: Black}, 190 | {Type: ColorType8Bit, Value8: 11}, 191 | {Type: ColorType24Bit, ValueR: 33, ValueG: 22, ValueB: 11}, 192 | } { 193 | // F() func must return Front Color (formed ESC+[+3x) 194 | if !strings.HasPrefix(c.F().String(), esc+"3") { 195 | t.Errorf("invalid front color: %s", strings.TrimPrefix(c.F().String(), esc)) 196 | } 197 | } 198 | 199 | for _, c := range []Color{ 200 | {}, 201 | {Type: ColorTypeName, Name: "invalidColor"}, 202 | } { 203 | c := c 204 | assert.Equal(t, "", c.F().String()) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /config/color_type.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // ColorType is the type of color 4 | type ColorType string 5 | 6 | const ( 7 | // ColorTypeNone defines empty 8 | ColorTypeNone = ColorType("none") 9 | // ColorType8Bit defines 8-bit (256) colors 10 | ColorType8Bit = ColorType("8bit") 11 | // ColorType24Bit defines 24-bit (R: 8bit + G: 8bit + B: 8bit ; full) colors 12 | ColorType24Bit = ColorType("24bit") 13 | // ColorTypeName defines named colors 14 | ColorTypeName = ColorType("named") 15 | ) 16 | 17 | func (l ColorType) String() string { return string(l) } 18 | -------------------------------------------------------------------------------- /config/concat.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/wacul/ptr" 4 | 5 | func concatInt(a, b *int) *int { 6 | if a == nil { 7 | if b == nil { 8 | return nil 9 | } 10 | f := *b 11 | return &f 12 | } 13 | return a 14 | } 15 | 16 | func actualInt(b *int) *int { 17 | if b == nil { 18 | return ptr.Int(0) 19 | } 20 | return b 21 | } 22 | 23 | func concatConfig(base, other *Config) *Config { 24 | if base == nil { 25 | if other == nil { 26 | return nil 27 | } 28 | base = &Config{} 29 | } 30 | if other == nil { 31 | other = &Config{} 32 | } 33 | return &Config{ 34 | LabelType: concatLabelType(base.LabelType, other.LabelType), 35 | BuildStyle: concatStyle(base.BuildStyle, other.BuildStyle), 36 | StartStyle: concatStyle(base.StartStyle, other.StartStyle), 37 | PassStyle: concatStyle(base.PassStyle, other.PassStyle), 38 | FailStyle: concatStyle(base.FailStyle, other.FailStyle), 39 | PassPackageStyle: concatStyle(base.PassPackageStyle, other.PassPackageStyle), 40 | FailPackageStyle: concatStyle(base.FailPackageStyle, other.FailPackageStyle), 41 | SkipStyle: concatStyle(base.SkipStyle, other.SkipStyle), 42 | FileStyle: concatStyle(base.FileStyle, other.FileStyle), 43 | LineStyle: concatStyle(base.LineStyle, other.LineStyle), 44 | CoverThreshold: concatInt(base.CoverThreshold, other.CoverThreshold), 45 | CoveredStyle: concatStyle(base.CoveredStyle, other.CoveredStyle), 46 | UncoveredStyle: concatStyle(base.UncoveredStyle, other.UncoveredStyle), 47 | Removals: append(base.Removals, other.Removals...), 48 | LeaveTestPrefix: concatBool(base.LeaveTestPrefix, other.LeaveTestPrefix), 49 | } 50 | } 51 | 52 | func actualConfig(config *Config) *Config { 53 | if config == nil { 54 | config = &Config{} 55 | } 56 | return &Config{ 57 | LabelType: actualLabelType(config.LabelType), 58 | BuildStyle: actualStyle(config.BuildStyle), 59 | StartStyle: actualStyle(config.StartStyle), 60 | PassStyle: actualStyle(config.PassStyle), 61 | FailStyle: actualStyle(config.FailStyle), 62 | PassPackageStyle: actualStyle(config.PassPackageStyle), 63 | FailPackageStyle: actualStyle(config.FailPackageStyle), 64 | SkipStyle: actualStyle(config.SkipStyle), 65 | FileStyle: actualStyle(config.FileStyle), 66 | LineStyle: actualStyle(config.LineStyle), 67 | CoverThreshold: actualInt(config.CoverThreshold), 68 | CoveredStyle: actualStyle(config.CoveredStyle), 69 | UncoveredStyle: actualStyle(config.UncoveredStyle), 70 | Removals: config.Removals, 71 | LeaveTestPrefix: actualBool(config.LeaveTestPrefix), 72 | } 73 | } 74 | 75 | func concatColor(a, b *Color) *Color { 76 | if a == nil { 77 | return b 78 | } 79 | return a 80 | } 81 | 82 | func actualColor(c *Color) *Color { 83 | if c == nil { 84 | return &Color{ 85 | Type: ColorTypeNone, 86 | } 87 | } 88 | return c 89 | } 90 | 91 | func concatLabelType(base, other *LabelType) *LabelType { 92 | if base == nil { 93 | if other == nil { 94 | return nil 95 | } 96 | return other 97 | } 98 | return base 99 | } 100 | 101 | func actualLabelType(t *LabelType) *LabelType { 102 | if t == nil { 103 | l := LabelTypeLong 104 | return &l 105 | } 106 | return t 107 | } 108 | 109 | func concatStyle(base, other *Style) *Style { 110 | if base == nil { 111 | if other == nil { 112 | return nil 113 | } 114 | base = &Style{} 115 | } 116 | if other == nil { 117 | other = &Style{} 118 | } 119 | return &Style{ 120 | Hide: concatBool(base.Hide, other.Hide), 121 | Bold: concatBool(base.Bold, other.Bold), 122 | Faint: concatBool(base.Faint, other.Faint), 123 | Italic: concatBool(base.Italic, other.Italic), 124 | Underline: concatBool(base.Underline, other.Underline), 125 | BlinkSlow: concatBool(base.BlinkSlow, other.BlinkSlow), 126 | BlinkRapid: concatBool(base.BlinkRapid, other.BlinkRapid), 127 | Inverse: concatBool(base.Inverse, other.Inverse), 128 | Conceal: concatBool(base.Conceal, other.Conceal), 129 | CrossOut: concatBool(base.CrossOut, other.CrossOut), 130 | Frame: concatBool(base.Frame, other.Frame), 131 | Encircle: concatBool(base.Encircle, other.Encircle), 132 | Overline: concatBool(base.Overline, other.Overline), 133 | Foreground: concatColor(base.Foreground, other.Foreground), 134 | Background: concatColor(base.Background, other.Background), 135 | } 136 | } 137 | 138 | func actualStyle(s *Style) *Style { 139 | if s == nil { 140 | s = &Style{} 141 | } 142 | return &Style{ 143 | Hide: actualBool(s.Hide), 144 | Bold: actualBool(s.Bold), 145 | Faint: actualBool(s.Faint), 146 | Italic: actualBool(s.Italic), 147 | Underline: actualBool(s.Underline), 148 | BlinkSlow: actualBool(s.BlinkSlow), 149 | BlinkRapid: actualBool(s.BlinkRapid), 150 | Inverse: actualBool(s.Inverse), 151 | Conceal: actualBool(s.Conceal), 152 | CrossOut: actualBool(s.CrossOut), 153 | Frame: actualBool(s.Frame), 154 | Encircle: actualBool(s.Encircle), 155 | Overline: actualBool(s.Overline), 156 | Foreground: actualColor(s.Foreground), 157 | Background: actualColor(s.Background), 158 | } 159 | } 160 | 161 | func concatBool(a, b *bool) *bool { 162 | if a == nil { 163 | if b == nil { 164 | return nil 165 | } 166 | f := *b 167 | return &f 168 | } 169 | return a 170 | } 171 | 172 | func actualBool(b *bool) *bool { 173 | if b == nil { 174 | return ptr.Bool(false) 175 | } 176 | return b 177 | } 178 | -------------------------------------------------------------------------------- /config/concat_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/wacul/ptr" 8 | ) 9 | 10 | func TestConcatConfig(t *testing.T) { 11 | assert.Nil(t, concatConfig(nil, nil), "concat(NIL,NIL)") 12 | assert.Equal(t, &Config{CoverThreshold: ptr.Int(1)}, concatConfig(&Config{CoverThreshold: ptr.Int(1)}, nil), "concat(VAL, NIL)") 13 | assert.Equal(t, &Config{CoverThreshold: ptr.Int(1)}, concatConfig(&Config{CoverThreshold: ptr.Int(1)}, &Config{CoverThreshold: ptr.Int(2)}), "concat(VAL, VAL)") 14 | assert.Equal(t, &Config{CoverThreshold: ptr.Int(2)}, concatConfig(nil, &Config{CoverThreshold: ptr.Int(2)}), "concat(NIL, VAL)") 15 | } 16 | 17 | func TestActualConfig(t *testing.T) { 18 | t.Run("initial actual config", func(t *testing.T) { 19 | act := actualConfig(nil) 20 | if act == nil { 21 | t.Error("expected that the actual config never be nil, but not") 22 | t.FailNow() 23 | } 24 | if act.BuildStyle == nil { 25 | t.Error("expect that any property of the actual config never be nil, but not") 26 | } 27 | }) 28 | 29 | t.Run("actual config will save a value", func(t *testing.T) { 30 | act := actualConfig(&Config{ 31 | CoverThreshold: ptr.Int(10), 32 | }) 33 | if act == nil { 34 | t.Error("expect that the actual config never be nil, but not") 35 | t.FailNow() 36 | } 37 | if act.CoverThreshold == nil { 38 | t.Error("expect that any property of the actual config never be nil, but not") 39 | t.FailNow() 40 | } 41 | if *act.CoverThreshold != 10 { 42 | t.Error("expect that the actual config will save a value, but not") 43 | } 44 | }) 45 | 46 | } 47 | 48 | func TestConcatColor(t *testing.T) { 49 | color := func(n uint8) *Color { 50 | return &Color{Type: ColorType8Bit, Value8: n} 51 | } 52 | assert.Nil(t, concatColor(nil, nil), "concat(NIL,NIL)") 53 | assert.Equal(t, color(1), concatColor(color(1), nil), "concat(VAL, NIL)") 54 | assert.Equal(t, color(1), concatColor(color(1), color(2)), "concat(VAL, VAL)") 55 | assert.Equal(t, color(2), concatColor(nil, color(2)), "concat(NIL, VAL)") 56 | } 57 | 58 | func TestActualColor(t *testing.T) { 59 | color := func(n uint8) *Color { 60 | return &Color{Type: ColorType8Bit, Value8: n} 61 | } 62 | assert.Equal(t, &Color{Type: ColorTypeNone}, actualColor(nil), "actual(NIL)") 63 | assert.Equal(t, color(1), actualColor(color(1)), "actual(VAL)") 64 | } 65 | 66 | func TestConcatLabelType(t *testing.T) { 67 | foo := (*LabelType)(ptr.String("foo")) 68 | bar := (*LabelType)(ptr.String("bar")) 69 | assert.Nil(t, concatLabelType(nil, nil), "concat(NIL,NIL)") 70 | assert.Equal(t, foo, concatLabelType(foo, nil), "concat(VAL, NIL)") 71 | assert.Equal(t, foo, concatLabelType(foo, bar), "concat(VAL, VAL)") 72 | assert.Equal(t, bar, concatLabelType(nil, bar), "concat(NIL, VAL)") 73 | } 74 | 75 | func TestActualLabelType(t *testing.T) { 76 | long := LabelTypeLong 77 | foo := (*LabelType)(ptr.String("foo")) 78 | assert.Equal(t, &long, actualLabelType(nil), "actual(NIL)") 79 | assert.Equal(t, foo, actualLabelType(foo), "actual(VAL)") 80 | } 81 | 82 | func TestConcatStyle(t *testing.T) { 83 | style := func(n uint8) *Style { 84 | return &Style{Foreground: &Color{Type: ColorType8Bit, Value8: n}} 85 | } 86 | assert.Nil(t, concatStyle(nil, nil), "concat(NIL,NIL)") 87 | assert.Equal(t, style(1), concatStyle(style(1), nil), "concat(VAL, NIL)") 88 | assert.Equal(t, style(1), concatStyle(style(1), style(2)), "concat(VAL, VAL)") 89 | assert.Equal(t, style(2), concatStyle(nil, style(2)), "concat(NIL, VAL)") 90 | } 91 | 92 | func TestConcatInt(t *testing.T) { 93 | assert.Nil(t, concatInt(nil, nil), "concat(NIL,NIL)") 94 | assert.Equal(t, ptr.Int(1), concatInt(ptr.Int(1), nil), "concat(VAL, NIL)") 95 | assert.Equal(t, ptr.Int(1), concatInt(ptr.Int(1), ptr.Int(2)), "concat(VAL, VAL)") 96 | assert.Equal(t, ptr.Int(2), concatInt(nil, ptr.Int(2)), "concat(NIL, VAL)") 97 | } 98 | 99 | func TestActualInt(t *testing.T) { 100 | assert.Equal(t, ptr.Int(0), actualInt(nil), "actual(NIL)") 101 | assert.Equal(t, ptr.Int(1), actualInt(ptr.Int(1)), "actual(VAL)") 102 | } 103 | 104 | func TestConcatBool(t *testing.T) { 105 | assert.Nil(t, concatBool(nil, nil), "concat(NIL,NIL)") 106 | assert.Equal(t, ptr.Bool(true), concatBool(ptr.Bool(true), nil), "concat(VAL, NIL)") 107 | assert.Equal(t, ptr.Bool(true), concatBool(ptr.Bool(true), ptr.Bool(false)), "concat(VAL, VAL)") 108 | assert.Equal(t, ptr.Bool(false), concatBool(nil, ptr.Bool(false)), "concat(NIL, VAL)") 109 | } 110 | 111 | func TestActualBool(t *testing.T) { 112 | assert.Equal(t, ptr.Bool(false), actualBool(nil), "actual(NIL)") 113 | assert.Equal(t, ptr.Bool(true), actualBool(ptr.Bool(true)), "actual(VAL)") 114 | } 115 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config holds settings for richgo 4 | type Config struct { 5 | LabelType *LabelType `json:"labelType,omitempty" yaml:"labelType,omitempty"` 6 | 7 | BuildStyle *Style `json:"buildStyle,omitempty" yaml:"buildStyle,omitempty"` 8 | StartStyle *Style `json:"startStyle,omitempty" yaml:"startStyle,omitempty"` 9 | PassStyle *Style `json:"passStyle,omitempty" yaml:"passStyle,omitempty"` 10 | FailStyle *Style `json:"failStyle,omitempty" yaml:"failStyle,omitempty"` 11 | SkipStyle *Style `json:"skipStyle,omitempty" yaml:"skipStyle,omitempty"` 12 | FileStyle *Style `json:"fileStyle,omitempty" yaml:"fileStyle,omitempty"` 13 | LineStyle *Style `json:"lineStyle,omitempty" yaml:"lineStyle,omitempty"` 14 | 15 | PassPackageStyle *Style `json:"passPackageStyle,omitempty" yaml:"passPackageStyle,omitempty"` 16 | FailPackageStyle *Style `json:"failPackageStyle,omitempty" yaml:"failPackageStyle,omitempty"` 17 | 18 | CoverThreshold *int `json:"coverThreshold,omitempty" yaml:"coverThreshold,omitempty"` 19 | CoveredStyle *Style `json:"coveredStyle,omitempty" yaml:"coveredStyle,omitempty"` 20 | UncoveredStyle *Style `json:"uncoveredStyle,omitempty" yaml:"uncoveredStyle,omitempty"` 21 | 22 | Removals []string `json:"removals,omitempty" yaml:"removals,omitempty"` 23 | 24 | LeaveTestPrefix *bool `json:"leaveTestPrefix,omitempty" yaml:"leaveTestPrefix,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/wacul/ptr" 4 | 5 | var defaultConfig Config 6 | 7 | func init() { 8 | var long = LabelTypeLong 9 | defaultConfig = Config{ 10 | LabelType: &long, 11 | BuildStyle: &Style{ 12 | Bold: ptr.Bool(true), 13 | Foreground: &Color{ 14 | Type: ColorTypeName, 15 | Name: Yellow, 16 | }, 17 | }, 18 | StartStyle: &Style{ 19 | Foreground: &Color{ 20 | Type: ColorTypeName, 21 | Name: LightBlack, 22 | }, 23 | }, 24 | PassStyle: &Style{ 25 | Foreground: &Color{ 26 | Type: ColorTypeName, 27 | Name: Green, 28 | }, 29 | }, 30 | FailStyle: &Style{ 31 | Bold: ptr.Bool(true), 32 | Foreground: &Color{ 33 | Type: ColorTypeName, 34 | Name: Red, 35 | }, 36 | }, 37 | SkipStyle: &Style{ 38 | Foreground: &Color{ 39 | Type: ColorTypeName, 40 | Name: LightBlack, 41 | }, 42 | }, 43 | CoverThreshold: ptr.Int(50), 44 | CoveredStyle: &Style{ 45 | Foreground: &Color{ 46 | Type: ColorTypeName, 47 | Name: Green, 48 | }, 49 | }, 50 | UncoveredStyle: &Style{ 51 | Bold: ptr.Bool(true), 52 | Foreground: &Color{ 53 | Type: ColorTypeName, 54 | Name: Yellow, 55 | }, 56 | }, 57 | FileStyle: &Style{ 58 | Foreground: &Color{ 59 | Type: ColorTypeName, 60 | Name: Cyan, 61 | }, 62 | }, 63 | LineStyle: &Style{ 64 | Foreground: &Color{ 65 | Type: ColorTypeName, 66 | Name: Magenta, 67 | }, 68 | }, 69 | PassPackageStyle: &Style{ 70 | Foreground: &Color{ 71 | Type: ColorTypeName, 72 | Name: Green, 73 | }, 74 | Hide: ptr.True(), 75 | }, 76 | FailPackageStyle: &Style{ 77 | Hide: ptr.True(), 78 | Bold: ptr.Bool(true), 79 | Foreground: &Color{ 80 | Type: ColorTypeName, 81 | Name: Red, 82 | }, 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /config/label_type.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | // LabelType is the type of line-labels 6 | type LabelType string 7 | 8 | const ( 9 | // LabelTypeNone suppress line-labels 10 | LabelTypeNone = LabelType("none") 11 | // LabelTypeShort prints single-character line-label 12 | LabelTypeShort = LabelType("short") 13 | // LabelTypeLong prints text line-label 14 | LabelTypeLong = LabelType("long") 15 | ) 16 | 17 | func (l LabelType) String() string { return string(l) } 18 | 19 | // MarshalJSON implements Marshaler 20 | func (l LabelType) MarshalJSON() ([]byte, error) { 21 | return []byte(fmt.Sprintf("%q", l)), nil 22 | } 23 | 24 | // UnmarshalJSON implements Unmarshaler 25 | func (l *LabelType) UnmarshalJSON(raw []byte) error { 26 | switch str := string(raw); str { 27 | case `"none"`: 28 | *l = LabelTypeNone 29 | case `"short"`: 30 | *l = LabelTypeShort 31 | case `"long"`: 32 | *l = LabelTypeLong 33 | default: 34 | return fmt.Errorf("invalid LabelType %s", str) 35 | } 36 | return nil 37 | } 38 | 39 | // LabelTypes defines the possible values of LabelType 40 | func LabelTypes() []LabelType { 41 | return []LabelType{ 42 | LabelTypeNone, 43 | LabelTypeShort, 44 | LabelTypeLong, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/label_type_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLabelTypeMarshaling(t *testing.T) { 11 | type container struct { 12 | L LabelType 13 | } 14 | for _, l := range LabelTypes() { 15 | var d container 16 | raw, err := json.Marshal(container{l}) 17 | if assert.NoError(t, err, "marshaling", l) { 18 | if err := json.Unmarshal(raw, &d); assert.NoError(t, err, "unmarshaling", l) { 19 | assert.Equal(t, l, d.L, "idenpocy of", l) 20 | } 21 | } 22 | } 23 | assert.Error(t, json.Unmarshal([]byte(`{"L":null}`), &container{}), "nil body") 24 | assert.Error(t, json.Unmarshal([]byte(`{"L":""}`), &container{}), "empty body") 25 | } 26 | -------------------------------------------------------------------------------- /config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go/build" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/kyoh86/xdg" 10 | yaml "gopkg.in/yaml.v2" 11 | ) 12 | 13 | const ( 14 | // Filename is filename of configurations for this app. 15 | Filename = ".richstyle" 16 | // LocalOnlyEnvName is the name of environment variable 17 | // to stop searching configuration files excepting current directory. 18 | LocalOnlyEnvName = "RICHGO_LOCAL" 19 | ) 20 | 21 | var ( 22 | // Extensions is extension choices of configurations for this app. 23 | Extensions = []string{ 24 | "", 25 | ".yaml", 26 | ".yml", 27 | } 28 | // C is global configuration 29 | C Config 30 | ) 31 | 32 | func loadableSources() []string { 33 | dirs := []string{} 34 | 35 | if dir, err := os.Getwd(); err == nil { 36 | dirs = append(dirs, dir) 37 | } 38 | 39 | localOnly := os.Getenv(LocalOnlyEnvName) 40 | if localOnly != "1" { 41 | dirs = append(dirs, build.Default.GOPATH) 42 | dirs = appendIndirect(dirs, getEnvPath("GOROOT")) 43 | if xdgHome := xdg.ConfigHome(); xdgHome != "" { 44 | dirs = append(dirs, xdgHome) 45 | } 46 | dirs = appendIndirect(dirs, getEnvPath("HOME")) 47 | } 48 | 49 | paths := make([]string, 0, len(dirs)*len(Extensions)) 50 | for _, d := range dirs { 51 | for _, e := range Extensions { 52 | paths = append(paths, filepath.Join(d, Filename+e)) 53 | } 54 | } 55 | return paths 56 | } 57 | 58 | var loadForTest func(path string) ([]byte, error) 59 | 60 | func load(path string) ([]byte, error) { 61 | if loadForTest != nil { 62 | return loadForTest(path) 63 | } 64 | return os.ReadFile(path) 65 | } 66 | 67 | // Load configurations from file 68 | func Load() { 69 | paths := loadableSources() 70 | c := &defaultConfig 71 | for _, p := range paths { 72 | data, err := load(p) 73 | if err != nil { 74 | if !os.IsNotExist(err) { 75 | log.Println("error reading from", p, ": ", err) 76 | } 77 | continue 78 | } 79 | var loaded Config 80 | if err := yaml.Unmarshal(data, &loaded); err != nil { 81 | log.Println("error unmarshaling yaml from", p, ": ", err) 82 | continue 83 | } 84 | c = concatConfig(&loaded, c) 85 | } 86 | C = *actualConfig(c) 87 | } 88 | 89 | // Default is the default configuration 90 | func Default() { 91 | C = *actualConfig(&defaultConfig) 92 | } 93 | 94 | func appendIndirect(arr []string, ptr *string) []string { 95 | if ptr != nil { 96 | return append(arr, *ptr) 97 | } 98 | return arr 99 | } 100 | 101 | func getEnvPath(envName string) *string { 102 | envPath := os.Getenv(envName) 103 | if envPath == "" { 104 | return nil 105 | } 106 | if isDir(envPath) { 107 | return &envPath 108 | } 109 | return nil 110 | } 111 | 112 | var isDirForTest func(path string) bool 113 | 114 | func isDir(path string) bool { 115 | if isDirForTest != nil { 116 | return isDirForTest(path) 117 | } 118 | if stat, err := os.Stat(path); err == nil && stat.IsDir() { 119 | return true 120 | } 121 | return false 122 | } 123 | -------------------------------------------------------------------------------- /config/load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/wacul/ptr" 13 | yaml "gopkg.in/yaml.v2" 14 | ) 15 | 16 | func TestInGlobal(t *testing.T) { 17 | os.Setenv("HOME", "/") 18 | os.Setenv("GOPATH", "/") 19 | os.Setenv("GOROOT", "/foo/bar/path/not/exists") 20 | os.Setenv("PWD", "/home/kyoh86/go/src/github.com/kyoh86/richgo") 21 | err := os.Setenv(LocalOnlyEnvName, "0") 22 | if err != nil { 23 | t.Errorf("failed to set env: %s", err) 24 | t.FailNow() 25 | } 26 | sources := loadableSources() 27 | if len(sources) == 0 { 28 | t.Errorf("failed to get loadable sources") 29 | } 30 | } 31 | 32 | func TestInGlobalWithNotCoveredEnv(t *testing.T) { 33 | os.Clearenv() 34 | os.Setenv("HOME", "/home/kyoh86") 35 | os.Setenv("GOPATH", "/home/kyoh86/go") 36 | os.Unsetenv("GOROOT") 37 | os.Setenv("PWD", "/home/kyoh86/go/src/github.com/kyoh86/richgo") 38 | isDirForTest = func(path string) bool { 39 | return len(path) > 0 40 | } 41 | defer func() { isDirForTest = nil }() 42 | err := os.Setenv(LocalOnlyEnvName, "0") 43 | if err != nil { 44 | t.Errorf("failed to set env: %s", err) 45 | t.FailNow() 46 | } 47 | sources := loadableSources() 48 | if len(sources) == 0 { 49 | t.Errorf("failed to get loadable sources") 50 | } 51 | } 52 | 53 | func TestInLocal(t *testing.T) { 54 | err := os.Setenv(LocalOnlyEnvName, "1") 55 | if err != nil { 56 | t.Errorf("failed to set env: %s", err) 57 | t.FailNow() 58 | } 59 | sources := loadableSources() 60 | if len(sources) == 0 { 61 | t.Errorf("failed to get loadable sources") 62 | } 63 | wd, err := os.Getwd() 64 | if err != nil { 65 | t.Errorf("failed to get working directory: %q", err) 66 | t.FailNow() 67 | } 68 | for _, s := range sources { 69 | rel, err := filepath.Rel(wd, s) 70 | if err != nil { 71 | t.Errorf("failed to get relative path to %q from %q: %q", s, wd, err) 72 | } 73 | if strings.HasPrefix(rel, "..") { 74 | t.Errorf("expect that any source will be in local, but not (%q)", s) 75 | } 76 | } 77 | } 78 | 79 | func TestLoad(t *testing.T) { 80 | t.Run("no trick", func(t *testing.T) { 81 | if err := os.Setenv("PWD", "/home/kyoh86/go/src/github.com/kyoh86/richgo"); err != nil { 82 | t.Errorf("failed to set env: %s", err) 83 | t.FailNow() 84 | } 85 | 86 | if err := os.Setenv(LocalOnlyEnvName, "1"); err != nil { 87 | t.Errorf("failed to set env: %s", err) 88 | t.FailNow() 89 | } 90 | Load() 91 | }) 92 | 93 | t.Run("with valid files", func(t *testing.T) { 94 | if err := os.Setenv("PWD", "/home/kyoh86/go/src/github.com/kyoh86/richgo"); err != nil { 95 | t.Errorf("failed to set env: %s", err) 96 | t.FailNow() 97 | } 98 | 99 | if err := os.Setenv(LocalOnlyEnvName, "1"); err != nil { 100 | t.Errorf("failed to set env: %s", err) 101 | t.FailNow() 102 | } 103 | 104 | loadForTest = func(p string) ([]byte, error) { 105 | return yaml.Marshal(&Config{ 106 | CoverThreshold: ptr.Int(10), 107 | }) 108 | } 109 | 110 | Load() 111 | if C.CoverThreshold == nil { 112 | t.Error("expect that a config loaded correctly but 'CoverThreshold' is nil") 113 | t.FailNow() 114 | } 115 | if *C.CoverThreshold != 10 { 116 | t.Errorf("expect that a 'CoverThreshold' is 10, but %d", *C.CoverThreshold) 117 | } 118 | }) 119 | 120 | t.Run("with invalid file", func(t *testing.T) { 121 | loadForTest = func(p string) ([]byte, error) { 122 | return nil, errors.New("test error") 123 | } 124 | 125 | w := bytes.Buffer{} 126 | log.SetFlags(0) 127 | log.SetOutput(&w) 128 | Load() 129 | if !strings.HasPrefix(w.String(), "error reading from") { 130 | t.Error("expect that a Load func puts error in reading a file") 131 | } 132 | }) 133 | 134 | t.Run("with invalid yaml", func(t *testing.T) { 135 | loadForTest = func(p string) ([]byte, error) { 136 | return []byte(`":`), nil 137 | } 138 | 139 | w := bytes.Buffer{} 140 | log.SetFlags(0) 141 | log.SetOutput(&w) 142 | Load() 143 | if !strings.HasPrefix(w.String(), "error unmarshaling yaml from") { 144 | t.Error("expect that a Load func puts error in unmarshaling a file") 145 | } 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /config/style.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/morikuni/aec" 5 | ) 6 | 7 | // Style format the text with ANSI 8 | type Style struct { 9 | // Hide text 10 | Hide *bool `json:"hide,omitempty" yaml:"hide,omitempty"` 11 | 12 | // Bold set the text style to bold or increased intensity. 13 | Bold *bool `json:"bold,omitempty" yaml:"bold,omitempty"` 14 | // Faint set the text style to faint. 15 | Faint *bool `json:"faint,omitempty" yaml:"faint,omitempty"` 16 | // Italic set the text style to italic. 17 | Italic *bool `json:"italic,omitempty" yaml:"italic,omitempty"` 18 | // Underline set the text style to underline. 19 | Underline *bool `json:"underline,omitempty" yaml:"underline,omitempty"` 20 | // BlinkSlow set the text style to slow blink. 21 | BlinkSlow *bool `json:"blinkSlow,omitempty" yaml:"blinkSlow,omitempty"` 22 | // BlinkRapid set the text style to rapid blink. 23 | BlinkRapid *bool `json:"blinkRapid,omitempty" yaml:"blinkRapid,omitempty"` 24 | // Inverse swap the foreground color and background color. 25 | Inverse *bool `json:"inverse,omitempty" yaml:"inverse,omitempty"` 26 | // Conceal set the text style to conceal. 27 | Conceal *bool `json:"conceal,omitempty" yaml:"conceal,omitempty"` 28 | // CrossOut set the text style to crossed out. 29 | CrossOut *bool `json:"crossOut,omitempty" yaml:"crossOut,omitempty"` 30 | // Frame set the text style to framed. 31 | Frame *bool `json:"frame,omitempty" yaml:"frame,omitempty"` 32 | // Encircle set the text style to encircled. 33 | Encircle *bool `json:"encircle,omitempty" yaml:"encircle,omitempty"` 34 | // Overline set the text style to overlined. 35 | Overline *bool `json:"overline,omitempty" yaml:"overline,omitempty"` 36 | 37 | // Foreground set the fore-color of text 38 | Foreground *Color `json:"foreground,omitempty" yaml:"foreground,omitempty"` 39 | // Foreground set the back-color of text 40 | Background *Color `json:"background,omitempty" yaml:"background,omitempty"` 41 | } 42 | 43 | // ANSI get the ANSI string 44 | func (s *Style) ANSI() aec.ANSI { 45 | if s == nil { 46 | return emptyColor // when a prevLineStyle is not set, editor/test/test.go calls it in nil 47 | } 48 | 49 | ansi := s.Background.B() 50 | ansi = ansi.With(s.Foreground.F()) 51 | for _, style := range []struct { 52 | flag *bool 53 | ansi aec.ANSI 54 | }{ 55 | {s.Bold, aec.Bold}, 56 | {s.Faint, aec.Faint}, 57 | {s.Italic, aec.Italic}, 58 | {s.Underline, aec.Underline}, 59 | {s.BlinkSlow, aec.BlinkSlow}, 60 | {s.BlinkRapid, aec.BlinkRapid}, 61 | {s.Inverse, aec.Inverse}, 62 | {s.Conceal, aec.Conceal}, 63 | {s.CrossOut, aec.CrossOut}, 64 | {s.Frame, aec.Frame}, 65 | {s.Encircle, aec.Encircle}, 66 | {s.Overline, aec.Overline}, 67 | } { 68 | if *style.flag { 69 | ansi = ansi.With(style.ansi) 70 | } 71 | } 72 | return ansi 73 | } 74 | 75 | // Apply style To string 76 | func (s *Style) Apply(str string) string { 77 | if s == nil { 78 | return str 79 | } 80 | 81 | if s.Hide != nil && *s.Hide { 82 | return "" 83 | } 84 | 85 | ansi := s.ANSI() 86 | if ansi == emptyColor { 87 | return str 88 | } 89 | 90 | if len(ansi.String()) == 0 { 91 | return str 92 | } 93 | 94 | return aec.Apply(str, ansi) 95 | } 96 | -------------------------------------------------------------------------------- /config/style_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/morikuni/aec" 7 | "github.com/wacul/ptr" 8 | ) 9 | 10 | func TestApply(t *testing.T) { 11 | const base = "abc" 12 | for exp, style := range map[string]*Style{ 13 | aec.RedF.Apply(base): actualStyle(&Style{ 14 | Foreground: &Color{Type: ColorTypeName, Name: Red}, 15 | }), 16 | aec.Bold.Apply(base): actualStyle(&Style{ 17 | Bold: ptr.Bool(true), 18 | }), 19 | "": actualStyle(&Style{ 20 | Hide: ptr.Bool(true), 21 | }), 22 | base: actualStyle(nil), 23 | } { 24 | act := style.Apply(base) 25 | if exp != act { 26 | t.Errorf("expect that a style applied as %q, but %q", exp, act) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /editor/editor.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Editor is the line-processor interface 9 | type Editor interface { 10 | Edit(line string) (string, error) 11 | } 12 | 13 | // Stream build io.WriteCloser that process lines with editor and write to base io.Writer 14 | func Stream(base io.Writer, editor ...Editor) io.WriteCloser { 15 | return &stream{editors: editor, base: base} 16 | } 17 | 18 | type stream struct { 19 | editors []Editor 20 | base io.Writer 21 | buffer []byte 22 | } 23 | 24 | func (s *stream) writeLines(lines [][]byte) error { 25 | for _, line := range lines { 26 | line := bytes.TrimSuffix(line, []byte{'\r'}) 27 | text := string(append(line, '\n')) 28 | for _, e := range s.editors { 29 | t, err := e.Edit(text) 30 | if err != nil { 31 | return err 32 | } 33 | text = t 34 | } 35 | if _, err := s.base.Write([]byte(text)); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func (s *stream) Write(b []byte) (int, error) { 43 | lines := bytes.Split(append(s.buffer, b...), []byte("\n")) 44 | s.buffer = lines[len(lines)-1] 45 | lines = lines[:len(lines)-1] 46 | if err := s.writeLines(lines); err != nil { 47 | return 0, err 48 | } 49 | return len(b), nil 50 | } 51 | 52 | func (s *stream) Close() error { 53 | lines := bytes.Split(s.buffer, []byte(`\n`)) 54 | s.buffer = nil 55 | return s.writeLines(lines) 56 | } 57 | -------------------------------------------------------------------------------- /editor/editor_test.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type editorFunc func(string) (string, error) 13 | 14 | func (f editorFunc) Edit(line string) (string, error) { 15 | return f(line) 16 | } 17 | 18 | func addPrefixEditor(prefix string) Editor { 19 | return editorFunc(func(line string) (string, error) { 20 | return prefix + line, nil 21 | }) 22 | } 23 | 24 | func TestWriter(t *testing.T) { 25 | t.Run("valid editor and writer", func(t *testing.T) { 26 | var buffer bytes.Buffer 27 | stream := Stream(&buffer, addPrefixEditor("A"), addPrefixEditor("B")) 28 | { 29 | _, err := stream.Write([]byte("ab")) 30 | require.NoError(t, err, "write in stream") 31 | assert.Empty(t, buffer.Bytes()) 32 | } 33 | { 34 | _, err := stream.Write([]byte("\ncd")) 35 | require.NoError(t, err, "write in stream") 36 | assert.Equal(t, "BAab\n", buffer.String()) 37 | } 38 | { 39 | require.NoError(t, stream.Close(), "close a stream") 40 | // It's undesirable spec :( 41 | // A following newline is unnecessary. 42 | assert.Equal(t, "BAab\nBAcd\n", buffer.String()) 43 | } 44 | }) 45 | t.Run("invalid editor", func(t *testing.T) { 46 | var buffer bytes.Buffer 47 | stream := Stream(&buffer, editorFunc(func(string) (string, error) { return "", errors.New("test") })) 48 | { 49 | _, err := stream.Write([]byte("ab")) 50 | require.NoError(t, err, "write in stream") 51 | assert.Empty(t, buffer.Bytes()) 52 | } 53 | { 54 | _, err := stream.Write([]byte("\ncd")) 55 | require.EqualError(t, err, "test") 56 | assert.Empty(t, buffer.Bytes()) 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /editor/parrot.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | // Parrot will not change input 4 | func Parrot() Editor { 5 | return &parrot{} 6 | } 7 | 8 | // parrot through output raw. 9 | type parrot struct{} 10 | 11 | func (e *parrot) Edit(line string) (string, error) { 12 | return line, nil 13 | } 14 | -------------------------------------------------------------------------------- /editor/parrot_test.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParrot(t *testing.T) { 10 | const src = "18wRDgGPuvsy6egaevFx" 11 | parrot := Parrot() 12 | out, err := parrot.Edit(src) 13 | if assert.NoError(t, err) { 14 | assert.Equal(t, src, out) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /editor/regexp_flow.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import "regexp" 4 | 5 | // RegexRepl : replacer 6 | type RegexRepl struct { 7 | Exp *regexp.Regexp 8 | Repl string 9 | Func func(string) string 10 | } 11 | 12 | // Replaces string with 13 | func Replaces(str string, rs ...RegexRepl) string { 14 | for _, r := range rs { 15 | x := r.Exp 16 | switch { 17 | case r.Func != nil: 18 | str = x.ReplaceAllStringFunc(str, r.Func) 19 | default: 20 | str = x.ReplaceAllString(str, r.Repl) 21 | } 22 | } 23 | return str 24 | } 25 | -------------------------------------------------------------------------------- /editor/regexp_flow_test.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReplaces(t *testing.T) { 12 | assert.Equal(t, 13 | "A!A!A!-D!E!L!A!X!-C!C!C!", 14 | Replaces( 15 | "aaabbbccc", 16 | RegexRepl{ 17 | Exp: regexp.MustCompile(`b{3}`), 18 | Repl: "-delax-", 19 | }, 20 | RegexRepl{ 21 | Exp: regexp.MustCompile(`\w`), 22 | Func: func(s string) string { 23 | return strings.ToUpper(s) 24 | }, 25 | }, 26 | RegexRepl{ 27 | Exp: regexp.MustCompile(`\w`), 28 | Repl: "x", 29 | Func: func(s string) string { 30 | return s + "!" 31 | }, 32 | }, 33 | ), 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /editor/test/e2e_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/kyoh86/richgo/config" 10 | _ "github.com/kyoh86/richgo/editor/test/statik" 11 | "github.com/rakyll/statik/fs" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestE2E(t *testing.T) { 17 | mustAsset := func(t *testing.T, name string) []byte { 18 | statikFS, err := fs.New() 19 | if err != nil { 20 | t.Fatalf("failed to init statik FS: %s", err.Error()) 21 | } 22 | 23 | // Access individual files by their paths. 24 | r, err := statikFS.Open(name) 25 | if err != nil { 26 | t.Fatalf("failed to open %s: %s", name, err) 27 | } 28 | defer r.Close() 29 | buf, err := io.ReadAll(r) 30 | if err != nil { 31 | t.Fatalf("failed to load %s: %s", name, err) 32 | } 33 | return buf 34 | } 35 | raws := bytes.Split(mustAsset(t, "/out_raw.txt"), []byte("\n")) 36 | exps := bytes.Split(mustAsset(t, "/out_colored.txt"), []byte("\n")) 37 | 38 | config.Default() 39 | editor := New() 40 | var expi int 41 | for _, raw := range raws { 42 | act, err := editor.Edit(string(raw)) 43 | require.NoError(t, err) 44 | for _, line := range strings.Split(act, "\n") { 45 | if len(line) > 0 { 46 | require.True(t, len(exps) > expi, "should have length more than", expi) 47 | assert.Equal(t, string(exps[expi]), line, "at line %d", expi+1) 48 | expi++ 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /editor/test/label_set.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/kyoh86/richgo/config" 7 | ) 8 | 9 | type labelSet struct { 10 | build string 11 | start string 12 | pass string 13 | fail string 14 | skip string 15 | cover string 16 | anonym string 17 | } 18 | 19 | var labelSets = map[config.LabelType]labelSet{ 20 | config.LabelTypeLong: newLabelSet("| ", "BUILD", "START", "PASS", "FAIL", "SKIP", "COVER"), 21 | config.LabelTypeShort: newLabelSet(" ", "!!", ">", "o", "x", "-", "%"), 22 | config.LabelTypeNone: newLabelSet("", "", "", "", "", "", ""), 23 | } 24 | 25 | func labels() labelSet { 26 | return labelSets[*config.C.LabelType] 27 | } 28 | 29 | func maxInt(numbers ...int) int { 30 | m := 0 31 | for _, n := range numbers { 32 | if m < n { 33 | m = n 34 | } 35 | } 36 | return m 37 | } 38 | 39 | func newLabelSet(suffix, build, start, pass, fail, skip, cover string) labelSet { 40 | max := maxInt( 41 | len(build), 42 | len(start), 43 | len(pass), 44 | len(fail), 45 | len(skip), 46 | len(cover), 47 | ) 48 | 49 | anonym := strings.Repeat(" ", max) 50 | return labelSet{ 51 | build: string((build + anonym)[:max]) + suffix, 52 | start: string((start + anonym)[:max]) + suffix, 53 | pass: string((pass + anonym)[:max]) + suffix, 54 | fail: string((fail + anonym)[:max]) + suffix, 55 | skip: string((skip + anonym)[:max]) + suffix, 56 | cover: string((cover + anonym)[:max]) + suffix, 57 | anonym: anonym + suffix, 58 | } 59 | } 60 | 61 | func (s labelSet) Build() string { 62 | return s.build 63 | } 64 | func (s labelSet) Start() string { 65 | return s.start 66 | } 67 | func (s labelSet) Pass() string { 68 | return s.pass 69 | } 70 | func (s labelSet) Fail() string { 71 | return s.fail 72 | } 73 | func (s labelSet) Skip() string { 74 | return s.skip 75 | } 76 | func (s labelSet) Cover() string { 77 | return s.cover 78 | } 79 | func (s labelSet) Anonym() string { 80 | return s.anonym 81 | } 82 | -------------------------------------------------------------------------------- /editor/test/label_set_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | -------------------------------------------------------------------------------- /editor/test/statik/statik.go: -------------------------------------------------------------------------------- 1 | // Code generated by statik. DO NOT EDIT. 2 | 3 | package statik 4 | 5 | import ( 6 | "github.com/rakyll/statik/fs" 7 | ) 8 | 9 | 10 | func init() { 11 | data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\xb9\x8a\xc3R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00out_colored.txtUT\x05\x00\x01\xae\x0f\xb9`\xac\x95\xdfn\xda<\x18\xc6\x8f\xdb[\xe0\xe4\xfd\xf4i\x1a\xdd\xd4\xe04\xa2-9c\xdd:!\xaaR5l'\x11B!1\xc1\xc2\x8e#\xe2lC\xe2\xe2''\x86\xa4\x89C\x18\x9b\xc5\x81\x89\xdf?\xbf\xe7Ilw\\\xcbb\x1d\xd7d\x9f\xbe\x8d\x9e>\xef $b\x95.\x0c\x9f\xb3\xdez\xcbW\xf7\xb7\xbd\x0d\xf1W!\xef%\x1e\x8b)\xee-RB\x83\xa5G(\xb8'\x87\x1a\x02'b\xd6q\x11\xbb<\xb4\x039v\xd0q\xad[V\x8d/fs\x99i\x84\\\xe6v\\\xab\xcf\xec{\xfbN\xfd\xc9\xeb\xd8 \x8cG\xce!\x8d\x02\xbc$\x11\x0e\xa0+\xb61\x86\x0f2\x93D\xa11\x85\x95\x97@\xc4aI0\x0d\x80o\x80a\xb1\xe2\x01\xa2\x81\x8d\xd0yt}\xad\xc9\x05\xa2u\xc3^\x86\x8e\x03\x85\x0bU_\x0f\x01:\x0d-\x9e:k\x12\xb7r'k\x12kvL \xbcT}\xd4\xfa^\xd0_\xf0\xbe\xf9\x9c\x1b\xccU\xdd4\xcc\xb9\xac#T-:\xa7\x84a\x9e\x8aF\x9dj}_\xf1mx\x15S\xadB\xd7:\xfe!\xe8\x8b\xd6\xd3\x94\x8f>\xff\x817^\x88mp#\x0e\x89\xf0\x04f8\x12\xc9L\xbb\x83/Z\xce\xe9\x0b\xd9\xc5J\xce\xca-_\x07\xd9\x14\xe4\x1c\x07\xb3\x9a\x7f\x0f\x92\x19\xf5k\x16\xa8\xe7\xf5\xbd\xf20\xf9\xfe\xe5u\x07}d\xa0w\xe0\xfe/\xc7\\\x8eY\xadD\x1b\xa3\xafZ\x14\xb6\xe5E\xf9\xb2\xe4\x9d\x1exH\xa9\x9exHi#\xb2\x89J\xcc\xd98\x93\xd9\xa3\xb4\x04\xad\xca\xea\xa8\x8bOC\xddz6\xfc\xf46Q6\x89x\xf60\x01\xc1a\x93F\xfb\x8c\xfc\xf6T\xc49\xef\xfc0\xfe\x9c\x17\xb3Xle\x9f\x12\xf0 &?\xf3,\xbc\xf9\xa4\x88\xf2\x80\xe3\xa7\x9a\xe7\xfb8\xae\xefAU\xbc\xfa\x9e\xfe\xb1rEx\xaa\xee\xfc|j/*\x05_\xb8\xea\xe5\xc1\x92P\xac\xb67\xfeEDV:M\xe0\xe6\xf2w\x00\x00\x00\xff\xffPK\x07\x08Aj\xd0\xbcr\x02\x00\x00\xbf \x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xf1\x8a\xc3R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00 \x00out_raw.txtUT\x05\x00\x01\x17\x10\xb9`\xa4\x94Q\xab\xda0\x14\xc7\x9f\xed\xa78c\x8c\xe9\x865\x9d\xb8{o@\x86\x0c\x1c\xe2\xd0\xcb\xea\x9e.r\xa9ml\x83iRL\xba\xe1\xb7\x1f\x89\xad\xd6\x9aV\xb9\x03\x1fB\xce9\xff\xff\xef\x1cO\xf3\x1eb\xaa\x92|\xe3\x86\"\x1d\xec\x0e\"y\xfc:\xd8\xd30\x89\xc5@\x06i\xc6\xc8`\x93S\x16m\x03\xca\xe0\xe5\xeeTW\x11\xa9\xd6N\xfd\xfa|z\xd5 n,\xf0#~\xc0\xa0\xdc\xa9\x10\x90\xf3\x88l)'\x11t\xd5!#\xf0I\xe7P\x1e\xbb+H\x02 \\\xc0\x96\x12\x16\x81\xd8CJT\"\"\x98\n\xd1s\xc6\xe31\xfc\xfa\xbd\x00\x80\x15\x91\xca7\x96\x8b\x1f\x0e\x00\xc0\xd1\xff\x95\xc7'\xbb'\x0c3\xf5Q\x8b)X\xce]\xd7\x05\xdcmP\x18\xf8\xf9F\x975jy\xc3B,`R\\*\xf6\xfb}\x98Nf?\xf1\x85\"t\x91\x8b\x90\xec\x19={\xca\xd9\xf4\x94l\xa3[\xce\xabLbwf\xf2\n\xa6\xe5\xfc]Cei\xb1\x9c;\x8a\xa6\x04\x7fA\xdeC\x1fy}\xe4\xad\x90\x87\xcd\xef3z\xc2\x085Y\x8c\xaamk\x1f\xdd\xca\xf3\xc4\xf7\xf1\x85\xcfU\xb7\xd7)g\x94\xd6n\xfd\x1d\xcd\xaa0rG\xb3\xca\x02\x81\x01\xf0\xe7\xb3g\\+\xba\xa9\xea\xe7\x9b\xb6X\xc9\xd7\n\xa0\xd7\xc0:\x82B\xe4j\x0e6\xd2\x9aY+\xf8\x8a\xa6D\xe4\xaa-Vj\x95\xa96\xbc\"\x06\xdda\xfb\xdfd\x97<\x95\xe9\x1dvB\xf1\x87\xec\x83\x98`x\xe1\x02\xa4\n\x14I Wrm\xc2\x9d\x1b\xefFGK\x0d\xe5]\xb9\xd5\xe7\xc8\x1cA\x9fI\xb4\xae\x8d\xe3\xbb&B\xa3Z\xe7\xc5\xedi\xbc:R\x81\x1f!\x17}\x00\xb1\xadt\xe0\x88\x1d\xc0M\xa8\xf0\xa8\xdb\xe9\x86A\x98\x90\xa8\xd7i\xd7\xb4\xa0N\x18\xb3\xb1N\x18k\x82\xf5\xd0\x7f\xd1\x06\x8c\xd9p\xad\xaa\xc5C\x8c\xe1o\xb0\xe7\xe6\xc0\x05\xe8K J\xc0>\xe7u\xb6\xb7\x92\x914S\x07-lC\xbb\x165\xdbV\xe5\xa8o\xc1B\x98r\xb3\xdb\xfcx\xae>\x1cA\x18\x92\xac\xfeu\x145Mc\x7fkk\x85\xfd}\x8d9\xdf\xe0.I3\xaar\x08\xb0\xa5\x8c\x14\x9f\x9c\xf3/\x00\x00\xff\xffPK\x07\x08\xb6\x05\x0f\x1b\"\x02\x00\x00\xd8\x07\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xb9\x8a\xc3RAj\xd0\xbcr\x02\x00\x00\xbf \x00\x00\x0f\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00out_colored.txtUT\x05\x00\x01\xae\x0f\xb9`PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xf1\x8a\xc3R\xb6\x05\x0f\x1b\"\x02\x00\x00\xd8\x07\x00\x00\x0b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xb8\x02\x00\x00out_raw.txtUT\x05\x00\x01\x17\x10\xb9`PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x88\x00\x00\x00\x1c\x05\x00\x00\x00\x00" 12 | fs.Register(data) 13 | } 14 | -------------------------------------------------------------------------------- /editor/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/kyoh86/richgo/config" 10 | "github.com/kyoh86/richgo/editor" 11 | "github.com/wacul/ptr" 12 | ) 13 | 14 | // New will format lines as `go test` output 15 | func New() editor.Editor { 16 | removals := make([]editor.RegexRepl, 0, len(config.C.Removals)) 17 | for _, r := range config.C.Removals { 18 | removals = append(removals, editor.RegexRepl{ 19 | Exp: regexp.MustCompile(r), 20 | }) 21 | } 22 | return &test{ 23 | additional: removals, 24 | } 25 | } 26 | 27 | // test through output raw. 28 | type test struct { 29 | prevLineStyle *config.Style 30 | additional []editor.RegexRepl 31 | } 32 | 33 | const noTestPattern = `[ \t]+\[(?:no test files|no tests to run)\]` 34 | 35 | var ( 36 | runhead = regexp.MustCompile(`(?m)^=== RUN Test.*`) 37 | passtail = regexp.MustCompile(`(?m)^([ \t]*)--- PASS: Test.*`) 38 | skiptail = regexp.MustCompile(`(?m)^([ \t]*)--- SKIP: Test.*`) 39 | failtail = regexp.MustCompile(`(?m)^([ \t]*)--- FAIL: Test.*`) 40 | passlonely = regexp.MustCompile(`(?m)^PASS[ \t]*$`) 41 | faillonely = regexp.MustCompile(`(?m)^FAIL[ \t]*$`) 42 | 43 | okPath = regexp.MustCompile(`(?m)^ok[ \t]+([^ \t]+)[ \t]*(?:[\d\.]+\w+|\(cached\))?[ \t]*(?:[ \t]+(coverage:[ \t]+\d+\.\d+% of statements)[ \t]*)?(?:` + noTestPattern + `)?$`) 44 | failPath = regexp.MustCompile(`(?m)^FAIL[ \t]+[^ \t]+[ \t]+(?:[\d\.]+\w+|\[build failed\])$`) 45 | notestPath = regexp.MustCompile(`(?m)^\?[ \t]+[^ \t]+` + noTestPattern + `$`) 46 | 47 | coverage = regexp.MustCompile(`(?m)^coverage: ((\d+)\.\d)+% of statements?$`) 48 | 49 | filename = regexp.MustCompile(`(?m)([^\s:]+\.go)((?::\d+){1,2})`) 50 | emptyline = regexp.MustCompile(`(?m)^[ \t]*\r?\n`) 51 | importpath = regexp.MustCompile(`(?m)^# ([^ ]+)(?: \[[^ \[\]]+\])?$`) 52 | 53 | any = regexp.MustCompile(`.*`) 54 | ) 55 | 56 | func (e *test) Edit(line string) (string, error) { 57 | var processed bool 58 | var style *config.Style 59 | edited := editor.Replaces(line, 60 | editor.RegexRepl{ 61 | Exp: importpath, 62 | Func: func(s string) string { 63 | s = strings.TrimPrefix(s, `# `) 64 | processed = true 65 | style = config.C.BuildStyle 66 | return style.Apply(labels().Build() + s) 67 | }, 68 | }, 69 | 70 | editor.RegexRepl{ 71 | Exp: runhead, 72 | Func: func(s string) string { 73 | if *config.C.LeaveTestPrefix { 74 | s = strings.TrimPrefix(s, `=== RUN `) 75 | } else { 76 | s = strings.TrimPrefix(s, `=== RUN Test`) 77 | } 78 | floors := strings.Split(s, `/`) 79 | processed = true 80 | 81 | clone := *config.C.StartStyle 82 | clone.Hide = ptr.Bool(false) 83 | style = &clone 84 | return config.C.StartStyle.Apply(labels().Start() + strings.Repeat(" ", len(floors)-1) + s) 85 | }, 86 | }, 87 | editor.RegexRepl{ 88 | Exp: passtail, 89 | Func: func(s string) string { 90 | s = strings.TrimLeft(s, " ") 91 | if *config.C.LeaveTestPrefix { 92 | s = strings.TrimPrefix(s, `--- PASS: `) 93 | } else { 94 | s = strings.TrimPrefix(s, `--- PASS: Test`) 95 | } 96 | floors := strings.Split(s, `/`) 97 | processed = true 98 | style = config.C.PassStyle 99 | return style.Apply(labels().Pass() + strings.Repeat(" ", len(floors)-1) + s) 100 | }, 101 | }, 102 | editor.RegexRepl{ 103 | Exp: failtail, 104 | Func: func(s string) string { 105 | s = strings.TrimLeft(s, " ") 106 | if *config.C.LeaveTestPrefix { 107 | s = strings.TrimPrefix(s, `--- FAIL: `) 108 | } else { 109 | s = strings.TrimPrefix(s, `--- FAIL: Test`) 110 | } 111 | floors := strings.Split(s, `/`) 112 | processed = true 113 | style = config.C.FailStyle 114 | return style.Apply(labels().Fail() + strings.Repeat(" ", len(floors)-1) + s) 115 | }, 116 | }, 117 | editor.RegexRepl{ 118 | Exp: skiptail, 119 | Func: func(s string) string { 120 | s = strings.TrimLeft(s, " ") 121 | if *config.C.LeaveTestPrefix { 122 | s = strings.TrimPrefix(s, `--- SKIP: `) 123 | } else { 124 | s = strings.TrimPrefix(s, `--- SKIP: Test`) 125 | } 126 | floors := strings.Split(s, `/`) 127 | processed = true 128 | style = config.C.SkipStyle 129 | return style.Apply(labels().Skip() + strings.Repeat(" ", len(floors)-1) + s) 130 | }, 131 | }, 132 | 133 | editor.RegexRepl{ 134 | Exp: okPath, 135 | Func: func(s string) string { 136 | matches := okPath.FindStringSubmatch(s) 137 | processed = true 138 | style = config.C.PassStyle 139 | 140 | ret := style.Apply(labels().Pass() + strings.Join(matches[1:3], " ")) 141 | if len(matches) == 4 { 142 | ret += "\n" + matches[3] 143 | } 144 | return ret 145 | }, 146 | }, 147 | editor.RegexRepl{ 148 | Exp: failPath, 149 | Func: func(s string) string { 150 | s = strings.TrimPrefix(strings.TrimLeft(s, " \t"), `FAIL`) 151 | processed = true 152 | style = config.C.FailStyle 153 | return style.Apply(labels().Fail() + s) 154 | }, 155 | }, 156 | editor.RegexRepl{ 157 | Exp: notestPath, 158 | Func: func(s string) string { 159 | s = strings.TrimLeft(s, " \t?") 160 | processed = true 161 | style = config.C.SkipStyle 162 | return style.Apply(labels().Skip() + s) 163 | }, 164 | }, 165 | 166 | editor.RegexRepl{ 167 | Exp: coverage, 168 | Func: func(s string) string { 169 | matches := coverage.FindStringSubmatch(s) 170 | fill, err := strconv.Atoi(matches[2]) 171 | if err != nil { 172 | panic(err) 173 | } 174 | s = fmt.Sprintf("%s%% [%s%s]", matches[1], strings.Repeat("#", fill/10), strings.Repeat("_", 10-fill/10)) 175 | coverStyle := config.C.CoveredStyle 176 | if fill < *config.C.CoverThreshold { 177 | coverStyle = config.C.UncoveredStyle 178 | } 179 | processed = true 180 | return coverStyle.Apply(labels().Cover()+s) + "\n" 181 | }, 182 | }, 183 | 184 | editor.RegexRepl{ 185 | Exp: passlonely, 186 | Func: func(s string) string { 187 | processed = true 188 | return config.C.PassPackageStyle.Apply("PASS") 189 | }, 190 | }, 191 | editor.RegexRepl{ 192 | Exp: faillonely, 193 | Func: func(s string) string { 194 | processed = true 195 | return config.C.FailPackageStyle.Apply("FAIL") 196 | }, 197 | }, 198 | 199 | editor.RegexRepl{ 200 | Exp: filename, 201 | Func: func(s string) string { 202 | return filename.ReplaceAllString(s, config.C.FileStyle.Apply(`$1`)+config.C.LineStyle.Apply(`$2`)) + e.prevLineStyle.ANSI().String() 203 | }, 204 | }, 205 | ) 206 | 207 | edited = editor.Replaces(edited, e.additional...) 208 | 209 | if !processed { 210 | edited = editor.Replaces(edited, 211 | editor.RegexRepl{ 212 | Exp: any, 213 | Func: func(s string) string { 214 | if s == "" { 215 | return "" 216 | } 217 | return e.prevLineStyle.Apply(labels().Anonym() + s) 218 | }, 219 | }, 220 | ) 221 | } 222 | 223 | edited = editor.Replaces(edited, 224 | editor.RegexRepl{Exp: emptyline}, 225 | ) 226 | 227 | if style != nil { 228 | e.prevLineStyle = style 229 | } 230 | 231 | return edited, nil 232 | } 233 | -------------------------------------------------------------------------------- /editor/tty.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mattn/go-isatty" 7 | ) 8 | 9 | const forceColorFlag = "RICHGO_FORCE_COLOR" 10 | 11 | // Formattable judge whether a descriptor (like a os.Stdout, os.Stderr or *os.File...) 12 | // is capable of colorization or not. The default behavior is to detect whether 13 | // it is connected to a TTY, but may be overridden by setting the environment 14 | // variable `RICHGO_FORCE_COLOR` to a non-empty value. 15 | func Formattable(descriptor interface { 16 | Fd() uintptr 17 | }) bool { 18 | return os.Getenv(forceColorFlag) != "" || isatty.IsTerminal(descriptor.Fd()) 19 | } 20 | -------------------------------------------------------------------------------- /editor/tty_test.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFormattable(t *testing.T) { 11 | if assert.NoError(t, os.Setenv(forceColorFlag, "1")) { 12 | assert.True(t, Formattable(nil)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kyoh86/richgo 2 | 3 | require ( 4 | github.com/kyoh86/xdg v1.2.0 5 | github.com/mattn/go-isatty v0.0.17 6 | github.com/morikuni/aec v1.0.0 7 | github.com/rakyll/statik v0.1.7 8 | github.com/stretchr/testify v1.8.2 9 | github.com/wacul/ptr v1.0.0 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/sys v0.5.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | 20 | go 1.17 21 | -------------------------------------------------------------------------------- /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/kyoh86/xdg v1.2.0 h1:CERuT/ShdTDj+A2UaX3hQ3mOV369+Sj+wyn2nIRIIkI= 5 | github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= 6 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 7 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 8 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 9 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 13 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 16 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 17 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 19 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 20 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 21 | github.com/wacul/ptr v1.0.0 h1:FIKu08Wx0YUIf9MNsfF62OCmBSmz5A1Tk65zWhOIL/I= 22 | github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= 23 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 25 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 29 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 30 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | 9 | "github.com/kyoh86/richgo/config" 10 | "github.com/kyoh86/richgo/editor" 11 | "github.com/kyoh86/richgo/editor/test" 12 | ) 13 | 14 | const testFilterCmd = "testfilter" 15 | const testCmd = "test" 16 | 17 | type factoryFunc func() editor.Editor 18 | 19 | var lps = map[string]factoryFunc{ 20 | "test": test.New, 21 | } 22 | 23 | func main() { 24 | config.Load() 25 | 26 | var cmd *exec.Cmd 27 | var factory factoryFunc = editor.Parrot 28 | var colorize bool 29 | 30 | // without arguments 31 | switch len(os.Args) { 32 | case 0: 33 | panic("no arguments") 34 | case 1: 35 | cmd = exec.Command("go") 36 | default: 37 | // This is a bit of a special case. Somebody is already 38 | // running `go test` for us, and just wants us to prettify the 39 | // output. 40 | switch os.Args[1] { 41 | case testFilterCmd: 42 | colorize = true 43 | cmd = exec.Command("cat", "-") 44 | factory = test.New 45 | case testCmd: 46 | colorize = true 47 | fallthrough 48 | default: 49 | cmd = exec.Command("go", os.Args[1:]...) 50 | // select a wrapper with subcommand 51 | if f, ok := lps[os.Args[1]]; ok { 52 | factory = f 53 | } 54 | } 55 | } 56 | 57 | stderr := io.WriteCloser(os.Stderr) 58 | stdout := io.WriteCloser(os.Stdout) 59 | if colorize { 60 | stderr = formatWriteCloser(os.Stderr, factory) 61 | defer stderr.Close() 62 | 63 | stdout = formatWriteCloser(os.Stdout, factory) 64 | defer stdout.Close() 65 | } 66 | cmd.Stderr = stderr 67 | cmd.Stdout = stdout 68 | cmd.Stdin = os.Stdin 69 | 70 | switch err := cmd.Run().(type) { 71 | case nil: 72 | // noop 73 | default: 74 | panic(err) 75 | case *exec.ExitError: 76 | if waitStatus, ok := err.Sys().(syscall.WaitStatus); ok { 77 | defer os.Exit(waitStatus.ExitStatus()) 78 | } else { 79 | panic(err) 80 | } 81 | } 82 | } 83 | 84 | func formatWriteCloser(wc io.WriteCloser, factory factoryFunc) io.WriteCloser { 85 | if editor.Formattable(os.Stderr) { 86 | return editor.Stream(wc, factory()) 87 | } 88 | return editor.Stream(wc, editor.Parrot()) 89 | } 90 | -------------------------------------------------------------------------------- /richgo.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "width": 180, 4 | "height": 46, 5 | "duration": 19.742369, 6 | "command": null, 7 | "title": "richgo-v0.2", 8 | "env": { 9 | "TERM": "xterm-256color", 10 | "SHELL": "/bin/zsh" 11 | }, 12 | "stdout": [ 13 | [ 14 | 1.0, 15 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 16 | ], 17 | [ 18 | 0.05, 19 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[34m$\u001b[39m \u001b[K\u001b[147C\u001b[39m\u001b[177D" 20 | ], 21 | [ 22 | 0.05, 23 | "\u001b[?2004h" 24 | ], 25 | [ 26 | 0.05, 27 | "\u001b[32m.\u001b[39m" 28 | ], 29 | [ 30 | 0.05, 31 | "\b\u001b[39m\u001b[4m.\u001b[4m/\u001b[24m" 32 | ], 33 | [ 34 | 0.05, 35 | "\b\b\u001b[4m.\u001b[4m/\u001b[4ms\u001b[24m" 36 | ], 37 | [ 38 | 0.05, 39 | "\b\u001b[4ms\u001b[4ma\u001b[24m" 40 | ], 41 | [ 42 | 0.05, 43 | "\b\u001b[4ma\u001b[4mm\u001b[24m" 44 | ], 45 | [ 46 | 0.05, 47 | "\b\u001b[4mm\u001b[4mp\u001b[24m" 48 | ], 49 | [ 50 | 0.05, 51 | "\b\u001b[4mp\u001b[4ml\u001b[24m" 52 | ], 53 | [ 54 | 0.05, 55 | "\b\u001b[4ml\u001b[4me\u001b[24m" 56 | ], 57 | [ 58 | 0.05, 59 | "\b\u001b[4me\u001b[4m/\u001b[24m" 60 | ], 61 | [ 62 | 0.05, 63 | "\b\u001b[4m/\u001b[4mr\u001b[24m" 64 | ], 65 | [ 66 | 0.05, 67 | "\b\u001b[4mr\u001b[4mu\u001b[24m" 68 | ], 69 | [ 70 | 0.05, 71 | "\b\u001b[4mu\u001b[4mn\u001b[24m" 72 | ], 73 | [ 74 | 0.05, 75 | "\b\u001b[4mn\u001b[4m.\u001b[24m" 76 | ], 77 | [ 78 | 0.05, 79 | "\b\u001b[4m.\u001b[4ms\u001b[24m" 80 | ], 81 | [ 82 | 0.1, 83 | "\u001b[14D\u001b[24m\u001b[32m.\u001b[24m\u001b[32m/\u001b[24m\u001b[32ms\u001b[24m\u001b[32ma\u001b[24m\u001b[32mm\u001b[24m\u001b[32mp\u001b[24m\u001b[32ml\u001b[24m\u001b[32me\u001b[24m\u001b[32m/\u001b[24m\u001b[32mr\u001b[24m\u001b[32mu\u001b[24m\u001b[32mn\u001b[24m\u001b[32m.\u001b[24m\u001b[32ms\u001b[32mh\u001b[39m" 84 | ], 85 | [ 86 | 0.5, 87 | "\u001b[?2004l\r\r\n" 88 | ], 89 | [ 90 | 0.1, 91 | "===================================== go test =====================================\r\n" 92 | ], 93 | [ 94 | 0.2, 95 | "# github.com/kyoh86/richgo/sample/buildfail\r\nsample/buildfail/buildfail_test.go:6: t.Foo undefined (type *testing.T has no field or method Foo)\r\n" 96 | ], 97 | [ 98 | 1.0, 99 | "=== RUN TestSampleNG\r\n=== RUN TestSampleNG/SubtestNG\r\n--- FAIL: TestSampleNG (0.00s)\r\n\tsample_ng_test.go:9: It's not OK... :(\r\n --- FAIL: TestSampleNG/SubtestNG (0.00s)\r\n \tsample_ng_test.go:13: It's also not OK... :(\r\n=== RUN TestSampleOK\r\n=== RUN TestSampleOK/SubtestOK\r\ntime:2017-01-01T01:01:01+09:00\r\n--- PASS: TestSampleOK (0.00s)\r\n\tsample_ok_test.go:11: It's OK!\r\n --- PASS: TestSampleOK/SubtestOK (0.00s)\r\n \tsample_ok_test.go:15: It's also OK!\r\n=== RUN TestSampleSkip\r\n--- SKIP: TestSampleSkip (0.00s)\r\n\tsample_skip_test.go:6: \r\n=== RUN TestSampleSkipSub\r\n=== RUN TestSampleSkipSub/SubtestSkip\r\n--- PASS: TestSampleSkipSub (0.00s)\r\n --- SKIP: TestSampleSkipSub/SubtestSkip (0.00s)\r\n \tsample_skip_test.go:11: \r\n=== RUN TestSampleTimeout\r\n=== RUN TestSampleTimeout/SubtestTimeout\r\n--- PASS: TestSampleTimeout (3.00s)\r\n --- PASS: TestSampleTimeout/SubtestTimeout (3.00s)\r\nFAIL\r\ncoverage: 0.0% of statements\r\nFAIL\tgithub.com/kyoh86/richgo/sample\t3.013s\r\nFAIL\tgithub.com/kyoh86/richgo" 100 | ], 101 | [ 102 | 0.01, 103 | "/sample/buildfail [build failed]\r\n=== RUN TestCover05\r\n--- PASS: TestCover05 (0.00s)\r\nPASS\r\ncoverage: 50.0% of statements\r\nok \tgithub.com/kyoh86/richgo/sample/cover05\t0.009s\tcoverage: 50.0% of statements\r\n=== RUN TestCoverAll\r\n--- PASS: TestCoverAll (0.00s)\r\nPASS\r\ncoverage: 100.0% of statements\r\nok \tgithub.com/kyoh86/richgo/sample/coverall\t0.007s\tcoverage: 100.0% of statements\r\n=== RUN TestNocover\r\n--- PASS: TestNocover (0.00s)\r\n\tnocover_test.go:6: accept\r\nPASS\r\ncoverage: 0.0% of statements\r\nok \tgithub.com/kyoh86/richgo/sample/nocover\t0.008s\tcoverage: 0.0% of statements\r\n? \tgithub.com/kyoh86/richgo/sample/notest\t[no test files]\r\n" 104 | ], 105 | [ 106 | 0.01, 107 | "\r\n" 108 | ], 109 | [ 110 | 0.01, 111 | "===================================== richgo test =====================================\r\n" 112 | ], 113 | [ 114 | 1.0, 115 | "\u001b[49m\u001b[33m\u001b[1mBUILD| github.com/kyoh86/richgo/sample/buildfail\u001b[0m\r\n\u001b[49m\u001b[33m\u001b[1m | \u001b[49m\u001b[36msample/buildfail/buildfail_test.go\u001b[0m:\u001b[49m\u001b[35m6\u001b[0m: t.Foo undefined (type *testing.T has no field or method Foo)\u001b[0m\r\n" 116 | ], 117 | [ 118 | 1.5, 119 | "\u001b[49m\u001b[90mSTART| SampleNG\u001b[0m\r\n" 120 | ], 121 | [ 122 | 0.01, 123 | "\u001b[49m\u001b[90mSTART| SampleNG/SubtestNG\u001b[0m\r\n\u001b[49m\u001b[31m\u001b[1mFAIL | SampleNG (0.00s)\u001b[0m\r\n" 124 | ], 125 | [ 126 | 0.01, 127 | "\u001b[49m\u001b[31m\u001b[1m | \u001b[49m\u001b[36msample_ng_test.go\u001b[0m:\u001b[49m\u001b[35m9\u001b[0m: It's not OK... :(\u001b[0m\r\n" 128 | ], 129 | [ 130 | 0.01, 131 | "\u001b[49m\u001b[31m\u001b[1mFAIL | SampleNG/SubtestNG (0.00s)\u001b[0m\r\n" 132 | ], 133 | [ 134 | 0.01, 135 | "\u001b[49m\u001b[31m\u001b[1m | \u001b[49m\u001b[36msample_ng_test.go\u001b[0m:\u001b[49m\u001b[35m13\u001b[0m: It's also not OK... :(\u001b[0m\r\n" 136 | ], 137 | [ 138 | 0.01, 139 | "\u001b[49m\u001b[90mSTART| SampleOK\u001b[0m\r\n" 140 | ], 141 | [ 142 | 0.01, 143 | "\u001b[49m\u001b[90mSTART| SampleOK/SubtestOK\u001b[0m\r\n" 144 | ], 145 | [ 146 | 0.01, 147 | "\u001b[49m\u001b[32mPASS | SampleOK (0.00s)\u001b[0m\r\n" 148 | ], 149 | [ 150 | 0.01, 151 | "\u001b[49m\u001b[32m | \u001b[49m\u001b[36msample_ok_test.go\u001b[0m:\u001b[49m\u001b[35m11\u001b[0m: It's OK!\u001b[0m\r\n" 152 | ], 153 | [ 154 | 0.01, 155 | "\u001b[49m\u001b[32mPASS | SampleOK/SubtestOK (0.00s)\u001b[0m\r\n" 156 | ], 157 | [ 158 | 0.01, 159 | "\u001b[49m\u001b[32m | \u001b[49m\u001b[36msample_ok_test.go\u001b[0m:\u001b[49m\u001b[35m15\u001b[0m: It's also OK!\u001b[0m\r\n" 160 | ], 161 | [ 162 | 0.01, 163 | "\u001b[49m\u001b[90mSTART| SampleSkip\u001b[0m\r\n" 164 | ], 165 | [ 166 | 0.01, 167 | "\u001b[49m\u001b[90mSKIP | SampleSkip (0.00s)\u001b[0m\r\n" 168 | ], 169 | [ 170 | 0.01, 171 | "\u001b[49m\u001b[90m | \u001b[49m\u001b[36msample_skip_test.go\u001b[0m:\u001b[49m\u001b[35m6\u001b[0m: \u001b[0m\r\n" 172 | ], 173 | [ 174 | 0.01, 175 | "\u001b[49m\u001b[90mSTART| SampleSkipSub\u001b[0m\r\n" 176 | ], 177 | [ 178 | 0.01, 179 | "\u001b[49m\u001b[90mSTART| SampleSkipSub/SubtestSkip\u001b[0m\r\n" 180 | ], 181 | [ 182 | 0.01, 183 | "\u001b[49m\u001b[32mPASS | SampleSkipSub (0.00s)\u001b[0m\r\n\u001b[49m\u001b[90mSKIP | SampleSkipSub/SubtestSkip (0.00s)\u001b[0m\r\n" 184 | ], 185 | [ 186 | 0.01, 187 | "\u001b[49m\u001b[90m | \u001b[49m\u001b[36msample_skip_test.go\u001b[0m:\u001b[49m\u001b[35m11\u001b[0m: \u001b[0m\r\n" 188 | ], 189 | [ 190 | 0.01, 191 | "\u001b[49m\u001b[90mSTART| SampleTimeout\u001b[0m\r\n" 192 | ], 193 | [ 194 | 0.01, 195 | "\u001b[49m\u001b[90mSTART| SampleTimeout/SubtestTimeout\u001b[0m\r\n" 196 | ], 197 | [ 198 | 0.01, 199 | "\u001b[49m\u001b[32mPASS | SampleTimeout (3.00s)\u001b[0m\r\n" 200 | ], 201 | [ 202 | 0.01, 203 | "\u001b[49m\u001b[32mPASS | SampleTimeout/SubtestTimeout (3.00s)\u001b[0m\r\n" 204 | ], 205 | [ 206 | 0.01, 207 | "\u001b[49m\u001b[33m\u001b[1mCOVER| 0.0% [__________]\u001b[0m\r\n" 208 | ], 209 | [ 210 | 0.01, 211 | "\u001b[49m\u001b[31m\u001b[1mFAIL | github.com/kyoh86/richgo/sample 3.012s\u001b[0m\r\n" 212 | ], 213 | [ 214 | 0.01, 215 | "\u001b[49m\u001b[31m\u001b[1mFAIL | github.com/kyoh86/richgo/sample/buildfail [build failed]\u001b[0m\r\n" 216 | ], 217 | [ 218 | 0.01, 219 | "\u001b[49m\u001b[90mSTART| Cover05\u001b[0m\r\n" 220 | ], 221 | [ 222 | 0.01, 223 | "\u001b[49m\u001b[32mPASS | Cover05 (0.00s)\u001b[0m\r\n" 224 | ], 225 | [ 226 | 0.01, 227 | "\u001b[49m\u001b[32mCOVER| 50.0% [#####_____]\u001b[0m\r\n" 228 | ], 229 | [ 230 | 0.01, 231 | "\u001b[49m\u001b[32mPASS | github.com/kyoh86/richgo/sample/cover05 0.010s\u001b[0m\r\n\u001b[49m\u001b[32mCOVER| 50.0% [#####_____]\u001b[0m\r\n" 232 | ], 233 | [ 234 | 0.01, 235 | "\u001b[49m\u001b[90mSTART| CoverAll\u001b[0m\r\n" 236 | ], 237 | [ 238 | 0.01, 239 | "\u001b[49m\u001b[32mPASS | CoverAll (0.00s)\u001b[0m\r\n" 240 | ], 241 | [ 242 | 0.01, 243 | "\u001b[49m\u001b[32mCOVER| 100.0% [##########]\u001b[0m\r\n" 244 | ], 245 | [ 246 | 0.01, 247 | "\u001b[49m\u001b[32mPASS | github.com/kyoh86/richgo/sample/coverall 0.009s\u001b[0m\r\n\u001b[49m\u001b[32mCOVER| 100.0% [##########]\u001b[0m\r\n" 248 | ], 249 | [ 250 | 0.01, 251 | "\u001b[49m\u001b[90mSTART| Nocover\u001b[0m\r\n\u001b[49m\u001b[32mPASS | Nocover (0.00s)\u001b[0m\r\n" 252 | ], 253 | [ 254 | 0.01, 255 | "\u001b[49m\u001b[32m | \u001b[49m\u001b[36mnocover_test.go\u001b[0m:\u001b[49m\u001b[35m6\u001b[0m: accept\u001b[0m\r\n" 256 | ], 257 | [ 258 | 0.01, 259 | "\u001b[49m\u001b[33m\u001b[1mCOVER| 0.0% [__________]\u001b[0m\r\n" 260 | ], 261 | [ 262 | 0.01, 263 | "\u001b[49m\u001b[32mPASS | github.com/kyoh86/richgo/sample/nocover 0.007s\u001b[0m\r\n\u001b[49m\u001b[33m\u001b[1mCOVER| 0.0% [__________]\u001b[0m\r\n" 264 | ], 265 | [ 266 | 0.01, 267 | "\u001b[49m\u001b[90mSKIP | github.com/kyoh86/richgo/sample/notest [no test files]\u001b[0m\r\n" 268 | ], 269 | [ 270 | 0.01, 271 | "exit status 2\r\n" 272 | ], 273 | [ 274 | 0.1, 275 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 276 | ], 277 | [ 278 | 0.1, 279 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[34m$\u001b[39m \u001b[K\u001b[147C\u001b[39m\u001b[177D\u001b[?2004h" 280 | ], 281 | [ 282 | 3.687453, 283 | "\u001b[?2004l\r\r\n" 284 | ] 285 | ] 286 | } 287 | -------------------------------------------------------------------------------- /sample/buildfail/buildfail_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package buildfail 4 | 5 | import "testing" 6 | 7 | func TestSampleBuildFail(t *testing.T) { 8 | _ = t.Foo 9 | } 10 | -------------------------------------------------------------------------------- /sample/cover05/cover05.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package cover05 4 | 5 | // Cover05 will be covered about 50% 6 | func Cover05(arg int) string { 7 | switch arg { 8 | case 1: 9 | return "case-1" 10 | case 2: 11 | return "case-2" 12 | default: 13 | return "others" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/cover05/cover05_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package cover05 4 | 5 | import "testing" 6 | 7 | func TestCover05(t *testing.T) { 8 | case0 := Cover05(0) 9 | if case0 != "others" { 10 | t.Error("0 is not left out") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/coverall/coverall.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package coverall 4 | 5 | // CoverAll will not be covered 6 | func CoverAll() string { 7 | return "CoverAll" 8 | } 9 | -------------------------------------------------------------------------------- /sample/coverall/coverall_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package coverall 4 | 5 | import "testing" 6 | 7 | func TestCoverAll(t *testing.T) { 8 | if CoverAll() != "CoverAll" { 9 | t.Error("not covered all") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample/emptytest/dummy.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package emptytest 4 | 5 | // Dummy is dummy. 6 | func Dummy() int { 7 | var j int 8 | for k := range make([]struct{}, 3) { 9 | j += k 10 | j *= 2 11 | } 12 | return j 13 | } 14 | -------------------------------------------------------------------------------- /sample/emptytest/dummy_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package emptytest 4 | -------------------------------------------------------------------------------- /sample/nocover/nocover.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package nocover 4 | 5 | // Nocover will not be covered 6 | func Nocover() string { 7 | return "Nocover" 8 | } 9 | -------------------------------------------------------------------------------- /sample/nocover/nocover_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package nocover 4 | 5 | import "testing" 6 | 7 | func TestNocover(t *testing.T) { 8 | t.Log("accept") 9 | } 10 | -------------------------------------------------------------------------------- /sample/notest/notest.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package notest 4 | -------------------------------------------------------------------------------- /sample/out_colored.txt: -------------------------------------------------------------------------------- 1 | BUILD| github.com/kyoh86/richgo/sample/buildfail [github.com/kyoh86/richgo/sample/buildfail.test] 2 |  | sample/buildfail/buildfail_test.go:8:7: t.Foo undefined (type *testing.T has no field or method Foo) 3 | START| SampleNG 4 |  | sample_ng_test.go:9: It's not OK... :( 5 | START| SampleNG/SubtestNG 6 |  | sample_ng_test.go:13: It's also not OK... :( 7 | FAIL | SampleNG (0.00s) 8 | FAIL | SampleNG/SubtestNG (0.00s) 9 | START| SampleOK 10 |  | sample_ok_test.go:11: It's OK! 11 | START| SampleOK/SubtestOK 12 |  | time:2017-01-01T01:01:01+09:00 13 |  | sample_ok_test.go:15: It's also OK! 14 | PASS | SampleOK (0.00s) 15 | PASS | SampleOK/SubtestOK (0.00s) 16 | START| SampleSkip 17 |  | sample_skip_test.go:8:  18 | SKIP | SampleSkip (0.00s) 19 | START| SampleSkipSub 20 | START| SampleSkipSub/SubtestSkip 21 |  | sample_skip_test.go:13:  22 | PASS | SampleSkipSub (0.00s) 23 | SKIP | SampleSkipSub/SubtestSkip (0.00s) 24 | START| SampleTimeout 25 | START| SampleTimeout/SubtestTimeout 26 | PASS | SampleTimeout (3.00s) 27 | PASS | SampleTimeout/SubtestTimeout (3.00s) 28 |  | coverage: [no statements] 29 | FAIL | github.com/kyoh86/richgo/sample 3.003s 30 | FAIL | github.com/kyoh86/richgo/sample/buildfail [build failed] 31 | START| Cover05 32 | PASS | Cover05 (0.00s) 33 | COVER| 50.0% [#####_____] 34 | PASS | github.com/kyoh86/richgo/sample/cover05 coverage: 50.0% of statements 35 | START| CoverAll 36 | PASS | CoverAll (0.00s) 37 | COVER| 100.0% [##########] 38 | PASS | github.com/kyoh86/richgo/sample/coverall coverage: 100.0% of statements 39 |  | testing: warning: no tests to run 40 | COVER| 0.0% [__________] 41 | PASS | github.com/kyoh86/richgo/sample/emptytest coverage: 0.0% of statements 42 | START| Nocover 43 |  | nocover_test.go:8: accept 44 | PASS | Nocover (0.00s) 45 | COVER| 0.0% [__________] 46 | PASS | github.com/kyoh86/richgo/sample/nocover coverage: 0.0% of statements 47 | SKIP | github.com/kyoh86/richgo/sample/notest [no test files] 48 | exit status 2 49 | -------------------------------------------------------------------------------- /sample/out_raw.txt: -------------------------------------------------------------------------------- 1 | # github.com/kyoh86/richgo/sample/buildfail [github.com/kyoh86/richgo/sample/buildfail.test] 2 | sample/buildfail/buildfail_test.go:8:7: t.Foo undefined (type *testing.T has no field or method Foo) 3 | === RUN TestSampleNG 4 | sample_ng_test.go:9: It's not OK... :( 5 | === RUN TestSampleNG/SubtestNG 6 | sample_ng_test.go:13: It's also not OK... :( 7 | --- FAIL: TestSampleNG (0.00s) 8 | --- FAIL: TestSampleNG/SubtestNG (0.00s) 9 | === RUN TestSampleOK 10 | sample_ok_test.go:11: It's OK! 11 | === RUN TestSampleOK/SubtestOK 12 | time:2017-01-01T01:01:01+09:00 13 | sample_ok_test.go:15: It's also OK! 14 | --- PASS: TestSampleOK (0.00s) 15 | --- PASS: TestSampleOK/SubtestOK (0.00s) 16 | === RUN TestSampleSkip 17 | sample_skip_test.go:8: 18 | --- SKIP: TestSampleSkip (0.00s) 19 | === RUN TestSampleSkipSub 20 | === RUN TestSampleSkipSub/SubtestSkip 21 | sample_skip_test.go:13: 22 | --- PASS: TestSampleSkipSub (0.00s) 23 | --- SKIP: TestSampleSkipSub/SubtestSkip (0.00s) 24 | === RUN TestSampleTimeout 25 | === RUN TestSampleTimeout/SubtestTimeout 26 | --- PASS: TestSampleTimeout (3.00s) 27 | --- PASS: TestSampleTimeout/SubtestTimeout (3.00s) 28 | FAIL 29 | coverage: [no statements] 30 | FAIL github.com/kyoh86/richgo/sample 3.003s 31 | FAIL github.com/kyoh86/richgo/sample/buildfail [build failed] 32 | === RUN TestCover05 33 | --- PASS: TestCover05 (0.00s) 34 | PASS 35 | coverage: 50.0% of statements 36 | ok github.com/kyoh86/richgo/sample/cover05 (cached) coverage: 50.0% of statements 37 | === RUN TestCoverAll 38 | --- PASS: TestCoverAll (0.00s) 39 | PASS 40 | coverage: 100.0% of statements 41 | ok github.com/kyoh86/richgo/sample/coverall (cached) coverage: 100.0% of statements 42 | testing: warning: no tests to run 43 | PASS 44 | coverage: 0.0% of statements 45 | ok github.com/kyoh86/richgo/sample/emptytest (cached) coverage: 0.0% of statements [no tests to run] 46 | === RUN TestNocover 47 | nocover_test.go:8: accept 48 | --- PASS: TestNocover (0.00s) 49 | PASS 50 | coverage: 0.0% of statements 51 | ok github.com/kyoh86/richgo/sample/nocover (cached) coverage: 0.0% of statements 52 | ? github.com/kyoh86/richgo/sample/notest [no test files] 53 | FAIL 54 | -------------------------------------------------------------------------------- /sample/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bin="$(cd -- "$(dirname -- "${BASH_SOURCE:-${(%):-%N}}")"; pwd)" 4 | cd "${bin}/.." 5 | 6 | export RICHGO_LOCAL=1 7 | 8 | OPTIONS=(test -tags=sample ./sample/... -cover) 9 | 10 | echo "===================================== go test =====================================" 11 | \go "${OPTIONS[@]}" 12 | 13 | echo "" 14 | echo "===================================== richgo test =====================================" 15 | go run . "${OPTIONS[@]}" 16 | 17 | echo "==================================== go test -v ====================================" 18 | \go "${OPTIONS[@]}" -v 19 | 20 | echo "" 21 | echo "==================================== richgo test -v ===================================" 22 | go run . "${OPTIONS[@]}" -v 23 | -------------------------------------------------------------------------------- /sample/sample_ng_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package sample 4 | 5 | import "testing" 6 | 7 | func TestSampleNG(t *testing.T) { 8 | t.Fail() 9 | t.Log("It's not OK... :(") 10 | 11 | t.Run("SubtestNG", func(t *testing.T) { 12 | t.Fail() 13 | t.Log("It's also not OK... :(") 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /sample/sample_ok_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package sample 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestSampleOK(t *testing.T) { 11 | t.Log("It's OK!") 12 | 13 | t.Run("SubtestOK", func(t *testing.T) { 14 | fmt.Println("time:2017-01-01T01:01:01+09:00") 15 | t.Log("It's also OK!") 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /sample/sample_skip_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package sample 4 | 5 | import "testing" 6 | 7 | func TestSampleSkip(t *testing.T) { 8 | t.Skip() 9 | } 10 | 11 | func TestSampleSkipSub(t *testing.T) { 12 | t.Run("SubtestSkip", func(t *testing.T) { 13 | t.Skip() 14 | t.Log("It's maybe skipped... :(") 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /sample/sample_timeout_test.go: -------------------------------------------------------------------------------- 1 | // +build sample 2 | 3 | package sample 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestSampleTimeout(t *testing.T) { 11 | t.Run("SubtestTimeout", func(t *testing.T) { 12 | // Trying a command `\go test ./sample/... -test.timeout 1s`, fail with timeout 13 | time.Sleep(3 * time.Second) 14 | }) 15 | } 16 | --------------------------------------------------------------------------------