From b98a9d64ae179532c9c8ed946b30941bd8b8bb7a Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 9 Jun 2023 21:16:42 +0200 Subject: [PATCH] Refactor (#5) --- .github/workflows/test-unit.yml | 136 -------------------------------- cmd/catp.pgo | Bin 14595 -> 12296 bytes cmd/catp/main.go | 89 +++++++++++++-------- cmd/go.mod | 9 --- cmd/go.sum | 7 -- go.mod | 6 +- go.sum | 8 +- go.work | 3 - progress.go | 54 +++++++------ 9 files changed, 95 insertions(+), 217 deletions(-) delete mode 100644 .github/workflows/test-unit.yml delete mode 100644 cmd/go.mod delete mode 100644 cmd/go.sum delete mode 100644 go.work diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml deleted file mode 100644 index 6fa0436..0000000 --- a/.github/workflows/test-unit.yml +++ /dev/null @@ -1,136 +0,0 @@ -# This script is provided by github.com/bool64/dev. -name: test-unit -on: - push: - branches: - - master - - main - pull_request: - -# Cancel the workflow in progress in newer build is about to start. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - GO111MODULE: "on" - RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing. - COV_GO_VERSION: 1.20.x # Version of Go to collect coverage - TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents -jobs: - test: - strategy: - matrix: - go-version: [ 1.20.x ] - runs-on: ubuntu-latest - steps: - - name: Install Go stable - if: matrix.go-version != 'tip' - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - - name: Install Go tip - if: matrix.go-version == 'tip' - run: | - curl -sL https://storage.googleapis.com/go-build-snap/go/linux-amd64/$(git ls-remote https://github.com/golang/go.git HEAD | awk '{print $1;}').tar.gz -o gotip.tar.gz - ls -lah gotip.tar.gz - mkdir -p ~/sdk/gotip - tar -C ~/sdk/gotip -xzf gotip.tar.gz - ~/sdk/gotip/bin/go version - echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV - - - name: Checkout code - uses: actions/checkout@v3 - - - name: Go cache - uses: actions/cache@v3 - with: - # In order: - # * Module download cache - # * Build cache (Linux) - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-go-cache-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-cache - - - name: Restore base test coverage - id: base-coverage - if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' - uses: actions/cache@v2 - with: - path: | - unit-base.txt - # Use base sha for PR or new commit hash for master/main push in test result key. - key: ${{ runner.os }}-unit-test-coverage-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }} - - - name: Run test for base code - if: matrix.go-version == env.COV_GO_VERSION && env.RUN_BASE_COVERAGE == 'on' && steps.base-coverage.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != '' - run: | - git fetch origin master ${{ github.event.pull_request.base.sha }} - HEAD=$(git rev-parse HEAD) - git reset --hard ${{ github.event.pull_request.base.sha }} - (make test-unit && go tool cover -func=./unit.coverprofile > unit-base.txt) || echo "No test-unit in base" - git reset --hard $HEAD - - - name: Test - id: test - run: | - make test-unit - go tool cover -func=./unit.coverprofile > unit.txt - TOTAL=$(grep 'total:' unit.txt) - echo "${TOTAL}" - echo "total=$TOTAL" >> $GITHUB_OUTPUT - - - name: Annotate missing test coverage - id: annotate - if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' - run: | - curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz - gocovdiff_hash=$(git hash-object ./gocovdiff) - [ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) - git fetch origin master ${{ github.event.pull_request.base.sha }} - REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) - echo "${REP}" - cat gha-unit.txt - DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") - TOTAL=$(cat delta-cov-unit.txt) - echo "rep<> $GITHUB_OUTPUT && echo "$REP" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT - echo "diff<> $GITHUB_OUTPUT && echo "$DIFF" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT - echo "total<> $GITHUB_OUTPUT && echo "$TOTAL" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT - - - name: Comment test coverage - continue-on-error: true - if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' - uses: marocchino/sticky-pull-request-comment@v2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - header: unit-test - message: | - ### Unit Test Coverage - ${{ steps.test.outputs.total }} - ${{ steps.annotate.outputs.total }} -
Coverage of changed lines - - ${{ steps.annotate.outputs.rep }} - -
- -
Coverage diff with base branch - - ${{ steps.annotate.outputs.diff }} - -
- - - name: Store base coverage - if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} - run: cp unit.txt unit-base.txt - - - name: Upload code coverage - if: matrix.go-version == env.COV_GO_VERSION - uses: codecov/codecov-action@v1 - with: - file: ./unit.coverprofile - flags: unittests diff --git a/cmd/catp.pgo b/cmd/catp.pgo index b4d4781de748a60336eb46866e93988ccccd0ab4..0d6ae225df76b4dfa111dd925b4825fa72480a90 100644 GIT binary patch literal 12296 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 z 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 literal 14595 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 1 { fileDonePercent := 100 * float64(r.currentFile.Bytes()) / float64(r.currentTotal) res = fmt.Sprintf("all: %.1f%% bytes read, %s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s", s.DonePercent, s.Task, fileDonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS, s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String()) - } else { res = fmt.Sprintf("%s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s", s.Task, s.DonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS, @@ -70,7 +72,12 @@ func (r *runner) scanFile(rd io.Reader) { for s.Scan() { for _, g := range r.grep { if bytes.Contains(s.Bytes(), g) { - _, _ = os.Stdout.Write(append(s.Bytes(), '\n')) + if _, err := os.Stdout.Write(append(s.Bytes(), '\n')); err != nil { + r.lastErr = err + + return + } + atomic.AddInt64(&r.matches, 1) break @@ -79,16 +86,21 @@ func (r *runner) scanFile(rd io.Reader) { } if err := s.Err(); err != nil { - log.Fatal(err) + r.lastErr = err } } -func (r *runner) cat(filename string) { - file, err := os.Open(filename) +func (r *runner) cat(filename string) (err error) { + file, err := os.Open(filename) //nolint:gosec if err != nil { - log.Fatal(err) + return err } - defer file.Close() + + defer func() { + if clErr := file.Close(); clErr != nil && err == nil { + err = clErr + } + }() r.currentFile = &progress.CountingReader{Reader: file} r.currentTotal = r.sizes[filename] @@ -97,18 +109,14 @@ func (r *runner) cat(filename string) { switch { case strings.HasSuffix(filename, ".gz"): if rd, err = gzip.NewReader(rd); err != nil { - log.Fatalf("failed to init gzip reader: %s", err) + return fmt.Errorf("failed to init gzip reader: %w", err) } case strings.HasSuffix(filename, ".zst"): if rd, err = zstd.NewReader(rd); err != nil { - log.Fatalf("failed to init gzip reader: %s", err) + return fmt.Errorf("failed to init gzip reader: %w", err) } } - if r.reverse { - - } - r.pr.Start(func(t *progress.Task) { t.TotalBytes = func() int64 { return r.totalBytes @@ -121,7 +129,7 @@ func (r *runner) cat(filename string) { t.Continue = true }) - if len(r.grep) > 0 || r.reverse { + if len(r.grep) > 0 { r.scanFile(rd) } else { r.readFile(rd) @@ -129,29 +137,42 @@ func (r *runner) cat(filename string) { r.pr.Stop() r.readBytes += r.currentFile.Bytes() + + return r.lastErr +} + +func startProfiling(cpuProfile string) { + f, err := os.Create(cpuProfile) //nolint:gosec + if err != nil { + log.Fatal(err) + } + + if err = pprof.StartCPUProfile(f); err != nil { + log.Fatal(err) + } + + go func() { + time.Sleep(10 * time.Second) + pprof.StopCPUProfile() + println("CPU profile written to", cpuProfile) + }() } func main() { grep := flag.String("grep", "", "grep pattern, may contain multiple patterns separated by \\|") cpuProfile := flag.String("dbg-cpu-prof", "", "write first 10 seconds of CPU profile to file") + ver := flag.Bool("version", false, "print version and exit") flag.Parse() - if *cpuProfile != "" { - f, err := os.Create(*cpuProfile) //nolint:gosec - if err != nil { - log.Fatal(err) - } + if *ver { + fmt.Println(version.Module("github.com/bool64/progress").Version) - if err = pprof.StartCPUProfile(f); err != nil { - log.Fatal(err) - } + return + } - go func() { - time.Sleep(10 * time.Second) - pprof.StopCPUProfile() - println("CPU profile written to", *cpuProfile) - }() + if *cpuProfile != "" { + startProfiling(*cpuProfile) } r := &runner{} @@ -165,7 +186,7 @@ func main() { r.sizes = make(map[string]int64) r.pr = &progress.Progress{ Interval: 5 * time.Second, - Print: func(status progress.ProgressStatus) { + Print: func(status progress.Status) { println(r.st(status)) }, } @@ -183,6 +204,8 @@ func main() { } for i := 0; i < flag.NArg(); i++ { - r.cat(flag.Arg(i)) + if err := r.cat(flag.Arg(i)); err != nil { + log.Fatal(err) + } } } diff --git a/cmd/go.mod b/cmd/go.mod deleted file mode 100644 index 192ac1a..0000000 --- a/cmd/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/bool64/progress/cmd - -go 1.20 - -require ( - github.com/bool64/progress v0.1.0 - github.com/klauspost/compress v1.16.5 - github.com/klauspost/pgzip v1.2.5 -) diff --git a/cmd/go.sum b/cmd/go.sum deleted file mode 100644 index d4b1c79..0000000 --- a/cmd/go.sum +++ /dev/null @@ -1,7 +0,0 @@ -github.com/bool64/dev v0.2.27 h1:mFT+B74mFVgUeUmm/EbfM6ELPA55lEXBjQ/AOHCwCOc= -github.com/bool64/progress v0.1.0 h1:khsGInrTrQMyskfcRTE9gP982koSqKobYFJ977JLxhA= -github.com/bool64/progress v0.1.0/go.mod h1:XwIA2+r2sEKxIRa6TFR0FqD3GHt0wz4dk4IL5CwbuKo= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= diff --git a/go.mod b/go.mod index 100f3b4..52c8315 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/bool64/progress go 1.20 -require github.com/bool64/dev v0.2.27 +require ( + github.com/bool64/dev v0.2.28 + github.com/klauspost/compress v1.16.5 + github.com/klauspost/pgzip v1.2.6 +) diff --git a/go.sum b/go.sum index b1bc0d6..f5e50b5 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ -github.com/bool64/dev v0.2.27 h1:mFT+B74mFVgUeUmm/EbfM6ELPA55lEXBjQ/AOHCwCOc= -github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.28 h1:6ayDfrB/jnNr2iQAZHI+uT3Qi6rErSbJYQs1y8rSrwM= +github.com/bool64/dev v0.2.28/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= diff --git a/go.work b/go.work deleted file mode 100644 index e1ff075..0000000 --- a/go.work +++ /dev/null @@ -1,3 +0,0 @@ -go 1.20 - -use ./cmd diff --git a/progress.go b/progress.go index 68bc400..7e6e676 100644 --- a/progress.go +++ b/progress.go @@ -1,3 +1,4 @@ +// Package progress provides helpers to print progress status. package progress import ( @@ -8,8 +9,8 @@ import ( "time" ) -// ProgressStatus describes current progress. -type ProgressStatus struct { +// Status describes current progress. +type Status struct { Task string DonePercent float64 LinesCompleted int64 @@ -17,13 +18,13 @@ type ProgressStatus struct { SpeedLPS float64 Elapsed time.Duration Remaining time.Duration - Metrics []ProgressMetric + Metrics []Metric } // Progress reports reading performance. type Progress struct { Interval time.Duration - Print func(status ProgressStatus) + Print func(status Status) ShowHeapStats bool ShowLinesStats bool done chan bool @@ -31,30 +32,30 @@ type Progress struct { lines func() int64 current func() int64 tot func() int64 - prnt func(s ProgressStatus) + prnt func(s Status) start time.Time - metrics []ProgressMetric + metrics []Metric } -// ProgressType describes metric value. -type ProgressType string +// Type describes metric value. +type Type string -// ProgressType values. +// Type values. const ( - ProgressBytes = ProgressType("bytes") - ProgressDuration = ProgressType("duration") - ProgressGauge = ProgressType("gauge") + Bytes = Type("bytes") + Duration = Type("duration") + Gauge = Type("gauge") ) -// ProgressMetric is an operation metric. -type ProgressMetric struct { +// Metric is an operation metric. +type Metric struct { Name string - Type ProgressType + Type Type Value *int64 } -// DefaultStatus renders ProgressStatus as a string. -func DefaultStatus(s ProgressStatus) string { +// DefaultStatus renders Status as a string. +func DefaultStatus(s Status) string { if s.Task != "" { s.Task += ": " } @@ -71,18 +72,18 @@ func DefaultStatus(s ProgressStatus) string { return res } -// MetricsStatus renders ProgressStatus metrics as a string. -func MetricsStatus(s ProgressStatus) string { +// MetricsStatus renders Status metrics as a string. +func MetricsStatus(s Status) string { metrics := "" for _, m := range s.Metrics { switch m.Type { - case ProgressBytes: + case Bytes: spdMBPS := float64(atomic.LoadInt64(m.Value)) / (s.Elapsed.Seconds() * 1024 * 1024) metrics += fmt.Sprintf("%s: %.1f MB/s, ", m.Name, spdMBPS) - case ProgressDuration: + case Duration: metrics += m.Name + ": " + time.Duration(atomic.LoadInt64(m.Value)).String() + ", " - case ProgressGauge: + case Gauge: metrics += fmt.Sprintf("%s: %d, ", m.Name, atomic.LoadInt64(m.Value)) } } @@ -94,6 +95,7 @@ func MetricsStatus(s ProgressStatus) string { return metrics } +// Task describes long running process. type Task struct { TotalBytes func() int64 CurrentBytes func() int64 @@ -123,7 +125,7 @@ func (p *Progress) Start(options ...func(t *Task)) { p.prnt = p.Print if p.prnt == nil { - p.prnt = func(s ProgressStatus) { + p.prnt = func(s Status) { println(DefaultStatus(s)) } } @@ -151,12 +153,12 @@ func (p *Progress) Start(options ...func(t *Task)) { } // AddMetrics adds more metrics to progress status message. -func (p *Progress) AddMetrics(metrics ...ProgressMetric) { +func (p *Progress) AddMetrics(metrics ...Metric) { p.metrics = append(p.metrics, metrics...) } func (p *Progress) printStatus(last bool) { - s := ProgressStatus{} + s := Status{} s.Task = p.task s.LinesCompleted = p.lines() s.Metrics = p.metrics @@ -251,5 +253,5 @@ func (cr *CountingWriter) Lines() int64 { // MetricsExposer provides metric counters. type MetricsExposer interface { - Metrics() []ProgressMetric + Metrics() []Metric }