Commit 266b02ac authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'job-log-support-mac-line-break' into 'master'

Support \r as line replacement in job logs

See merge request gitlab-org/gitlab!18933
parents b862289e fb9cf9cb
---
title: Let ANSI \r code replace the current job log line
merge_request: 18933
author:
type: fixed
...@@ -22,11 +22,11 @@ module Gitlab ...@@ -22,11 +22,11 @@ module Gitlab
start_offset = @state.offset start_offset = @state.offset
@state.set_current_line!(style: Style.new(@state.inherited_style)) @state.new_line!(
style: Style.new(@state.inherited_style))
stream.each_line do |line| stream.each_line do |line|
s = StringScanner.new(line) consume_line(line)
convert_line(s)
end end
# This must be assigned before flushing the current line # This must be assigned before flushing the current line
...@@ -52,26 +52,43 @@ module Gitlab ...@@ -52,26 +52,43 @@ module Gitlab
private private
def convert_line(scanner) def consume_line(line)
until scanner.eos? scanner = StringScanner.new(line)
if scanner.scan(Gitlab::Regex.build_trace_section_regex) consume_token(scanner) until scanner.eos?
handle_section(scanner) end
elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(scanner) def consume_token(scanner)
elsif scanner.scan(/\e(([@-_])(.*?)?)?$/) if scan_token(scanner, Gitlab::Regex.build_trace_section_regex, consume: false)
break handle_section(scanner)
elsif scanner.scan(/</) elsif scan_token(scanner, /\e([@-_])(.*?)([@-~])/)
@state.current_line << '&lt;' handle_sequence(scanner)
elsif scanner.scan(/\r?\n/) elsif scan_token(scanner, /\e(([@-_])(.*?)?)?$/)
# we advance the offset of the next current line # stop scanning
# so it does not start from \n scanner.terminate
flush_current_line(advance_offset: scanner.matched_size) elsif scan_token(scanner, /</)
else @state.current_line << '&lt;'
@state.current_line << scanner.scan(/./m) elsif scan_token(scanner, /\r?\n/)
end flush_current_line
elsif scan_token(scanner, /\r/)
@state.offset += scanner.matched_size # drop last line
@state.current_line.clear!
elsif scan_token(scanner, /.[^\e<\r\ns]*/m)
# this is a join from all previous tokens and first letters
# it always matches at least one character `.`
# it matches everything that is not start of:
# `\e`, `<`, `\r`, `\n`, `s` (for section_start)
@state.current_line << scanner[0]
else
raise 'invalid parser state'
end
end
def scan_token(scanner, match, consume: true)
scanner.scan(match).tap do |result|
# we need to move offset as soon
# as we match the token
@state.offset += scanner.matched_size if consume && result
end end
end end
...@@ -96,32 +113,50 @@ module Gitlab ...@@ -96,32 +113,50 @@ module Gitlab
section_name = sanitize_section_name(section) section_name = sanitize_section_name(section)
if action == "start" if action == "start"
handle_section_start(section_name, timestamp) handle_section_start(scanner, section_name, timestamp)
elsif action == "end" elsif action == "end"
handle_section_end(section_name, timestamp) handle_section_end(scanner, section_name, timestamp)
else
raise 'unsupported action'
end end
end end
def handle_section_start(section, timestamp) def handle_section_start(scanner, section, timestamp)
flush_current_line unless @state.current_line.empty? # We make a new line for new section
flush_current_line
@state.open_section(section, timestamp) @state.open_section(section, timestamp)
# we need to consume match after handling
# the open of section, as we want the section
# marker to be refresh on incremental update
@state.offset += scanner.matched_size
end end
def handle_section_end(section, timestamp) def handle_section_end(scanner, section, timestamp)
return unless @state.section_open?(section) return unless @state.section_open?(section)
flush_current_line unless @state.current_line.empty? # We flush the content to make the end
# of section to be a new line
flush_current_line
@state.close_section(section, timestamp) @state.close_section(section, timestamp)
# ensure that section end is detached from the last # we need to consume match before handling
# line in the section # as we want the section close marker
# not to be refreshed on incremental update
@state.offset += scanner.matched_size
# this flushes an empty line with `section_duration`
flush_current_line flush_current_line
end end
def flush_current_line(advance_offset: 0) def flush_current_line
@lines << @state.current_line.to_h unless @state.current_line.empty?
@lines << @state.current_line.to_h
end
@state.set_current_line!(advance_offset: advance_offset) @state.new_line!
end end
def sanitize_section_name(section) def sanitize_section_name(section)
......
...@@ -47,12 +47,17 @@ module Gitlab ...@@ -47,12 +47,17 @@ module Gitlab
@current_segment.text << data @current_segment.text << data
end end
def clear!
@segments.clear
@current_segment = Segment.new(style: style)
end
def style def style
@current_segment.style @current_segment.style
end end
def empty? def empty?
@segments.empty? && @current_segment.empty? @segments.empty? && @current_segment.empty? && @section_duration.nil?
end end
def update_style(ansi_commands) def update_style(ansi_commands)
......
...@@ -46,9 +46,9 @@ module Gitlab ...@@ -46,9 +46,9 @@ module Gitlab
@open_sections.key?(section) @open_sections.key?(section)
end end
def set_current_line!(style: nil, advance_offset: 0) def new_line!(style: nil)
new_line = Line.new( new_line = Line.new(
offset: @offset + advance_offset, offset: @offset,
style: style || @current_line.style, style: style || @current_line.style,
sections: @open_sections.keys sections: @open_sections.keys
) )
......
...@@ -12,11 +12,26 @@ describe Gitlab::Ci::Ansi2json do ...@@ -12,11 +12,26 @@ describe Gitlab::Ci::Ansi2json do
]) ])
end end
it 'adds new line in a separate element' do context 'new lines' do
expect(convert_json("Hello\nworld")).to eq([ it 'adds new line when encountering \n' do
{ offset: 0, content: [{ text: 'Hello' }] }, expect(convert_json("Hello\nworld")).to eq([
{ offset: 6, content: [{ text: 'world' }] } { offset: 0, content: [{ text: 'Hello' }] },
]) { offset: 6, content: [{ text: 'world' }] }
])
end
it 'adds new line when encountering \r\n' do
expect(convert_json("Hello\r\nworld")).to eq([
{ offset: 0, content: [{ text: 'Hello' }] },
{ offset: 7, content: [{ text: 'world' }] }
])
end
it 'replace the current line when encountering \r' do
expect(convert_json("Hello\rworld")).to eq([
{ offset: 0, content: [{ text: 'world' }] }
])
end
end end
it 'recognizes color changing ANSI sequences' do it 'recognizes color changing ANSI sequences' do
...@@ -113,10 +128,6 @@ describe Gitlab::Ci::Ansi2json do ...@@ -113,10 +128,6 @@ describe Gitlab::Ci::Ansi2json do
content: [], content: [],
section_duration: '01:03', section_duration: '01:03',
section: 'prepare-script' section: 'prepare-script'
},
{
offset: 63,
content: []
} }
]) ])
end end
...@@ -134,10 +145,6 @@ describe Gitlab::Ci::Ansi2json do ...@@ -134,10 +145,6 @@ describe Gitlab::Ci::Ansi2json do
content: [], content: [],
section: 'prepare-script', section: 'prepare-script',
section_duration: '01:03' section_duration: '01:03'
},
{
offset: 56,
content: []
} }
]) ])
end end
...@@ -157,7 +164,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -157,7 +164,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '01:03' section_duration: '01:03'
}, },
{ {
offset: 49, offset: 91,
content: [{ text: 'world' }] content: [{ text: 'world' }]
} }
]) ])
...@@ -198,7 +205,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -198,7 +205,7 @@ describe Gitlab::Ci::Ansi2json do
expect(convert_json("#{section_start}hello")).to eq([ expect(convert_json("#{section_start}hello")).to eq([
{ {
offset: 0, offset: 0,
content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }] content: [{ text: 'hello' }]
} }
]) ])
end end
...@@ -211,7 +218,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -211,7 +218,7 @@ describe Gitlab::Ci::Ansi2json do
expect(convert_json("#{section_start}hello")).to eq([ expect(convert_json("#{section_start}hello")).to eq([
{ {
offset: 0, offset: 0,
content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }] content: [{ text: 'hello' }]
} }
]) ])
end end
...@@ -231,10 +238,6 @@ describe Gitlab::Ci::Ansi2json do ...@@ -231,10 +238,6 @@ describe Gitlab::Ci::Ansi2json do
content: [], content: [],
section: 'prepare-script', section: 'prepare-script',
section_duration: '01:03' section_duration: '01:03'
},
{
offset: 95,
content: []
} }
]) ])
end end
...@@ -274,7 +277,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -274,7 +277,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '00:02' section_duration: '00:02'
}, },
{ {
offset: 106, offset: 155,
content: [{ text: 'baz' }], content: [{ text: 'baz' }],
section: 'prepare-script' section: 'prepare-script'
}, },
...@@ -285,7 +288,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -285,7 +288,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '01:03' section_duration: '01:03'
}, },
{ {
offset: 158, offset: 200,
content: [{ text: 'world' }] content: [{ text: 'world' }]
} }
]) ])
...@@ -318,14 +321,10 @@ describe Gitlab::Ci::Ansi2json do ...@@ -318,14 +321,10 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '00:02' section_duration: '00:02'
}, },
{ {
offset: 115, offset: 164,
content: [], content: [],
section: 'prepare-script', section: 'prepare-script',
section_duration: '01:03' section_duration: '01:03'
},
{
offset: 164,
content: []
} }
]) ])
end end
...@@ -380,7 +379,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -380,7 +379,7 @@ describe Gitlab::Ci::Ansi2json do
] ]
end end
it 'returns the full line' do it 'returns the line since last partially processed line' do
expect(pass2.lines).to eq(lines) expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_truthy expect(pass2.append).to be_truthy
end end
...@@ -399,7 +398,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -399,7 +398,7 @@ describe Gitlab::Ci::Ansi2json do
] ]
end end
it 'returns the full line' do it 'returns the line since last partially processed line' do
expect(pass2.lines).to eq(lines) expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_falsey expect(pass2.append).to be_falsey
end end
...@@ -416,7 +415,7 @@ describe Gitlab::Ci::Ansi2json do ...@@ -416,7 +415,7 @@ describe Gitlab::Ci::Ansi2json do
] ]
end end
it 'returns the full line' do it 'returns a blank line and the next line' do
expect(pass2.lines).to eq(lines) expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_falsey expect(pass2.append).to be_falsey
end end
...@@ -502,10 +501,6 @@ describe Gitlab::Ci::Ansi2json do ...@@ -502,10 +501,6 @@ describe Gitlab::Ci::Ansi2json do
content: [], content: [],
section: 'prepare-script', section: 'prepare-script',
section_duration: '01:03' section_duration: '01:03'
},
{
offset: 77,
content: []
} }
] ]
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment