diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a27e751 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +--- +name: CI +on: [push] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.41 + env: + VERBOSE: "true" + + tidy: + name: Tidy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Check if mods are tidy + run: make check-tidy + + benchmark: + name: Benchmarks + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/main' + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run benchmarks + run: make bench | tee output.raw + - name: Fix benchmark names + run: >- + perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw | + tr '-' '_' | tee output.txt + - name: Announce benchmark result + uses: rhysd/github-action-benchmark@v1 + with: + tool: "go" + output-file-path: output.txt + fail-on-alert: true + comment-on-alert: true + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: false + + cov: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Publish coverage + uses: paambaati/codeclimate-action@v2.7.4 + env: + VERBOSE: "true" + GOMAXPROCS: 4 + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: make cov + prefix: github.com/${{ github.repository }} + coverageLocations: | + ${{ github.workspace }}/coverage.out:gocov + + test: + name: Test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go_version: + - "1.15" + - "1.16" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.terraform_version }} + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run tests + run: make test + env: + VERBOSE: "true" + + benchmark-store: + name: Store benchmarks + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run benchmarks + run: make bench | tee output.raw + - name: Fix benchmark names + run: >- + perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw | + tr '-' '_' | tee output.txt + - name: Store benchmark result + uses: rhysd/github-action-benchmark@v1 + with: + tool: "go" + output-file-path: output.txt + github-token: ${{ secrets.ROMDOBOT_TOKEN }} + comment-on-alert: true + auto-push: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2663299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/*.tidy-check +/bin/* +/coverage.out +/output.txt diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..bf7eea1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,93 @@ +linters-settings: + funlen: + lines: 100 + statements: 150 + goconst: + min-occurrences: 5 + gocyclo: + min-complexity: 20 + golint: + min-confidence: 0 + govet: + check-shadowing: true + enable-all: true + disable: + - fieldalignment + lll: + line-length: 80 + tab-width: 4 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - deadcode + - depguard + - dupl + - durationcheck + - errcheck + - errorlint + - exhaustive + - exportloopref + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - godot + - gofumpt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + - lll + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - prealloc + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - structcheck + - tparallel + - typecheck + - unconvert + - unparam + - unused + - varcheck + - wastedassign + - whitespace + +issues: + exclude: + - Using the variable on range scope `tt` in function literal + - Using the variable on range scope `tc` in function literal + exclude-rules: + - path: "_test\\.go" + linters: + - funlen + - dupl + - goconst + - source: "^//go:generate " + linters: + - lll + - source: "`json:" + linters: + - lll + +run: + timeout: 2m + allow-parallel-runners: true + modules-download-mode: readonly diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe98967 --- /dev/null +++ b/Makefile @@ -0,0 +1,192 @@ +GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //') +SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \ + -or -name "Makefile") + +# Verbose output +ifdef VERBOSE +V = -v +endif + +# +# Environment +# + +BINDIR := bin +TOOLDIR := $(BINDIR)/tools + +# Global environment variables for all targets +SHELL ?= /bin/bash +SHELL := env \ + GO111MODULE=on \ + GOBIN=$(CURDIR)/$(TOOLDIR) \ + CGO_ENABLED=1 \ + PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \ + $(SHELL) + +# +# Defaults +# + +# Default target +.DEFAULT_GOAL := test + +# +# Tools +# + +TOOLS += $(TOOLDIR)/gobin +$(TOOLDIR)/gobin: + GO111MODULE=off go get -u github.com/myitcv/gobin + +# external tool +define tool # 1: binary-name, 2: go-import-path +TOOLS += $(TOOLDIR)/$(1) + +$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile + gobin $(V) "$(2)" +endef + +$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) +$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41)) +$(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) + +.PHONY: tools +tools: $(TOOLS) + +# +# Development +# + +BENCH ?= . +TESTARGS ?= + +.PHONY: clean +clean: + rm -f $(TOOLS) + rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check + +.PHONY: test +test: + go test $(V) -count=1 -race $(TESTARGS) ./... + +.PHONY: test-deps +test-deps: + go test all + +.PHONY: lint +lint: $(TOOLDIR)/golangci-lint + golangci-lint $(V) run + +.PHONY: format +format: $(TOOLDIR)/gofumports + gofumports -w . + +.SILENT: bench +.PHONY: bench +bench: + go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./... + +# +# Code Generation +# + +.PHONY: generate +generate: + go generate ./... + +.PHONY: check-generate +check-generate: + $(eval CHKDIR := $(shell mktemp -d)) + cp -av . "$(CHKDIR)" + make -C "$(CHKDIR)/" generate + ( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \ + ( rm -rf "$(CHKDIR)" && exit 1 ) + +# +# Coverage +# + +.PHONY: cov +cov: coverage.out + +.PHONY: cov-html +cov-html: coverage.out + go tool cover -html=./coverage.out + +.PHONY: cov-func +cov-func: coverage.out + go tool cover -func=./coverage.out + +coverage.out: $(SOURCES) + go test $(V) -covermode=count -coverprofile=./coverage.out ./... + +# +# Dependencies +# + +.PHONY: deps +deps: + go mod download + +.PHONY: deps-update +deps-update: + go get -u -t ./... + +.PHONY: deps-analyze +deps-analyze: $(TOOLDIR)/gomod + gomod analyze + +.PHONY: tidy +tidy: + go mod tidy $(V) + +.PHONY: verify +verify: + go mod verify + +.SILENT: check-tidy +.PHONY: check-tidy +check-tidy: + cp go.mod go.mod.tidy-check + cp go.sum go.sum.tidy-check + go mod tidy + ( \ + diff go.mod go.mod.tidy-check && \ + diff go.sum go.sum.tidy-check && \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum \ + ) || ( \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum; \ + exit 1 \ + ) + +# +# Documentation +# + +# Serve docs +.PHONY: docs +docs: godoc + $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) + @godoc -http=127.0.0.1:6060 + +# +# Release +# + +.PHONY: new-version +new-version: check-npx + npx standard-version + +.PHONY: next-version +next-version: check-npx + npx standard-version --dry-run + +.PHONY: check-npx +check-npx: + $(if $(shell which npx),,\ + $(error No npx found in PATH, please install NodeJS)) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5ac682f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/romdo/go-conventionalcommit + +go 1.15 + +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/line.go b/line.go new file mode 100644 index 0000000..f492401 --- /dev/null +++ b/line.go @@ -0,0 +1,102 @@ +package conventionalcommit + +const ( + lf = 10 // linefeed ("\n") character + cr = 13 // carriage return ("\r") character +) + +// Line represents a single line of text defined as; A continuous sequence of +// bytes which do not contain a \r (carriage return) or \n (line-feed) byte. +type Line struct { + // Line number within commit message, starting a 1 rather than 0, as + // text viewed in a text editor starts on line 1, not line 0. + Number int + + // Content is the raw bytes that make up the text content in the line. + Content []byte + + // Break is the linebreak type used at the end of the line. It will be one + // of "\n", "\r\n", "\r", or empty if it is the very last line. + Break []byte +} + +// Lines is a slice of *Line types with some helper methods attached. +type Lines []*Line + +// NewLines breaks the given byte slice down into a slice of Line structs, +// allowing easier inspection and manipulation of content on a line-by-line +// basis. +func NewLines(content []byte) Lines { + r := Lines{} + + if len(content) == 0 { + return r + } + + // List of start/end offsets for each line break. + var breaks [][]int + + // Locate each line break within content. + for i := 0; i < len(content); i++ { + if content[i] == lf { + breaks = append(breaks, []int{i, i + 1}) + } else if content[i] == cr { + b := []int{i, i + 1} + if i+1 < len(content) && content[i+1] == lf { + b[1]++ + i++ + } + breaks = append(breaks, b) + } + } + + // Return a single line if there are no line breaks. + if len(breaks) == 0 { + return Lines{{Number: 1, Content: content, Break: []byte{}}} + } + + // Extract each line based on linebreak offsets. + offset := 0 + for n, loc := range breaks { + r = append(r, &Line{ + Number: n + 1, + Content: content[offset:loc[0]], + Break: content[loc[0]:loc[1]], + }) + offset = loc[1] + } + + // Extract final line + r = append(r, &Line{ + Number: len(breaks) + 1, + Content: content[offset:], + Break: []byte{}, + }) + + return r +} + +// Bytes combines all Lines into a single byte slice, retaining the original +// line break types for each line. +func (s Lines) Bytes() []byte { + // Pre-calculate capacity of result byte slice. + size := 0 + for _, l := range s { + size = size + len(l.Content) + len(l.Break) + } + + b := make([]byte, 0, size) + + for _, l := range s { + b = append(b, l.Content...) + b = append(b, l.Break...) + } + + return b +} + +// Bytes combines all Lines into a single string, retaining the original line +// break types for each line. +func (s Lines) String() string { + return string(s.Bytes()) +} diff --git a/line_test.go b/line_test.go new file mode 100644 index 0000000..4d39057 --- /dev/null +++ b/line_test.go @@ -0,0 +1,594 @@ +package conventionalcommit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewLines(t *testing.T) { + tests := []struct { + name string + content []byte + want Lines + }{ + { + name: "nil", + content: nil, + want: Lines{}, + }, + { + name: "empty", + content: []byte{}, + want: Lines{}, + }, + { + name: "single line without trailing linebreak", + content: []byte("hello world"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte{}, + }, + }, + }, + { + name: "single line with trailing LF", + content: []byte("hello world\n"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "single line with trailing CRLF", + content: []byte("hello world\r\n"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "single line with trailing CR", + content: []byte("hello world\r"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by LF", + content: []byte("hello world\nfoo\nbar"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by LF with trailing LF", + content: []byte("hello world\nfoo\nbar\n"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by CRLF", + content: []byte("hello world\r\nfoo\r\nbar"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r\n"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by CRLF with trailing CRLF", + content: []byte("hello world\r\nfoo\r\nbar\r\n"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r\n"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte("\r\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by CR", + content: []byte("hello world\rfoo\rbar"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\r"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by CR with trailing CR", + content: []byte("hello world\rfoo\rbar\r"), + want: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r"), + }, + { + Number: 2, + Content: []byte("foo"), + Break: []byte("\r"), + }, + { + Number: 3, + Content: []byte("bar"), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by mixed break types", + content: []byte("hello\nworld\r\nfoo\rbar"), + want: Lines{ + { + Number: 1, + Content: []byte("hello"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("world"), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("foo"), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("bar"), + Break: []byte{}, + }, + }, + }, + { + name: "multiple lines separated by mixed break types with " + + "trailing LF", + content: []byte("hello\nworld\r\nfoo\rbar\n"), + want: Lines{ + { + Number: 1, + Content: []byte("hello"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("world"), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("foo"), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("bar"), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewLines(tt.content) + + assert.Equal(t, tt.want, got) + }) + } +} + +var linesBytesTestCases = []struct { + name string + lines Lines + want []byte +}{ + { + name: "single line", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + }, + }, + want: []byte("hello world"), + }, + { + name: "single line with trailing LF", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte("hello world\n"), + }, + { + name: "single line with trailing CRLF", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte("hello world\r\n"), + }, + { + name: "single line with trailing CR", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\r"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte("hello world\r"), + }, + { + name: "multi-line separated by LF", + lines: Lines{ + { + Number: 3, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte("Sed bibendum."), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 7, + Content: []byte("Integer placerat tristique nisl."), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte("Etiam vel neque nec dui bibendum."), + Break: []byte("\n"), + }, + { + Number: 9, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 10, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 11, + Content: []byte("Nullam libero mauris, dictum id, arcu."), + Break: []byte("\n"), + }, + { + Number: 12, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte( + "Aliquam feugiat tellus ut neque.\n" + + "Sed bibendum.\n" + + "Nullam libero mauris, consequat.\n" + + "\n" + + "Integer placerat tristique nisl.\n" + + "Etiam vel neque nec dui bibendum.\n" + + "\n" + + "\n" + + "Nullam libero mauris, dictum id, arcu.\n", + ), + }, + { + name: "multi-line separated by CRLF", + lines: Lines{ + { + Number: 3, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r\n"), + }, + { + Number: 4, + Content: []byte("Sed bibendum."), + Break: []byte("\r\n"), + }, + { + Number: 5, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\r\n"), + }, + { + Number: 6, + Content: []byte(""), + Break: []byte("\r\n"), + }, + { + Number: 7, + Content: []byte("Integer placerat tristique nisl."), + Break: []byte("\r\n"), + }, + { + Number: 8, + Content: []byte("Etiam vel neque nec dui bibendum."), + Break: []byte("\r\n"), + }, + { + Number: 9, + Content: []byte(""), + Break: []byte("\r\n"), + }, + { + Number: 10, + Content: []byte(""), + Break: []byte("\r\n"), + }, + { + Number: 11, + Content: []byte("Nullam libero mauris, dictum id, arcu."), + Break: []byte("\r\n"), + }, + { + Number: 12, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte( + "Aliquam feugiat tellus ut neque.\r\n" + + "Sed bibendum.\r\n" + + "Nullam libero mauris, consequat.\r\n" + + "\r\n" + + "Integer placerat tristique nisl.\r\n" + + "Etiam vel neque nec dui bibendum.\r\n" + + "\r\n" + + "\r\n" + + "Nullam libero mauris, dictum id, arcu.\r\n", + ), + }, + { + name: "multi-line separated by CR", + lines: Lines{ + { + Number: 3, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("Sed bibendum."), + Break: []byte("\r"), + }, + { + Number: 5, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\r"), + }, + { + Number: 6, + Content: []byte(""), + Break: []byte("\r"), + }, + { + Number: 7, + Content: []byte("Integer placerat tristique nisl."), + Break: []byte("\r"), + }, + { + Number: 8, + Content: []byte("Etiam vel neque nec dui bibendum."), + Break: []byte("\r"), + }, + { + Number: 9, + Content: []byte(""), + Break: []byte("\r"), + }, + { + Number: 10, + Content: []byte(""), + Break: []byte("\r"), + }, + { + Number: 11, + Content: []byte("Nullam libero mauris, dictum id, arcu."), + Break: []byte("\r"), + }, + { + Number: 12, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []byte( + "Aliquam feugiat tellus ut neque.\r" + + "Sed bibendum.\r" + + "Nullam libero mauris, consequat.\r" + + "\r" + + "Integer placerat tristique nisl.\r" + + "Etiam vel neque nec dui bibendum.\r" + + "\r" + + "\r" + + "Nullam libero mauris, dictum id, arcu.\r", + ), + }, +} + +func TestLines_Bytes(t *testing.T) { + for _, tt := range linesBytesTestCases { + t.Run(tt.name, func(t *testing.T) { + got := tt.lines.Bytes() + + assert.Equal(t, tt.want, got) + }) + } +} + +func BenchmarkLines_Bytes(b *testing.B) { + for _, tt := range linesBytesTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.lines.Bytes() + } + }) + } +} + +func TestLines_String(t *testing.T) { + for _, tt := range linesBytesTestCases { + t.Run(tt.name, func(t *testing.T) { + got := tt.lines.String() + + assert.Equal(t, string(tt.want), got) + }) + } +} + +func BenchmarkLines_String(b *testing.B) { + for _, tt := range linesBytesTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.lines.String() + } + }) + } +} diff --git a/paragraph.go b/paragraph.go new file mode 100644 index 0000000..deaad3c --- /dev/null +++ b/paragraph.go @@ -0,0 +1,30 @@ +package conventionalcommit + +import "bytes" + +// Paragraph represents a textual paragraph defined as; A continuous sequence of +// textual lines which are not empty or and do not consist of only whitespace. +type Paragraph struct { + // Lines is a list of lines which collectively form a paragraph. + Lines Lines +} + +func NewParagraphs(lines Lines) []*Paragraph { + r := []*Paragraph{} + + paragraph := &Paragraph{Lines: Lines{}} + for _, line := range lines { + if len(bytes.TrimSpace(line.Content)) > 0 { + paragraph.Lines = append(paragraph.Lines, line) + } else if len(paragraph.Lines) > 0 { + r = append(r, paragraph) + paragraph = &Paragraph{Lines: Lines{}} + } + } + + if len(paragraph.Lines) > 0 { + r = append(r, paragraph) + } + + return r +} diff --git a/paragraph_test.go b/paragraph_test.go new file mode 100644 index 0000000..db7fc52 --- /dev/null +++ b/paragraph_test.go @@ -0,0 +1,338 @@ +package conventionalcommit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewParagraphs(t *testing.T) { + tests := []struct { + name string + lines Lines + want []*Paragraph + }{ + { + name: "nil", + lines: nil, + want: []*Paragraph{}, + }, + { + name: "no lines", + lines: Lines{}, + want: []*Paragraph{}, + }, + { + name: "single empty line", + lines: Lines{ + { + Number: 1, + Content: []byte{}, + Break: []byte{}, + }, + }, + want: []*Paragraph{}, + }, + { + name: "multiple empty lines", + lines: Lines{ + { + Number: 1, + Content: []byte{}, + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte{}, + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte{}, + Break: []byte{}, + }, + }, + want: []*Paragraph{}, + }, + { + name: "single whitespace line", + lines: Lines{ + { + Number: 1, + Content: []byte("\t "), + Break: []byte{}, + }, + }, + want: []*Paragraph{}, + }, + { + name: "multiple whitespace lines", + lines: Lines{ + { + Number: 1, + Content: []byte{}, + Break: []byte("\t "), + }, + { + Number: 2, + Content: []byte{}, + Break: []byte("\t "), + }, + { + Number: 3, + Content: []byte("\t "), + Break: []byte{}, + }, + }, + want: []*Paragraph{}, + }, + { + name: "single line", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte{}, + }, + }, + want: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte{}, + }, + }, + }, + }, + }, + { + name: "multiple lines", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo bar"), + Break: []byte{}, + }, + }, + want: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo bar"), + Break: []byte{}, + }, + }, + }, + }, + }, + { + name: "multiple lines with trailing line break", + lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo bar"), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("hello world"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("foo bar"), + Break: []byte("\n"), + }, + }, + }, + }, + }, + { + name: "multiple paragraphs with excess blank lines", + lines: Lines{ + { + Number: 1, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("\t "), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("Sed bibendum."), + Break: []byte("\r"), + }, + { + Number: 5, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 7, + Content: []byte("Integer placerat tristique nisl."), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte("Etiam vel neque nec dui bibendum."), + Break: []byte("\n"), + }, + { + Number: 9, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 10, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 11, + Content: []byte("\t\t"), + Break: []byte("\n"), + }, + { + Number: 12, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 13, + Content: []byte("Donec hendrerit tempor tellus."), + Break: []byte("\n"), + }, + { + Number: 14, + Content: []byte("In id erat non orci commodo lobortis."), + Break: []byte("\n"), + }, + { + Number: 15, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 16, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 17, + Content: []byte("\t\t"), + Break: []byte("\n"), + }, + { + Number: 18, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 18, + Content: []byte(""), + Break: []byte{}, + }, + }, + want: []*Paragraph{ + { + Lines: Lines{ + { + Number: 3, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("Sed bibendum."), + Break: []byte("\r"), + }, + { + Number: 5, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 7, + Content: []byte("Integer placerat tristique nisl."), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte( + "Etiam vel neque nec dui bibendum.", + ), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 13, + Content: []byte("Donec hendrerit tempor tellus."), + Break: []byte("\n"), + }, + { + Number: 14, + Content: []byte( + "In id erat non orci commodo lobortis.", + ), + Break: []byte("\n"), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewParagraphs(tt.lines) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/raw_message.go b/raw_message.go new file mode 100644 index 0000000..01c0f8b --- /dev/null +++ b/raw_message.go @@ -0,0 +1,50 @@ +package conventionalcommit + +// RawMessage represents a commit message in a more structured form than a +// simple string or byte slice. This makes it easier to process a message for +// the purposes of extracting detailed information, linting, and formatting. +type RawMessage struct { + // Lines is a list of all individual lines of text in the commit message, + // which also includes the original line number, making it easy to pass a + // single Line around while still knowing where in the original commit + // message it belongs. + Lines Lines + + // Paragraphs is a list of textual paragraphs in the commit message. A + // paragraph is defined as any continuous sequence of lines which are not + // empty or consist of only whitespace. + Paragraphs []*Paragraph +} + +// NewRawMessage returns a RawMessage, with the given commit message broken down +// into individual lines of text, with sequential non-empty lines grouped into +// paragraphs. +func NewRawMessage(message []byte) *RawMessage { + r := &RawMessage{ + Lines: Lines{}, + Paragraphs: []*Paragraph{}, + } + + if len(message) == 0 { + return r + } + + r.Lines = NewLines(message) + r.Paragraphs = NewParagraphs(r.Lines) + + return r +} + +// Bytes renders the RawMessage back into a byte slice which is identical to the +// original input byte slice given to NewRawMessage. This includes retaining the +// original line break types for each line. +func (s *RawMessage) Bytes() []byte { + return s.Lines.Bytes() +} + +// String renders the RawMessage back into a string which is identical to the +// original input byte slice given to NewRawMessage. This includes retaining the +// original line break types for each line. +func (s *RawMessage) String() string { + return s.Lines.String() +} diff --git a/raw_message_test.go b/raw_message_test.go new file mode 100644 index 0000000..f7653a6 --- /dev/null +++ b/raw_message_test.go @@ -0,0 +1,641 @@ +package conventionalcommit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var rawMessageTestCases = []struct { + name string + bytes []byte + rawMessage *RawMessage +}{ + { + name: "nil", + bytes: nil, + rawMessage: &RawMessage{ + Lines: Lines{}, + Paragraphs: []*Paragraph{}, + }, + }, + { + name: "empty", + bytes: []byte(""), + rawMessage: &RawMessage{ + Lines: Lines{}, + Paragraphs: []*Paragraph{}, + }, + }, + { + name: "single space", + bytes: []byte(" "), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte(" "), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{}, + }, + }, + { + name: "subject only", + bytes: []byte("fix: a broken thing"), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte{}, + }, + }, + }, + }, + }, + }, + { + name: "subject and body", + bytes: []byte("fix: a broken thing\n\nIt is now fixed."), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + }, + }, + }, + }, + { + name: "subject and body with CRLF line breaks", + bytes: []byte("fix: a broken thing\r\n\r\nIt is now fixed."), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\r\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\r\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + }, + }, + }, + }, + { + name: "subject and body with CR line breaks", + bytes: []byte("fix: a broken thing\r\rIt is now fixed."), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\r"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\r"), + }, + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\r"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + }, + }, + }, + }, + { + name: "separated by whitespace line", + bytes: []byte("fix: a broken thing\n \nIt is now fixed."), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 3, + Content: []byte("It is now fixed."), + Break: []byte{}, + }, + }, + }, + }, + }, + }, + { + name: "subject and long body", + bytes: []byte(`fix: something broken + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec hendrerit +tempor tellus. Donec pretium posuere tellus. Proin quam nisl, tincidunt et, +mattis eget, convallis nec, purus. Cum sociis natoque penatibus et magnis dis +parturient montes, nascetur ridiculous mus. Nulla posuere. Donec vitae dolor. +Nullam tristique diam non turpis. Cras placerat accumsan nulla. Nullam rutrum. +Nam vestibulum accumsan nisl. + +Nullam eu ante vel est convallis dignissim. Fusce suscipit, wisi nec facilisis +facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc porta +vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam posuere. +Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa, quis +varius mi purus non odio. Pellentesque condimentum, magna ut suscipit hendrerit, +ipsum augue ornare nulla, non luctus diam neque sit amet urna. Curabitur +vulputate vestibulum lorem. Fusce sagittis, libero non molestie mollis, magna +orci ultrices dolor, at vulputate neque nulla lacinia eros. Sed id ligula quis +est convallis tempor. Curabitur lacinia pulvinar nibh. Nam a sapien. + +Phasellus lacus. Nam euismod tellus id erat.`), + rawMessage: &RawMessage{ + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: something broken"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte( + "Lorem ipsum dolor sit amet, consectetuer " + + "adipiscing elit. Donec hendrerit"), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte( + "tempor tellus. Donec pretium posuere tellus. " + + "Proin quam nisl, tincidunt et,"), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte( + "mattis eget, convallis nec, purus. Cum sociis " + + "natoque penatibus et magnis dis"), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte( + "parturient montes, nascetur ridiculous mus. " + + "Nulla posuere. Donec vitae dolor."), + Break: []byte("\n"), + }, + { + Number: 7, + Content: []byte( + "Nullam tristique diam non turpis. Cras placerat " + + "accumsan nulla. Nullam rutrum."), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte( + "Nam vestibulum accumsan nisl."), + Break: []byte("\n"), + }, + { + Number: 9, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 10, + Content: []byte( + "Nullam eu ante vel est convallis dignissim. " + + "Fusce suscipit, wisi nec facilisis", + ), + Break: []byte("\n"), + }, + { + Number: 11, + Content: []byte( + "facilisis, est dui fermentum leo, quis tempor " + + "ligula erat quis odio. Nunc porta", + ), + Break: []byte("\n"), + }, + { + Number: 12, + Content: []byte( + "vulputate tellus. Nunc rutrum turpis sed pede. " + + "Sed bibendum. Aliquam posuere.", + ), + Break: []byte("\n"), + }, + { + Number: 13, + Content: []byte( + "Nunc aliquet, augue nec adipiscing interdum, " + + "lacus tellus malesuada massa, quis", + ), + Break: []byte("\n"), + }, + { + Number: 14, + Content: []byte( + "varius mi purus non odio. Pellentesque " + + "condimentum, magna ut suscipit hendrerit,", + ), + Break: []byte("\n"), + }, + { + Number: 15, + Content: []byte( + "ipsum augue ornare nulla, non luctus diam neque " + + "sit amet urna. Curabitur", + ), + Break: []byte("\n"), + }, + { + Number: 16, + Content: []byte( + "vulputate vestibulum lorem. Fusce sagittis, " + + "libero non molestie mollis, magna", + ), + Break: []byte("\n"), + }, + { + Number: 17, + Content: []byte( + "orci ultrices dolor, at vulputate neque nulla " + + "lacinia eros. Sed id ligula quis", + ), + Break: []byte("\n"), + }, + { + Number: 18, + Content: []byte( + "est convallis tempor. Curabitur lacinia " + + "pulvinar nibh. Nam a sapien.", + ), + Break: []byte("\n"), + }, + { + Number: 19, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 20, + Content: []byte( + "Phasellus lacus. Nam euismod tellus id erat.", + ), + Break: []byte{}, + }, + }, + Paragraphs: []*Paragraph{ + { + Lines: Lines{ + { + Number: 1, + Content: []byte("fix: something broken"), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 3, + Content: []byte( + "Lorem ipsum dolor sit amet, " + + "consectetuer adipiscing elit. Donec " + + "hendrerit", + ), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte( + "tempor tellus. Donec pretium posuere " + + "tellus. Proin quam nisl, tincidunt " + + "et,", + ), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte( + "mattis eget, convallis nec, purus. Cum " + + "sociis natoque penatibus et magnis " + + "dis", + ), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte( + "parturient montes, nascetur ridiculous " + + "mus. Nulla posuere. Donec vitae " + + "dolor.", + ), + Break: []byte("\n"), + }, + { + Number: 7, + Content: []byte( + "Nullam tristique diam non turpis. Cras " + + "placerat accumsan nulla. Nullam " + + "rutrum.", + ), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte( + "Nam vestibulum accumsan nisl.", + ), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 10, + Content: []byte( + "Nullam eu ante vel est convallis " + + "dignissim. Fusce suscipit, wisi nec " + + "facilisis", + ), + Break: []byte("\n"), + }, + { + Number: 11, + Content: []byte( + "facilisis, est dui fermentum leo, quis " + + "tempor ligula erat quis odio. Nunc " + + "porta", + ), + Break: []byte("\n"), + }, + { + Number: 12, + Content: []byte( + "vulputate tellus. Nunc rutrum turpis " + + "sed pede. Sed bibendum. Aliquam " + + "posuere.", + ), + Break: []byte("\n"), + }, + { + Number: 13, + Content: []byte( + "Nunc aliquet, augue nec adipiscing " + + "interdum, lacus tellus malesuada " + + "massa, quis", + ), + Break: []byte("\n"), + }, + { + Number: 14, + Content: []byte( + "varius mi purus non odio. Pellentesque " + + "condimentum, magna ut suscipit " + + "hendrerit,", + ), + Break: []byte("\n"), + }, + { + Number: 15, + Content: []byte( + "ipsum augue ornare nulla, non luctus " + + "diam neque sit amet urna. Curabitur", + ), + Break: []byte("\n"), + }, + { + Number: 16, + Content: []byte( + "vulputate vestibulum lorem. Fusce " + + "sagittis, libero non molestie " + + "mollis, magna", + ), + Break: []byte("\n"), + }, + { + Number: 17, + Content: []byte( + "orci ultrices dolor, at vulputate neque " + + "nulla lacinia eros. Sed id ligula " + + "quis", + ), + Break: []byte("\n"), + }, + { + Number: 18, + Content: []byte( + "est convallis tempor. Curabitur lacinia " + + "pulvinar nibh. Nam a sapien.", + ), + Break: []byte("\n"), + }, + }, + }, + { + Lines: Lines{ + { + Number: 20, + Content: []byte( + "Phasellus lacus. Nam euismod tellus id " + + "erat.", + ), + Break: []byte{}, + }, + }, + }, + }, + }, + }, +} + +func TestNewRawMessage(t *testing.T) { + for _, tt := range rawMessageTestCases { + t.Run(tt.name, func(t *testing.T) { + got := NewRawMessage(tt.bytes) + + assert.Equal(t, tt.rawMessage, got) + }) + } +} + +func BenchmarkNewRawMessage(b *testing.B) { + for _, tt := range rawMessageTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = NewRawMessage(tt.bytes) + } + }) + } +} + +func TestRawMessage_Bytes(t *testing.T) { + for _, tt := range rawMessageTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + got := tt.rawMessage.Bytes() + + assert.Equal(t, tt.bytes, got) + }) + } +} + +func BenchmarkRawMessage_Bytes(b *testing.B) { + for _, tt := range rawMessageTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.rawMessage.Bytes() + } + }) + } +} + +func TestRawMessage_String(t *testing.T) { + for _, tt := range rawMessageTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + got := tt.rawMessage.String() + + assert.Equal(t, string(tt.bytes), got) + }) + } +} + +func BenchmarkRawMessage_String(b *testing.B) { + for _, tt := range rawMessageTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.rawMessage.String() + } + }) + } +}