[\/\w]*.rvm\/rubies\/[\S]*?\/(ruby|rbx|jruby) .*?<\/p>)/g
+# TODO: revisit those patterns
+FOLDS = [
+ Em.Object.create(name: 'schema', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:schema:load/, endPattern: /^\$/)
+ Em.Object.create(name: 'migrate', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:migrate/, endPattern: /^\$/)
+ Em.Object.create(name: 'bundle', startPattern: /^\$ bundle install/, endPattern: /^\$/)
+]
- filter: (log, path) ->
- log = @escape(log)
- log = @deansi(log)
- log = log.replace(/\r/g, '')
- log = @number(log, path)
- log = @fold(log)
- log = log.replace(/\n/g, '')
- log
+@Travis.Log = Em.Object.extend
+ init: ->
+ @set 'folds', []
+ @set 'line', 1
- stripPaths: (log) ->
- log.replace /\/home\/vagrant\/builds(\/[^\/\n]+){2}\//g, ''
+ for fold in FOLDS
+ @addFold fold
+
+ append: (lines) ->
+ log = @join lines
+ log = @escape log
+ log = @deansi log
+ lines = @split log
+
+ target = @get 'target'
+ index = 0
+ currentFold = @currentFold
+
+ @set 'lineNumber', 1 unless @get 'lineNumber'
+
+ result = []
+
+ for line in lines
+ if line == '\r'
+ @set 'replace', true
+ else if line == '\n'
+ @set 'newline', true
+ index += 1
+ else
+ if currentFold && ( @isFoldEnding(currentFold, line) )
+ # end of the fold, send fold to target
+ if result.length > 0
+ result.slice(-1)[0].foldEnd = true
+ target.appendLog result
+
+ @currentFold = currentFold = null
+ @set 'foldContinuation', false
+ result = []
+
+ if !currentFold && ( currentFold = @foldByStart(line) )
+ # beginning new fold, send current lines to target
+ if result.length > 0
+ target.appendLog result
+
+ result = []
+ start = index
+
+ payload = { content: line }
+
+ if currentFold
+ payload.fold = currentFold.get('name')
+
+ if @get 'foldContinuation'
+ payload.foldContinuation = true
+
+ payload.number = @get('lineNumber') + index
+
+ if @get 'replace'
+ @set 'replace', false
+ payload.replace = true
+ else if @get 'newline'
+ @set 'newline', false
+ else if payload.number != 1
+ payload.append = true
+
+ result.pushObject payload
+
+ if currentFold
+ @set 'foldContinuation', true
+
+ if result.length > 0
+ if currentFold
+ @currentFold = currentFold
+
+ target.appendLog result
+
+ nextLineNumber = @get('lineNumber') + index
+ @set 'lineNumber', nextLineNumber
+
+ join: (lines) ->
+ if typeof lines == 'string'
+ lines
+ else
+ lines.toArray().join ''
+
+ split: (log) ->
+ log = log.replace /\r\n/g, '\n'
+ lines = log.split(/(\n)/)
+
+ result = []
+ for line in lines
+ result.pushObjects line.split(/(\r)/)
+
+ result
escape: (log) ->
Handlebars.Utils.escapeExpression log
- escapeRuby: (log) ->
- log.replace /#<(\w+.*?)>/, '#<$1>'
-
- number: (log, path) ->
- path = "#{path}/"
- result = ''
- $.each log.trim().split('\n'), (ix, line) ->
- number = ix + 1
- pathWithNumber = "#{path}#L#{number}"
- result += '%@%@
\n'.fmt(pathWithNumber, number, number, number, line)
- result.trim()
-
deansi: (log) ->
log = log.replace(/\r\r/g, '\r')
.replace(/\033\[K\r/g, '\r')
- .replace(/^.*\r(?!$)/gm, '')
.replace(/\[2K/g, '')
.replace(/\033\(B/g, '')
.replace(/\033\[\d+G/, '')
@@ -52,17 +122,11 @@
text += (if classes.length then ('' + part.text + '') else part.text)
text.replace /\033/g, ''
- fold: (log) ->
- log = @unfold(log)
- $.each Travis.Log.FOLDS, (name, pattern) ->
- log = log.replace(pattern, ->
- '' + arguments[1].trim() + '
'
- )
- log
+ addFold: (fold) ->
+ @get('folds').pushObject fold
- unfold: (log) ->
- log.replace /([\s\S]*?)<\/div>/g, '$1\n'
-
- location: ->
- window.location.hash
+ foldByStart: (line) ->
+ @get('folds').find (fold) -> line.match(fold.get('startPattern'))
+ isFoldEnding: (fold, line) ->
+ line.match(fold.get('endPattern'))
diff --git a/assets/scripts/spec/unit/artifact_spec.coffee b/assets/scripts/spec/unit/artifact_spec.coffee
new file mode 100644
index 00000000..4458152e
--- /dev/null
+++ b/assets/scripts/spec/unit/artifact_spec.coffee
@@ -0,0 +1,36 @@
+store = null
+record = null
+
+describe 'Travis.Artifact', ->
+ beforeEach ->
+ store = Travis.Store.create()
+
+ afterEach ->
+ store.destroy()
+
+ describe 'with part of the body loaded', ->
+ beforeEach =>
+ store.load Travis.Artifact, 1, { id: 1, body: 'first\nsecond\n' }
+ record = store.find(Travis.Artifact, 1)
+
+ it 'packs the existing part of the body to parts', ->
+ expect( record.get('parts').toArray() ).toEqual( ['first\nsecond\n'] )
+
+ it 'adds new chunks of log to parts', ->
+ record.append('third\n')
+ expect( record.get('parts').toArray() ).toEqual( ['first\nsecond\n', 'third\n'] )
+
+ it 'properly handles array observers', ->
+ called = 0
+ observer = {
+ arrayDidChange: -> called += 1
+ arrayWillChange: -> called += 1
+ }
+
+ record.get('parts').addArrayObserver observer,
+ willChange: 'arrayWillChange'
+ didChange: 'arrayDidChange'
+
+ record.append('something')
+
+ expect(called).toEqual 2
diff --git a/assets/scripts/spec/unit/log_spec.coffee b/assets/scripts/spec/unit/log_spec.coffee
new file mode 100644
index 00000000..4c0d1ce9
--- /dev/null
+++ b/assets/scripts/spec/unit/log_spec.coffee
@@ -0,0 +1,232 @@
+log = null
+target = null
+
+describe 'Travis.Log', ->
+ beforeEach ->
+ target = Em.Object.create
+ calls: []
+ appendLog: (payloads) ->
+ lines = payloads.map (p) ->
+ line = p.content
+ delete p.content
+ line
+
+ @get('calls').pushObject
+ options: payloads
+ lines: lines
+
+ log = Travis.Log.create(target: target)
+
+ it 'works with log passed as a string', ->
+ log.append '1\n2'
+
+ expect( target.get('calls.firstObject.lines') ).toEqual ['1', '2']
+
+
+ it 'splits lines', ->
+ log.append ['1\r\n2\n\n', '3']
+
+ expect( target.get('calls.length') ).toEqual 1
+ expect( target.get('calls.firstObject.lines') ).toEqual ['1', '2', '', '3']
+
+ it 'escapes html characters', ->
+ log.append '<>'
+
+ expect( target.get('calls.firstObject.lines') ).toEqual ['<>']
+
+ it 'normalizes ansi mess', ->
+ log.append ['foo\r\r', 'bar']
+
+ expect( target.get('calls.firstObject.lines') ).toEqual [ 'foo', 'bar' ]
+
+ it 'calls target with folds separation', ->
+ fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
+ log.addFold fold
+
+ fold = Em.Object.create name: 'qux', startPattern: /^\$ qux/, endPattern: /^\$/
+ log.addFold fold
+
+ log.append [
+ '1\n', '2\n'
+ '$ foo --foo\n', '1\n'
+ '$ bar\n'
+ '$ baz\n'
+ '$ qux\n', '1\n', '2\n'
+ '$ end\n'
+ ]
+
+ # expect( target.get('calls.length') ).toEqual 5
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['1', '2']
+ expect( options[0]).toEqual [ { number: 1 }, { number: 2 } ]
+
+ expect( lines[1] ).toEqual ['$ foo --foo', '1']
+ expect( options[1]).toEqual [
+ { number: 3, fold: 'foo' },
+ { number: 4, fold: 'foo', foldContinuation: true, foldEnd: true }]
+
+ expect( lines[2] ).toEqual ['$ bar', '$ baz']
+ expect( options[2]).toEqual [{ number: 5 }, { number: 6 }]
+
+ expect( lines[3] ).toEqual ['$ qux', '1', '2']
+ expect( options[3]).toEqual [
+ { number: 7, fold: 'qux' },
+ { number: 8, fold: 'qux', foldContinuation: true },
+ { number: 9, fold: 'qux', foldContinuation: true, foldEnd: true }]
+
+ expect( lines[4] ).toEqual ['$ end', '']
+ expect( options[4]).toEqual [{ number: 10 }, { number: 11 }]
+
+ it 'works properly when log is started with fold', ->
+ fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
+ log.addFold fold
+
+ log.append [
+ '$ foo --foo\n', '1\n'
+ '$ bar\n'
+ ]
+
+ expect( target.get('calls.length') ).toEqual 2
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo --foo', '1']
+ expect( options[0]).toEqual [
+ { number: 1, fold: 'foo' },
+ { number: 2, fold: 'foo', foldContinuation: true, foldEnd: true }]
+
+ expect( lines[1] ).toEqual ['$ bar', '']
+ expect( options[1]).toEqual [{ number: 3 }, { number: 4 }]
+
+ it 'works properly for 2 consecutive folds', ->
+ fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
+ log.addFold fold
+
+ log.append [
+ '$ foo --foo\n', '1\n'
+ '$ foo --bar\n', '2\n'
+ '$ bar\n'
+ ]
+
+ expect( target.get('calls.length') ).toEqual 3
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo --foo', '1']
+ expect( options[0]).toEqual [
+ { number: 1, fold: 'foo' },
+ { number: 2, fold: 'foo', foldContinuation: true, foldEnd: true }]
+
+ expect( lines[1] ).toEqual ['$ foo --bar', '2']
+ expect( options[1]).toEqual [
+ { number: 3, fold: 'foo' },
+ { number: 4, fold: 'foo', foldContinuation: true, foldEnd: true }]
+
+ expect( lines[2] ).toEqual ['$ bar', '']
+ expect( options[2]).toEqual [{ number: 5 }, { number: 6 }]
+
+ it 'works fine with not finalized fold', ->
+ fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
+ log.addFold fold
+
+ log.append [
+ '$ foo --foo\n', '1\n'
+ ]
+
+ expect( target.get('calls.length') ).toEqual 1
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo --foo', '1', '']
+ expect( options[0]).toEqual [
+ { fold: 'foo', number: 1 },
+ { fold: 'foo', number: 2, foldContinuation: true },
+ { fold: 'foo', number: 3, foldContinuation: true }]
+
+ it 'allows to continue fold', ->
+ fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
+ log.addFold fold
+
+ log.append [
+ '$ foo --foo\n', '1\n'
+ ]
+
+ log.append '2\n'
+
+ log.append [
+ '3\n'
+ '$ bar\n'
+ ]
+
+ expect( target.get('calls.length') ).toEqual 4
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo --foo', '1', '']
+ expect( options[0]).toEqual [
+ { fold: 'foo', number: 1 },
+ { fold: 'foo', number: 2, foldContinuation: true },
+ { fold: 'foo', number: 3, foldContinuation: true }]
+
+ expect( lines[1] ).toEqual ['2', '']
+ expect( options[1]).toEqual [
+ { fold: 'foo', number: 3, foldContinuation: true, append: true }
+ { fold: 'foo', number: 4, foldContinuation: true }
+ ]
+
+ expect( lines[2] ).toEqual ['3']
+ expect( options[2]).toEqual [
+ { fold: 'foo', number: 4, foldContinuation: true, append: true, foldEnd: true }
+ ]
+
+ expect( lines[3] ).toEqual ['$ bar', '']
+ expect( options[3]).toEqual [{ number: 5 }, { number: 6 }]
+
+ it 'notifies that the line should be appended', ->
+ log.append '$ foo\n.'
+
+ log.append '...'
+
+ log.append '..\n$ bar\n'
+
+ expect( target.get('calls.length') ).toEqual 3
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo', '.']
+ expect( options[0]).toEqual [{ number: 1 }, { number: 2 }]
+
+ expect( lines[1] ).toEqual ['...']
+ expect( options[1]).toEqual [{ append: true, number: 2 }]
+
+ expect( lines[2] ).toEqual ['..', '$ bar', '']
+ expect( options[2]).toEqual [{ append: true, number: 2 }, { number: 3 }, { number: 4 }]
+
+ it 'notifies that the line should be replaced', ->
+ log.append '$ foo\n'
+
+ log.append '\rDownloading 50%'
+ log.append '\rDownloading 100%\r\n'
+
+ log.append '$ bar\n'
+
+ expect( target.get('calls.length') ).toEqual 4
+ lines = target.get('calls').map (call) -> call.lines
+ options = target.get('calls').map (call) -> call.options
+
+ expect( lines[0] ).toEqual ['$ foo', '']
+ expect( options[0]).toEqual [{ number: 1 }, { number: 2 }]
+
+ expect( lines[1] ).toEqual ['', 'Downloading 50%']
+ expect( options[1]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }]
+
+ expect( lines[2] ).toEqual ['', 'Downloading 100%', '']
+ expect( options[2]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }, { number: 3 }]
+
+ expect( lines[3] ).toEqual ['$ bar', '']
+ expect( options[3]).toEqual [{ number: 3, append: true }, { number: 4 }]
+
+ it 'notifies that the line should be replaced even if carriage return is in the middle', ->
+
diff --git a/assets/scripts/spec/unit/views/log_view_spec.coffee b/assets/scripts/spec/unit/views/log_view_spec.coffee
new file mode 100644
index 00000000..fc6394a2
--- /dev/null
+++ b/assets/scripts/spec/unit/views/log_view_spec.coffee
@@ -0,0 +1,74 @@
+view = null
+store = null
+record = null
+
+describe 'Travis.LogView', ->
+ beforeEach ->
+ store = Travis.Store.create()
+
+ afterEach ->
+ store.destroy()
+ view.remove()
+ view.destroy()
+
+ it 'works fine with existing log, which is appended', ->
+ store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
+ log = Travis.Artifact.find(1)
+
+ Ember.run ->
+ view = Travis.LogView.create(context: null)
+ view.append()
+
+ expect( $('#log').length ).toEqual 1
+ console.log $('#log')
+
+ job = Ember.Object.create log: log, subscribe: (-> )
+
+ Ember.run ->
+ view.set 'context', job
+ log.set 'isLoaded', true
+
+ expect( view.$('#log p').length ).toEqual 1
+ expect( view.$('#log p').text().trim() ).toEqual '1$ start'
+
+ Ember.run ->
+ log.append('$ end')
+
+ expect( view.$('#log p').length ).toEqual 2
+ expect( view.$('#log p').text().trim() ).toEqual '1$ start2$ end'
+
+ it 'works fine with log already attahed to view', ->
+ store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
+ log = Travis.Artifact.find(1)
+ job = Ember.Object.create log: log, subscribe: (-> )
+
+ Ember.run ->
+ view = Travis.LogView.create()
+ view.set('context', job)
+ view.append()
+
+ Ember.run ->
+ log.append('end')
+
+ expect( view.$('#log p').length ).toEqual 2
+ expect( view.$('#log p').text().trim() ).toEqual '1$ start2end'
+
+ it 'folds items', ->
+ store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
+ log = Travis.Artifact.find(1)
+ job = Ember.Object.create log: log, subscribe: (-> )
+
+ Ember.run ->
+ view = Travis.LogView.create()
+ view.set('context', job)
+ view.append()
+
+ Ember.run ->
+ log.append '$ bundle install\n1\n2'
+
+ Ember.run ->
+ log.append '3\n4\n$ something'
+
+ expect( view.$('#log > p').length ).toEqual 2
+ expect( view.$('#log .fold.bundle').length ).toEqual 1
+ expect( view.$('#log .fold.bundle > p').length ).toEqual 5
diff --git a/assets/styles/main/log.sass b/assets/styles/main/log.sass
index 484d6757..0710e42c 100644
--- a/assets/styles/main/log.sass
+++ b/assets/styles/main/log.sass
@@ -39,6 +39,10 @@ pre#log
&.open
height: auto
background-image: inline-image('ui/log.fold.open.2.png')
+ &.show-first-line:not(.open)
+ height: 36px
+ p:not(:first-child):not(:last-child)
+ display: none
#log.loading
padding: 25px 0 0 10px