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.
This commit is contained in:
2021-08-08 20:26:55 +01:00
parent 99a28a0346
commit ce4b06f67c
8 changed files with 1771 additions and 0 deletions

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/romdo/go-conventionalcommit
go 1.15
require github.com/stretchr/testify v1.7.0

11
go.sum Normal file
View File

@@ -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=

102
line.go Normal file
View File

@@ -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())
}

594
line_test.go Normal file
View File

@@ -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()
}
})
}
}

30
paragraph.go Normal file
View File

@@ -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
}

338
paragraph_test.go Normal file
View File

@@ -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)
})
}
}

50
raw_message.go Normal file
View File

@@ -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()
}

641
raw_message_test.go Normal file
View File

@@ -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()
}
})
}
}