diff --git a/buffer.go b/buffer.go new file mode 100644 index 0000000..5679b9a --- /dev/null +++ b/buffer.go @@ -0,0 +1,215 @@ +package conventionalcommit + +import ( + "regexp" +) + +// footerToken will match against all variations of Conventional Commit footer +// formats. +// +// Examples of valid footer tokens: +// +// Approved-by: John Carter +// ReviewdBy: Noctis +// Fixes #49 +// Reverts #SOL-42 +// BREAKING CHANGE: Flux capacitor no longer exists. +// BREAKING-CHANGE: Time will flow backwads +// +// Examples of invalid footer tokens: +// +// Approved-by: +// Approved-by:John Carter +// Approved by: John Carter +// ReviewdBy: Noctis +// Fixes#49 +// Fixes # +// Fixes 49 +// BREAKING CHANGE:Flux capacitor no longer exists. +// Breaking Change: Flux capacitor no longer exists. +// Breaking-Change: Time will flow backwads +// +var footerToken = regexp.MustCompile( + `^(?:([\w-]+)\s+(#.+)|([\w-]+|BREAKING[\s-]CHANGE):\s+(.+))$`, +) + +// Buffer 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. +// +// The commit message is conceptually broken down into two three separate +// sections: +// +// - Head section holds the commit message subject/description, along with type +// and scope for conventional commits. The head section should only ever be a +// single line according to git convention, but Buffer supports multi-line +// headers so they can be parsed and handled as needed. +// +// - Body section holds the rest of the message. Except if the last paragraph +// starts with a footer token, then the last paragraph is omitted from the +// body section. +// +// - Foot section holds conventional commit footers. It is always the last +// paragraph of a commit message, and is only considered to be the foot +// section if the first line of the paragraph beings with a footer token. +// +// Each section is returned as a Lines type, which provides per-line access to +// the text within the section. +type Buffer struct { + // firstLine is the lines offset for the first line which contains any + // non-whitespace character. + firstLine int + + // lastLine is the lines offset for the last line which contains any + // non-whitespace character. + lastLine int + + // headLen is the number of lines that the headLen section (first paragraph) + // spans. + headLen int + + // footLen is the number of lines that the footLen section (last paragraph) + // spans. + footLen int + + // 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 +} + +// NewBuffer returns a Buffer, with the given commit message broken down into +// individual lines of text, with sequential non-empty lines grouped into +// paragraphs. +func NewBuffer(message []byte) *Buffer { + buf := &Buffer{ + lines: Lines{}, + } + + if len(message) == 0 { + return buf + } + + buf.lines = NewLines(message) + // Find fist non-whitespace line. + if i := buf.lines.FirstTextIndex(); i > -1 { + buf.firstLine = i + } + + // Find last non-whitespace line. + if i := buf.lines.LastTextIndex(); i > -1 { + buf.lastLine = i + } + + // Determine number of lines in first paragraph (head section). + for i := buf.firstLine; i <= buf.lastLine; i++ { + if buf.lines[i].Blank() { + break + } + buf.headLen++ + } + + // Determine number of lines in the last paragraph. + lastLen := 0 + for i := buf.lastLine; i > buf.firstLine+buf.headLen; i-- { + if buf.lines[i].Blank() { + break + } + lastLen++ + } + + // If last paragraph starts with a Convention Commit footer token, it is the + // foot section, otherwise it is part of the body. + if lastLen > 0 { + line := buf.lines[buf.lastLine-lastLen+1] + if footerToken.Match(line.Content) { + buf.footLen = lastLen + } + } + + return buf +} + +// Head returns the first paragraph, defined as the first group of sequential +// lines which contain any non-whitespace characters. +func (s *Buffer) Head() Lines { + return s.lines[s.firstLine : s.firstLine+s.headLen] +} + +// Body returns all lines between the first and last paragraphs. If the body is +// surrounded by multiple empty lines, they will be removed, ensuring first and +// last line of body is not a blank whitespace line. +func (s *Buffer) Body() Lines { + if s.firstLine == s.lastLine { + return Lines{} + } + + first := s.firstLine + s.headLen + 1 + last := s.lastLine + 1 + + if s.footLen > 0 { + last -= s.footLen + } + + return s.lines[first:last].Trim() +} + +// Head returns the last paragraph, defined as the last group of sequential +// lines which contain any non-whitespace characters. +func (s *Buffer) Foot() Lines { + if s.footLen == 0 { + return Lines{} + } + + return s.lines[s.lastLine-s.footLen+1 : s.lastLine+1] +} + +// Lines returns all lines with any blank lines from the beginning and end of +// the buffer removed. Effectively all lines from the first to the last line +// which contain any non-whitespace characters. +func (s *Buffer) Lines() Lines { + if s.lastLine+1 > len(s.lines) || (s.lastLine == 0 && s.lines[0].Blank()) { + return Lines{} + } + + return s.lines[s.firstLine : s.lastLine+1] +} + +func (s *Buffer) LineCount() int { + if s.headLen == 0 { + return 0 + } + + return (s.lastLine + 1) - s.firstLine +} + +// Bytes renders the Buffer back into a byte slice, without any leading or +// trailing whitespace lines. Leading whitespace on the first line which +// contains non-whitespace characters is retained. It is only whole lines +// consisting of only whitespace which are excluded. +func (s *Buffer) Bytes() []byte { + return s.Lines().Bytes() +} + +// String renders the Buffer back into a string, without any leading or trailing +// whitespace lines. Leading whitespace on the first line which contains +// non-whitespace characters is retained. It is only whole lines consisting of +// only whitespace which are excluded. +func (s *Buffer) String() string { + return s.Lines().String() +} + +// BytesRaw renders the Buffer back into a byte slice which is identical to the +// original input byte slice given to NewBuffer. This includes retaining the +// original line break types for each line. +func (s *Buffer) BytesRaw() []byte { + return s.lines.Bytes() +} + +// StringRaw renders the Buffer back into a string which is identical to the +// original input byte slice given to NewBuffer. This includes retaining the +// original line break types for each line. +func (s *Buffer) StringRaw() string { + return s.lines.String() +} diff --git a/buffer_test.go b/buffer_test.go new file mode 100644 index 0000000..4b0aa9f --- /dev/null +++ b/buffer_test.go @@ -0,0 +1,1107 @@ +package conventionalcommit + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var bufferTestCases = []struct { + name string + bytes []byte + wantBuffer *Buffer + wantHead []int + wantBody []int + wantFoot []int + wantLines [2]int +}{ + { + name: "nil", + bytes: nil, + wantBuffer: &Buffer{ + lines: Lines{}, + }, + wantHead: []int{}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{0, 0}, + }, + { + name: "empty", + bytes: []byte(""), + wantBuffer: &Buffer{ + lines: Lines{}, + }, + wantHead: []int{}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{0, 0}, + }, + { + name: "single whitespace line", + bytes: []byte(" "), + wantBuffer: &Buffer{ + lines: Lines{ + {Number: 1, Content: []byte(" "), Break: []byte{}}, + }, + }, + wantHead: []int{}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{0, 0}, + }, + { + name: "multiple whitespace lines", + bytes: []byte("\n\n \n\n\t\n"), + wantBuffer: &Buffer{ + lines: Lines{ + {Number: 1, Content: []byte(""), Break: []byte("\n")}, + {Number: 2, Content: []byte(""), Break: []byte("\n")}, + {Number: 3, Content: []byte(" "), Break: []byte("\n")}, + {Number: 4, Content: []byte(""), Break: []byte("\n")}, + {Number: 5, Content: []byte("\t"), Break: []byte("\n")}, + {Number: 6, Content: []byte(""), Break: []byte{}}, + }, + }, + wantHead: []int{}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{0, 0}, + }, + { + name: "single line", + bytes: []byte("fix: a broken thing"), + wantBuffer: &Buffer{ + headLen: 1, + lines: Lines{ + { + Number: 1, + Content: []byte("fix: a broken thing"), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{0, 1}, + }, + { + name: "single line surrounded by whitespace", + bytes: []byte("\n \n\nfix: a broken thing\n\t\n"), + wantBuffer: &Buffer{ + firstLine: 3, + lastLine: 3, + headLen: 1, + lines: Lines{ + {Number: 1, Content: []byte(""), Break: []byte("\n")}, + {Number: 2, Content: []byte(" "), Break: []byte("\n")}, + {Number: 3, Content: []byte(""), Break: []byte("\n")}, + { + Number: 4, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + {Number: 5, Content: []byte("\t"), Break: []byte("\n")}, + {Number: 6, Content: []byte(""), Break: []byte{}}, + }, + }, + wantHead: []int{3}, + wantBody: []int{}, + wantFoot: []int{}, + wantLines: [2]int{3, 1}, + }, + { + name: "subject and body", + bytes: []byte("fix: a broken thing\n\nIt is now fixed."), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 2, + headLen: 1, + footLen: 0, + 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{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{}, + wantLines: [2]int{0, 3}, + }, + { + name: "subject and body with word footer token", + bytes: []byte(`fix: a broken thing + +It is now fixed. + +Reviewed-by: John Carter`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 4, + headLen: 1, + footLen: 1, + 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("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("Reviewed-by: John Carter"), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{4}, + wantLines: [2]int{0, 5}, + }, + { + name: "subject and body with reference footer token", + bytes: []byte(`fix: a broken thing + +It is now fixed. + +Fixes #39`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 4, + headLen: 1, + footLen: 1, + 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("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("Fixes #39"), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{4}, + wantLines: [2]int{0, 5}, + }, + { + name: "subject and body with BREAKING CHANGE footer", + bytes: []byte(`refactor!: re-transpile the fugiator + +This should improve performance. + +BREAKING CHANGE: New argument is required, or BOOM!`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 4, + headLen: 1, + footLen: 1, + lines: Lines{ + { + Number: 1, + Content: []byte("refactor!: re-transpile the fugiator"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("This should improve performance."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte( + "BREAKING CHANGE: New argument is required, or BOOM!", + ), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{4}, + wantLines: [2]int{0, 5}, + }, + { + name: "subject and body with BREAKING-CHANGE footer", + bytes: []byte(`refactor!: re-transpile the fugiator + +This should improve performance. + +BREAKING-CHANGE: New argument is required, or BOOM!`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 4, + headLen: 1, + footLen: 1, + lines: Lines{ + { + Number: 1, + Content: []byte("refactor!: re-transpile the fugiator"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("This should improve performance."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte( + "BREAKING-CHANGE: New argument is required, or BOOM!", + ), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{4}, + wantLines: [2]int{0, 5}, + }, + { + name: "subject and body with invalid footer token", + bytes: []byte(`refactor!: re-transpile the fugiator + +This should improve performance. + +Reviewed by: John Carter`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 4, + headLen: 1, + footLen: 0, + lines: Lines{ + { + Number: 1, + Content: []byte("refactor!: re-transpile the fugiator"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("This should improve performance."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("Reviewed by: John Carter"), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2, 3, 4}, + wantFoot: []int{}, + wantLines: [2]int{0, 5}, + }, + { + name: "subject and body with valid footer token on second line", + bytes: []byte(`refactor!: re-transpile the fugiator + +This should improve performance. + +the invalid footer starts here +Reviewed-by: John Carter`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 5, + headLen: 1, + footLen: 0, + lines: Lines{ + { + Number: 1, + Content: []byte("refactor!: re-transpile the fugiator"), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("This should improve performance."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("the invalid footer starts here"), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte("Reviewed-by: John Carter"), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2, 3, 4, 5}, + wantFoot: []int{}, + wantLines: [2]int{0, 6}, + }, + { + name: "subject and body with CRLF line breaks", + bytes: []byte("fix: a broken thing\r\n\r\nIt is now fixed."), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 2, + headLen: 1, + footLen: 0, + 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{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{}, + wantLines: [2]int{0, 3}, + }, + { + name: "subject and body with CR line breaks", + bytes: []byte("fix: a broken thing\r\rIt is now fixed."), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 2, + headLen: 1, + footLen: 0, + 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{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{}, + wantLines: [2]int{0, 3}, + }, + { + name: "separated by whitespace line", + bytes: []byte("fix: a broken thing\n \nIt is now fixed."), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 2, + headLen: 1, + footLen: 0, + 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{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{2}, + wantFoot: []int{}, + wantLines: [2]int{0, 3}, + }, + { + name: "multi-line head and body", + bytes: []byte(` + +foo +bar + +foz +baz + +hello +world + +`), + wantBuffer: &Buffer{ + firstLine: 2, + lastLine: 9, + headLen: 2, + footLen: 0, + lines: Lines{ + {Number: 1, Content: []byte(""), Break: []byte("\n")}, + {Number: 2, Content: []byte(""), Break: []byte("\n")}, + {Number: 3, Content: []byte("foo"), Break: []byte("\n")}, + {Number: 4, Content: []byte("bar"), Break: []byte("\n")}, + {Number: 5, Content: []byte(""), Break: []byte("\n")}, + {Number: 6, Content: []byte("foz"), Break: []byte("\n")}, + {Number: 7, Content: []byte("baz"), Break: []byte("\n")}, + {Number: 8, Content: []byte(""), Break: []byte("\n")}, + {Number: 9, Content: []byte("hello"), Break: []byte("\n")}, + {Number: 10, Content: []byte("world"), Break: []byte("\n")}, + {Number: 11, Content: []byte(""), Break: []byte("\n")}, + {Number: 12, Content: []byte(""), Break: []byte{}}, + }, + }, + wantHead: []int{2, 3}, + wantBody: []int{5, 6, 7, 8, 9}, + wantFoot: []int{}, + wantLines: [2]int{2, 8}, + }, + { + name: "body surrounded by whitespace lines", + bytes: []byte(` + +foo +bar + + + +foz +baz + + + +hello +world + + +`), + wantBuffer: &Buffer{ + firstLine: 2, + lastLine: 13, + headLen: 2, + footLen: 0, + lines: Lines{ + {Number: 1, Content: []byte(""), Break: []byte("\n")}, + {Number: 2, Content: []byte(""), Break: []byte("\n")}, + {Number: 3, Content: []byte("foo"), Break: []byte("\n")}, + {Number: 4, Content: []byte("bar"), Break: []byte("\n")}, + {Number: 5, Content: []byte(""), Break: []byte("\n")}, + {Number: 6, Content: []byte(""), Break: []byte("\n")}, + {Number: 7, Content: []byte(""), Break: []byte("\n")}, + {Number: 8, Content: []byte("foz"), Break: []byte("\n")}, + {Number: 9, Content: []byte("baz"), Break: []byte("\n")}, + {Number: 10, Content: []byte(""), Break: []byte("\n")}, + {Number: 11, Content: []byte(""), Break: []byte("\n")}, + {Number: 12, Content: []byte(""), Break: []byte("\n")}, + {Number: 13, Content: []byte("hello"), Break: []byte("\n")}, + {Number: 14, Content: []byte("world"), Break: []byte("\n")}, + {Number: 15, Content: []byte(""), Break: []byte("\n")}, + {Number: 16, Content: []byte(""), Break: []byte("\n")}, + {Number: 17, Content: []byte(""), Break: []byte{}}, + }, + }, + wantHead: []int{2, 3}, + wantBody: []int{7, 8, 9, 10, 11, 12, 13}, + wantFoot: []int{}, + wantLines: [2]int{2, 12}, + }, + { + name: "whitespace-only body", + bytes: []byte(` + +foo +bar + + + + +Approved-by: John Smith + +`), + wantBuffer: &Buffer{ + firstLine: 2, + lastLine: 8, + headLen: 2, + footLen: 1, + lines: Lines{ + {Number: 1, Content: []byte(""), Break: []byte("\n")}, + {Number: 2, Content: []byte(""), Break: []byte("\n")}, + {Number: 3, Content: []byte("foo"), Break: []byte("\n")}, + {Number: 4, Content: []byte("bar"), Break: []byte("\n")}, + {Number: 5, Content: []byte(""), Break: []byte("\n")}, + {Number: 6, Content: []byte(""), Break: []byte("\n")}, + {Number: 7, Content: []byte(""), Break: []byte("\n")}, + {Number: 8, Content: []byte(""), Break: []byte("\n")}, + { + Number: 9, + Content: []byte("Approved-by: John Smith"), + Break: []byte("\n"), + }, + {Number: 10, Content: []byte(""), Break: []byte("\n")}, + {Number: 11, Content: []byte(""), Break: []byte{}}, + }, + }, + wantHead: []int{2, 3}, + wantBody: []int{}, + wantFoot: []int{8}, + wantLines: [2]int{2, 7}, + }, + { + name: "subject and body surrounded by whitespace", + bytes: []byte( + "\n \nfix: a broken thing\n\nIt is now fixed.\n \n\n", + ), + wantBuffer: &Buffer{ + firstLine: 2, + lastLine: 4, + headLen: 1, + footLen: 0, + lines: Lines{ + { + Number: 1, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("fix: a broken thing"), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte("It is now fixed."), + Break: []byte("\n"), + }, + { + Number: 6, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 7, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 8, + Content: []byte(""), + Break: []byte{}, + }, + }, + }, + wantHead: []int{2}, + wantBody: []int{4}, + wantFoot: []int{}, + wantLines: [2]int{2, 3}, + }, + { + 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. + +Phasellus lacus. Nam euismod tellus id erat. 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.`), + wantBuffer: &Buffer{ + firstLine: 0, + lastLine: 20, + headLen: 1, + footLen: 0, + 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."), + Break: []byte("\n"), + }, + {Number: 15, Content: []byte(""), Break: []byte("\n")}, + { + Number: 16, + Content: []byte("Phasellus lacus. Nam euismod tellus id " + + "erat. Pellentesque condimentum, magna ut"), + Break: []byte("\n"), + }, + { + Number: 17, + Content: []byte("suscipit hendrerit, ipsum augue ornare " + + "nulla, non luctus diam neque sit amet"), + Break: []byte("\n"), + }, + { + Number: 18, + Content: []byte("urna. Curabitur vulputate vestibulum " + + "lorem. Fusce sagittis, libero non molestie"), + Break: []byte("\n"), + }, + { + Number: 19, + Content: []byte("mollis, magna orci ultrices dolor, at " + + "vulputate neque nulla lacinia eros. Sed id"), + Break: []byte("\n"), + }, + { + Number: 20, + Content: []byte("ligula quis est convallis tempor. " + + "Curabitur lacinia pulvinar nibh. Nam a"), + Break: []byte("\n"), + }, + { + Number: 21, + Content: []byte("sapien."), + Break: []byte{}, + }, + }, + }, + wantHead: []int{0}, + wantBody: []int{ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + }, + wantFoot: []int{}, + wantLines: [2]int{0, 21}, + }, +} + +func TestNewBuffer(t *testing.T) { + for _, tt := range bufferTestCases { + t.Run(tt.name, func(t *testing.T) { + got := NewBuffer(tt.bytes) + + assert.Equal(t, tt.wantBuffer, got) + }) + } +} + +func BenchmarkNewBuffer(b *testing.B) { + for _, tt := range bufferTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = NewBuffer(tt.bytes) + } + }) + } +} + +func TestBuffer_Head(t *testing.T) { + for _, tt := range bufferTestCases { + t.Run(tt.name, func(t *testing.T) { + want := Lines{} + for _, i := range tt.wantHead { + want = append(want, tt.wantBuffer.lines[i]) + } + + got := tt.wantBuffer.Head() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkBuffer_Head(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.Head() + } + }) + } +} + +func TestBuffer_Body(t *testing.T) { + for _, tt := range bufferTestCases { + t.Run(tt.name, func(t *testing.T) { + want := Lines{} + for _, i := range tt.wantBody { + want = append(want, tt.wantBuffer.lines[i]) + } + + got := tt.wantBuffer.Body() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkBuffer_Body(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.Body() + } + }) + } +} + +func TestBuffer_Foot(t *testing.T) { + for _, tt := range bufferTestCases { + t.Run(tt.name, func(t *testing.T) { + want := Lines{} + for _, i := range tt.wantFoot { + want = append(want, tt.wantBuffer.lines[i]) + } + + got := tt.wantBuffer.Foot() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkBuffer_Foot(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.Foot() + } + }) + } +} + +func TestBuffer_Lines(t *testing.T) { + for _, tt := range bufferTestCases { + t.Run(tt.name, func(t *testing.T) { + start := tt.wantLines[0] + end := tt.wantLines[0] + tt.wantLines[1] + want := tt.wantBuffer.lines[start:end] + + got := tt.wantBuffer.Lines() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkBuffer_Lines(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.Lines() + } + }) + } +} + +func TestBuffer_Bytes(t *testing.T) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + start := tt.wantLines[0] + end := tt.wantLines[0] + tt.wantLines[1] + want := tt.wantBuffer.lines[start:end].Bytes() + + got := tt.wantBuffer.Bytes() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkMessage_Bytes(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.Bytes() + } + }) + } +} + +func TestBuffer_String(t *testing.T) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + start := tt.wantLines[0] + end := tt.wantLines[0] + tt.wantLines[1] + want := tt.wantBuffer.lines[start:end].String() + + got := tt.wantBuffer.String() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkMessage_String(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.String() + } + }) + } +} + +func TestBuffer_BytesRaw(t *testing.T) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + got := tt.wantBuffer.BytesRaw() + + assert.Equal(t, tt.bytes, got) + }) + } +} + +func BenchmarkBuffer_BytesRaw(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.BytesRaw() + } + }) + } +} + +func TestBuffer_StringRaw(t *testing.T) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + t.Run(tt.name, func(t *testing.T) { + got := tt.wantBuffer.StringRaw() + + assert.Equal(t, string(tt.bytes), got) + }) + } +} + +func BenchmarkBuffer_StringRaw(b *testing.B) { + for _, tt := range bufferTestCases { + if tt.bytes == nil { + continue + } + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.wantBuffer.StringRaw() + } + }) + } +} diff --git a/line.go b/line.go index f492401..4bfbd39 100644 --- a/line.go +++ b/line.go @@ -1,5 +1,10 @@ package conventionalcommit +import ( + "bytes" + "strings" +) + const ( lf = 10 // linefeed ("\n") character cr = 13 // carriage return ("\r") character @@ -20,6 +25,17 @@ type Line struct { Break []byte } +// Empty returns true if line content has a length of zero. +func (s *Line) Empty() bool { + return len(s.Content) == 0 +} + +// Blank returns true if line content has a length of zero after leading and +// trailing white space has been trimmed. +func (s *Line) Blank() bool { + return len(bytes.TrimSpace(s.Content)) == 0 +} + // Lines is a slice of *Line types with some helper methods attached. type Lines []*Line @@ -28,8 +44,9 @@ type Lines []*Line // basis. func NewLines(content []byte) Lines { r := Lines{} + cLen := len(content) - if len(content) == 0 { + if cLen == 0 { return r } @@ -37,12 +54,13 @@ func NewLines(content []byte) Lines { var breaks [][]int // Locate each line break within content. - for i := 0; i < len(content); i++ { - if content[i] == lf { + for i := 0; i < cLen; i++ { + switch content[i] { + case lf: breaks = append(breaks, []int{i, i + 1}) - } else if content[i] == cr { + case cr: b := []int{i, i + 1} - if i+1 < len(content) && content[i+1] == lf { + if i+1 < cLen && content[i+1] == lf { b[1]++ i++ } @@ -76,6 +94,45 @@ func NewLines(content []byte) Lines { return r } +// FirstTextIndex returns the line offset of the first line which contains any +// non-whitespace characters. +func (s Lines) FirstTextIndex() int { + for i, line := range s { + if !line.Blank() { + return i + } + } + + return -1 +} + +// LastTextIndex returns the line offset of the last line which contains any +// non-whitespace characters. +func (s Lines) LastTextIndex() int { + for i := len(s) - 1; i >= 0; i-- { + if !s[i].Blank() { + return i + } + } + + return -1 +} + +// Trim returns a new Lines instance where all leading and trailing whitespace +// lines have been removed, based on index values from FirstTextIndex() and +// LastTextIndex(). +// +// If there are no lines with non-whitespace characters, a empty Lines type is +// returned. +func (s Lines) Trim() Lines { + start := s.FirstTextIndex() + if start == -1 { + return Lines{} + } + + return s[start : s.LastTextIndex()+1] +} + // Bytes combines all Lines into a single byte slice, retaining the original // line break types for each line. func (s Lines) Bytes() []byte { @@ -100,3 +157,12 @@ func (s Lines) Bytes() []byte { func (s Lines) String() string { return string(s.Bytes()) } + +func (s Lines) Join(sep string) string { + r := make([]string, 0, len(s)) + for _, line := range s { + r = append(r, string(line.Content)) + } + + return strings.Join(r, sep) +} diff --git a/line_test.go b/line_test.go index 4d39057..5ea956f 100644 --- a/line_test.go +++ b/line_test.go @@ -6,6 +6,156 @@ import ( "github.com/stretchr/testify/assert" ) +func TestLine_Empty(t *testing.T) { + tests := []struct { + name string + line *Line + want bool + }{ + { + name: "nil", + line: &Line{}, + want: true, + }, + { + name: "empty", + line: &Line{ + Number: 1, + Content: []byte(""), + Break: []byte{}, + }, + want: true, + }, + { + name: "space only", + line: &Line{ + Number: 1, + Content: []byte(" "), + Break: []byte{}, + }, + want: false, + }, + { + name: "tab only", + line: &Line{ + Number: 1, + Content: []byte("\t\t"), + Break: []byte{}, + }, + want: false, + }, + { + name: "spaces and tabs", + line: &Line{ + Number: 1, + Content: []byte(" \t "), + Break: []byte{}, + }, + want: false, + }, + { + name: "text", + line: &Line{ + Number: 1, + Content: []byte("foobar"), + Break: []byte{}, + }, + want: false, + }, + { + name: "text with surrounding white space", + line: &Line{ + Number: 1, + Content: []byte(" foobar "), + Break: []byte{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.line.Empty() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestLine_Blank(t *testing.T) { + tests := []struct { + name string + line *Line + want bool + }{ + { + name: "nil", + line: &Line{}, + want: true, + }, + { + name: "empty", + line: &Line{ + Number: 1, + Content: []byte(""), + Break: []byte{}, + }, + want: true, + }, + { + name: "space only", + line: &Line{ + Number: 1, + Content: []byte(" "), + Break: []byte{}, + }, + want: true, + }, + { + name: "tab only", + line: &Line{ + Number: 1, + Content: []byte("\t\t"), + Break: []byte{}, + }, + want: true, + }, + { + name: "spaces and tabs", + line: &Line{ + Number: 1, + Content: []byte(" \t "), + Break: []byte{}, + }, + want: true, + }, + { + name: "text", + line: &Line{ + Number: 1, + Content: []byte("foobar"), + Break: []byte{}, + }, + want: false, + }, + { + name: "text with surrounding white space", + line: &Line{ + Number: 1, + Content: []byte(" foobar "), + Break: []byte{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.line.Blank() + + assert.Equal(t, tt.want, got) + }) + } +} + func TestNewLines(t *testing.T) { tests := []struct { name string @@ -290,11 +440,67 @@ func TestNewLines(t *testing.T) { } } -var linesBytesTestCases = []struct { - name string - lines Lines - want []byte +var linesTestCases = []struct { + name string + lines Lines + bytes []byte + firstTextIndex int + lastTextIndex int }{ + { + name: "no lines", + lines: Lines{}, + bytes: []byte(""), + firstTextIndex: -1, + lastTextIndex: -1, + }, + { + name: "empty line", + lines: Lines{ + { + Number: 1, + Content: []byte(""), + }, + }, + bytes: []byte(""), + firstTextIndex: -1, + lastTextIndex: -1, + }, + { + name: "whitespace line", + lines: Lines{ + { + Number: 1, + Content: []byte(" "), + }, + }, + bytes: []byte(" "), + firstTextIndex: -1, + lastTextIndex: -1, + }, + { + name: "multiple whitespace lines", + lines: Lines{ + { + Number: 1, + Content: []byte(" "), + Break: []byte("\n"), + }, + { + Number: 2, + Content: []byte("\t"), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte(" "), + Break: []byte{}, + }, + }, + bytes: []byte(" \n\t\n "), + firstTextIndex: -1, + lastTextIndex: -1, + }, { name: "single line", lines: Lines{ @@ -303,7 +509,9 @@ var linesBytesTestCases = []struct { Content: []byte("hello world"), }, }, - want: []byte("hello world"), + bytes: []byte("hello world"), + firstTextIndex: 0, + lastTextIndex: 0, }, { name: "single line with trailing LF", @@ -319,7 +527,9 @@ var linesBytesTestCases = []struct { Break: []byte{}, }, }, - want: []byte("hello world\n"), + bytes: []byte("hello world\n"), + firstTextIndex: 0, + lastTextIndex: 0, }, { name: "single line with trailing CRLF", @@ -335,7 +545,9 @@ var linesBytesTestCases = []struct { Break: []byte{}, }, }, - want: []byte("hello world\r\n"), + bytes: []byte("hello world\r\n"), + firstTextIndex: 0, + lastTextIndex: 0, }, { name: "single line with trailing CR", @@ -351,41 +563,53 @@ var linesBytesTestCases = []struct { Break: []byte{}, }, }, - want: []byte("hello world\r"), + bytes: []byte("hello world\r"), + firstTextIndex: 0, + lastTextIndex: 0, }, { 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, + Number: 1, Content: []byte(""), Break: []byte("\n"), }, { - Number: 7, + Number: 2, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\n"), + }, + { + Number: 3, + Content: []byte("Sed bibendum."), + Break: []byte("\n"), + }, + { + Number: 4, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\n"), + }, + { + Number: 5, + Content: []byte(""), + Break: []byte("\n"), + }, + { + Number: 6, Content: []byte("Integer placerat tristique nisl."), Break: []byte("\n"), }, { - Number: 8, + Number: 7, Content: []byte("Etiam vel neque nec dui bibendum."), Break: []byte("\n"), }, + { + Number: 8, + Content: []byte(""), + Break: []byte("\n"), + }, { Number: 9, Content: []byte(""), @@ -393,22 +617,18 @@ var linesBytesTestCases = []struct { }, { Number: 10, - Content: []byte(""), - Break: []byte("\n"), - }, - { - Number: 11, Content: []byte("Nullam libero mauris, dictum id, arcu."), Break: []byte("\n"), }, { - Number: 12, + Number: 11, Content: []byte(""), Break: []byte{}, }, }, - want: []byte( - "Aliquam feugiat tellus ut neque.\n" + + bytes: []byte( + "\n" + + "Aliquam feugiat tellus ut neque.\n" + "Sed bibendum.\n" + "Nullam libero mauris, consequat.\n" + "\n" + @@ -418,40 +638,52 @@ var linesBytesTestCases = []struct { "\n" + "Nullam libero mauris, dictum id, arcu.\n", ), + firstTextIndex: 1, + lastTextIndex: 9, }, { 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, + Number: 1, Content: []byte(""), Break: []byte("\r\n"), }, { - Number: 7, + Number: 2, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r\n"), + }, + { + Number: 3, + Content: []byte("Sed bibendum."), + Break: []byte("\r\n"), + }, + { + Number: 4, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\r\n"), + }, + { + Number: 5, + Content: []byte(""), + Break: []byte("\r\n"), + }, + { + Number: 6, Content: []byte("Integer placerat tristique nisl."), Break: []byte("\r\n"), }, { - Number: 8, + Number: 7, Content: []byte("Etiam vel neque nec dui bibendum."), Break: []byte("\r\n"), }, + { + Number: 8, + Content: []byte(""), + Break: []byte("\r\n"), + }, { Number: 9, Content: []byte(""), @@ -459,22 +691,18 @@ var linesBytesTestCases = []struct { }, { Number: 10, - Content: []byte(""), - Break: []byte("\r\n"), - }, - { - Number: 11, Content: []byte("Nullam libero mauris, dictum id, arcu."), Break: []byte("\r\n"), }, { - Number: 12, + Number: 11, Content: []byte(""), Break: []byte{}, }, }, - want: []byte( - "Aliquam feugiat tellus ut neque.\r\n" + + bytes: []byte( + "\r\n" + + "Aliquam feugiat tellus ut neque.\r\n" + "Sed bibendum.\r\n" + "Nullam libero mauris, consequat.\r\n" + "\r\n" + @@ -484,40 +712,52 @@ var linesBytesTestCases = []struct { "\r\n" + "Nullam libero mauris, dictum id, arcu.\r\n", ), + firstTextIndex: 1, + lastTextIndex: 9, }, { 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, + Number: 1, Content: []byte(""), Break: []byte("\r"), }, { - Number: 7, + Number: 2, + Content: []byte("Aliquam feugiat tellus ut neque."), + Break: []byte("\r"), + }, + { + Number: 3, + Content: []byte("Sed bibendum."), + Break: []byte("\r"), + }, + { + Number: 4, + Content: []byte("Nullam libero mauris, consequat."), + Break: []byte("\r"), + }, + { + Number: 5, + Content: []byte(""), + Break: []byte("\r"), + }, + { + Number: 6, Content: []byte("Integer placerat tristique nisl."), Break: []byte("\r"), }, { - Number: 8, + Number: 7, Content: []byte("Etiam vel neque nec dui bibendum."), Break: []byte("\r"), }, + { + Number: 8, + Content: []byte(""), + Break: []byte("\r"), + }, { Number: 9, Content: []byte(""), @@ -525,22 +765,18 @@ var linesBytesTestCases = []struct { }, { Number: 10, - Content: []byte(""), - Break: []byte("\r"), - }, - { - Number: 11, Content: []byte("Nullam libero mauris, dictum id, arcu."), Break: []byte("\r"), }, { - Number: 12, + Number: 11, Content: []byte(""), Break: []byte{}, }, }, - want: []byte( - "Aliquam feugiat tellus ut neque.\r" + + bytes: []byte( + "\r" + + "Aliquam feugiat tellus ut neque.\r" + "Sed bibendum.\r" + "Nullam libero mauris, consequat.\r" + "\r" + @@ -550,21 +786,88 @@ var linesBytesTestCases = []struct { "\r" + "Nullam libero mauris, dictum id, arcu.\r", ), + firstTextIndex: 1, + lastTextIndex: 9, }, } +func TestLines_FirstTextIndex(t *testing.T) { + for _, tt := range linesTestCases { + t.Run(tt.name, func(t *testing.T) { + got := tt.lines.FirstTextIndex() + + assert.Equal(t, tt.firstTextIndex, got) + }) + } +} + +func BenchmarkLines_FirstTextIndex(b *testing.B) { + for _, tt := range linesTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.lines.FirstTextIndex() + } + }) + } +} + +func TestLines_LastTextIndex(t *testing.T) { + for _, tt := range linesTestCases { + t.Run(tt.name, func(t *testing.T) { + got := tt.lines.LastTextIndex() + + assert.Equal(t, tt.lastTextIndex, got) + }) + } +} + +func BenchmarkLines_LastTextIndex(b *testing.B) { + for _, tt := range linesTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.lines.LastTextIndex() + } + }) + } +} + +func TestLines_Trim(t *testing.T) { + for _, tt := range linesTestCases { + t.Run(tt.name, func(t *testing.T) { + want := Lines{} + if tt.firstTextIndex != -1 { + want = tt.lines[tt.firstTextIndex : tt.lastTextIndex+1] + } + + got := tt.lines.Trim() + + assert.Equal(t, want, got) + }) + } +} + +func BenchmarkLines_Trim(b *testing.B) { + for _, tt := range linesTestCases { + b.Run(tt.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = tt.lines.Trim() + } + }) + } +} + func TestLines_Bytes(t *testing.T) { - for _, tt := range linesBytesTestCases { + for _, tt := range linesTestCases { t.Run(tt.name, func(t *testing.T) { got := tt.lines.Bytes() - assert.Equal(t, tt.want, got) + assert.Equal(t, tt.bytes, got) }) } } func BenchmarkLines_Bytes(b *testing.B) { - for _, tt := range linesBytesTestCases { + for _, tt := range linesTestCases { b.Run(tt.name, func(b *testing.B) { for n := 0; n < b.N; n++ { _ = tt.lines.Bytes() @@ -574,17 +877,17 @@ func BenchmarkLines_Bytes(b *testing.B) { } func TestLines_String(t *testing.T) { - for _, tt := range linesBytesTestCases { + for _, tt := range linesTestCases { t.Run(tt.name, func(t *testing.T) { got := tt.lines.String() - assert.Equal(t, string(tt.want), got) + assert.Equal(t, string(tt.bytes), got) }) } } func BenchmarkLines_String(b *testing.B) { - for _, tt := range linesBytesTestCases { + for _, tt := range linesTestCases { 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 deleted file mode 100644 index deaad3c..0000000 --- a/paragraph.go +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index db7fc52..0000000 --- a/paragraph_test.go +++ /dev/null @@ -1,338 +0,0 @@ -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 deleted file mode 100644 index 01c0f8b..0000000 --- a/raw_message.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index f7653a6..0000000 --- a/raw_message_test.go +++ /dev/null @@ -1,641 +0,0 @@ -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() - } - }) - } -}