From 99a28a0346d165ed6fc0dbde3af28d2dcc39a77b Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sun, 8 Aug 2021 20:31:07 +0100 Subject: [PATCH 1/2] ci(github): setup GitHub Actions --- .github/workflows/ci.yml | 145 +++++++++++++++++++++++++++++ .gitignore | 4 + .golangci.yml | 93 +++++++++++++++++++ Makefile | 192 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Makefile 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)) From ce4b06f67cde027e784f91227e064ae9e778bee0 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sun, 8 Aug 2021 20:26:55 +0100 Subject: [PATCH 2/2] feat(parser): implement RawMessage The NewRawMessage function returns a RawMessage struct, which has broken the given commit message down into separate lines, and also grouped the lines into paragraphs. This should make it easier to implement proper conventional commit parser, linter, and formatter. --- go.mod | 5 + go.sum | 11 + line.go | 102 +++++++ line_test.go | 594 ++++++++++++++++++++++++++++++++++++++++ paragraph.go | 30 +++ paragraph_test.go | 338 +++++++++++++++++++++++ raw_message.go | 50 ++++ raw_message_test.go | 641 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1771 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 line.go create mode 100644 line_test.go create mode 100644 paragraph.go create mode 100644 paragraph_test.go create mode 100644 raw_message.go create mode 100644 raw_message_test.go 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() + } + }) + } +}