Commit fb9cf9cb authored by Fabio Pitino's avatar Fabio Pitino Committed by Douglas Barbosa Alexandre

Support \r as line break for job logs

This bugfix is usefult to translate terminal behavior
where \r would refresh the current line or for Mac's
line break.
parent b862289e
---
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