Animation frames generation on the backend

openid
Marcin Kulik 11 years ago
parent bc6e1ddc3d
commit e862acedb7

@ -0,0 +1,9 @@
class CellDecorator < ApplicationDecorator
delegate_all
def css_class
BrushPresenter.new(brush).to_css_class
end
end

@ -11,7 +11,15 @@ class SnapshotDecorator < ApplicationDecorator
def line(line_no)
line = (0...width).map { |column_no| model.cell(column_no, line_no) }
LineOptimizer.new(line).optimize
decorate_cells(optimize_line(line))
end
def optimize_line(line)
LineOptimizer.new.optimize(line)
end
def decorate_cells(cells)
cells.map { |cell| CellDecorator.new(cell) }
end
end

@ -5,6 +5,7 @@ class Asciicast < ActiveRecord::Base
mount_uploader :stdin_timing, StdinTimingUploader
mount_uploader :stdout_data, StdoutDataUploader
mount_uploader :stdout_timing, StdoutTimingUploader
mount_uploader :stdout_frames, BaseUploader
serialize :snapshot, JSON
@ -50,14 +51,16 @@ class Asciicast < ActiveRecord::Base
end
end
def update_snapshot(snapshot)
self.snapshot = snapshot
save!
end
def stdout
@stdout ||= BufferedStdout.new(stdout_data.decompressed_path,
stdout_timing.decompressed_path)
stdout_timing.decompressed_path).lazy
end
def with_terminal
terminal = Terminal.new(terminal_columns, terminal_lines)
yield(terminal)
ensure
terminal.release
end
end

@ -1,5 +1,7 @@
class Brush
ALLOWED_ATTRIBUTES = [:fg, :bg, :bold, :underline, :inverse, :blink]
def initialize(attributes = {})
@attributes = attributes.symbolize_keys
end
@ -57,6 +59,10 @@ class Brush
fg.nil? && bg.nil? && !bold? && !underline? && !inverse? && !blink?
end
def as_json(*)
attributes.slice(*ALLOWED_ATTRIBUTES)
end
protected
attr_reader :attributes

@ -15,8 +15,8 @@ class Cell
text == other.text && brush == other.brush
end
def css_class
BrushPresenter.new(brush).to_css_class
def as_json(*)
[text, brush.as_json]
end
end

@ -0,0 +1,18 @@
class Cursor
attr_reader :x, :y, :visible
def initialize(x, y, visible)
@x, @y, @visible = x, y, visible
end
def diff(other)
diff = {}
diff[:x] = x if other && x != other.x || other.nil?
diff[:y] = y if other && y != other.y || other.nil?
diff[:visible] = visible if other && visible != other.visible || other.nil?
diff
end
end

@ -0,0 +1,24 @@
class Frame
attr_reader :snapshot, :cursor
def initialize(snapshot, cursor)
@snapshot = snapshot
@cursor = cursor
end
def diff(other)
FrameDiff.new(snapshot_diff(other), cursor_diff(other))
end
private
def snapshot_diff(other)
snapshot.diff(other && other.snapshot)
end
def cursor_diff(other)
cursor.diff(other && other.cursor)
end
end

@ -0,0 +1,28 @@
class FrameDiff
def initialize(line_changes, cursor_changes)
@line_changes = line_changes
@cursor_changes = cursor_changes
end
def as_json(*)
json = {}
json[:lines] = optimized_line_changes unless line_changes.blank?
json[:cursor] = cursor_changes unless cursor_changes.blank?
json
end
private
attr_reader :line_changes, :cursor_changes
def optimized_line_changes
line_optimizer = LineOptimizer.new
line_changes.each_with_object({}) do |(k, v), h|
h[k] = line_optimizer.optimize(v)
end
end
end

@ -0,0 +1,24 @@
class FrameDiffList
include Enumerable
delegate :each, :to => :frame_diffs
def initialize(frames)
@frames = frames
end
private
attr_reader :frames
def frame_diffs
previous_frame = nil
frames.map { |delay, frame|
diff = frame.diff(previous_frame)
previous_frame = frame
[delay, diff]
}
end
end

@ -23,31 +23,18 @@ class Grid
def diff(other)
(0...height).each_with_object({}) do |y, diff|
if other.lines[y] != lines[y]
diff[y] = other.lines[y]
if other.nil? || other.lines[y] != lines[y]
diff[y] = lines[y]
end
end
end
def trailing_empty_lines
n = 0
(height - 1).downto(0) do |y|
break unless line_empty?(y)
n += 1
end
n
def as_json(*)
lines.as_json
end
protected
attr_reader :lines
private
def line_empty?(y)
lines[y].empty? || lines[y].all? { |item| item.empty? }
end
end

@ -1,6 +1,4 @@
class Snapshot
delegate :width, :height, :cell, :to => :grid
class Snapshot < Grid
def self.build(data)
data = data.map { |cells|
@ -9,26 +7,32 @@ class Snapshot
}
}
grid = Grid.new(data)
new(grid)
end
def initialize(grid)
@grid = grid
new(data)
end
def thumbnail(w, h)
x = 0
y = height - h - grid.trailing_empty_lines
y = height - h - trailing_empty_lines
y = 0 if y < 0
cropped_grid = grid.crop(x, y, w, h)
self.class.new(cropped_grid)
crop(x, y, w, h)
end
private
attr_reader :grid
def trailing_empty_lines
n = 0
(height - 1).downto(0) do |y|
break unless line_empty?(y)
n += 1
end
n
end
def line_empty?(y)
lines[y].empty? || lines[y].all? { |cell| cell.empty? }
end
end

@ -16,14 +16,6 @@ class Stdout
end
end
def each_until(seconds)
each do |delay, frame_data|
seconds -= delay
break if seconds <= 0
yield(delay, frame_data)
end
end
private
def delay_and_data_for_line(file, line)

@ -16,7 +16,11 @@ class Terminal
assign_cell(lines, x, y, char, screen_attribute)
end
lines
Snapshot.build(lines)
end
def cursor
Cursor.new(screen.cursor_x, screen.cursor_y, screen.cursor_visible?)
end
def release

@ -5,7 +5,7 @@ class AsciicastCreator
options = { :without_protection => true }
Asciicast.create!(attributes, options).tap do |asciicast|
SnapshotWorker.perform_async(asciicast.id)
AsciicastWorker.perform_async(asciicast.id)
end
end

@ -0,0 +1,26 @@
require 'tempfile'
class AsciicastFramesFileUpdater
def initialize(file_writer = JsonFileWriter.new)
@file_writer = file_writer
end
def update(asciicast)
file = Tempfile.new('frames')
asciicast.with_terminal do |terminal|
film = Film.new(asciicast.stdout, terminal)
file_writer.write_enumerable(file, film.frames)
end
asciicast.update_attribute(:stdout_frames, file)
ensure
file.unlink if file
end
private
attr_reader :file_writer
end

@ -0,0 +1,8 @@
class AsciicastProcessor
def process(asciicast)
AsciicastSnapshotUpdater.new.update(asciicast)
AsciicastFramesFileUpdater.new.update(asciicast)
end
end

@ -0,0 +1,16 @@
class AsciicastSnapshotUpdater
def update(asciicast, at_seconds = asciicast.duration / 2)
snapshot = generate_snapshot(asciicast, at_seconds)
asciicast.update_attribute(:snapshot, snapshot)
end
private
def generate_snapshot(asciicast, at_seconds)
asciicast.with_terminal do |terminal|
Film.new(asciicast.stdout, terminal).snapshot_at(at_seconds)
end
end
end

@ -0,0 +1,37 @@
class Film
def initialize(stdout, terminal)
@stdout = stdout
@terminal = terminal
end
def snapshot_at(time)
stdout_each_until(time) do |delay, data|
terminal.feed(data)
end
terminal.snapshot
end
def frames
frames = stdout.map do |delay, data|
terminal.feed(data)
[delay, Frame.new(terminal.snapshot, terminal.cursor)]
end
FrameDiffList.new(frames)
end
private
def stdout_each_until(seconds)
stdout.each do |delay, frame_data|
seconds -= delay
break if seconds <= 0
yield(delay, frame_data)
end
end
attr_reader :stdout, :terminal
end

@ -0,0 +1,21 @@
class JsonFileWriter
def write_enumerable(file, array)
first = true
file << '['
array.each do |item|
if first
first = false
else
file << ','
end
file << item.to_json
end
file << ']'
file.close
end
end

@ -1,10 +1,6 @@
class LineOptimizer
def initialize(line)
@line = line
end
def optimize
def optimize(line)
return [] if line.empty?
text = [line[0].text]
@ -26,8 +22,4 @@ class LineOptimizer
cells
end
private
attr_reader :line
end

@ -1,17 +0,0 @@
class SnapshotCreator
def create(width, height, stdout, duration)
terminal = Terminal.new(width, height)
seconds = (duration / 2).to_i
stdout.each_until(seconds) do |delay, data|
terminal.feed(data)
end
terminal.snapshot
ensure
terminal.release
end
end

@ -0,0 +1,13 @@
class AsciicastWorker
include Sidekiq::Worker
def perform(asciicast_id)
asciicast = Asciicast.find(asciicast_id)
AsciicastProcessor.new.process(asciicast)
rescue ActiveRecord::RecordNotFound
# oh well...
end
end

@ -1,21 +0,0 @@
class SnapshotWorker
include Sidekiq::Worker
def perform(asciicast_id)
asciicast = Asciicast.find(asciicast_id)
snapshot = SnapshotCreator.new.create(
asciicast.terminal_columns,
asciicast.terminal_lines,
asciicast.stdout,
asciicast.duration
)
asciicast.update_snapshot(snapshot)
rescue ActiveRecord::RecordNotFound
# oh well...
end
end

@ -0,0 +1,5 @@
class AddStdoutFramesToAsciicast < ActiveRecord::Migration
def change
add_column :asciicasts, :stdout_frames, :string
end
end

@ -9,90 +9,91 @@
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(:version => 20130725202344) do
ActiveRecord::Schema.define(version: 20130828162232) do
create_table "asciicasts", :force => true do |t|
create_table "asciicasts", force: true do |t|
t.integer "user_id"
t.string "title"
t.float "duration", :null => false
t.float "duration", null: false
t.datetime "recorded_at"
t.string "terminal_type"
t.integer "terminal_columns", :null => false
t.integer "terminal_lines", :null => false
t.integer "terminal_columns", null: false
t.integer "terminal_lines", null: false
t.string "command"
t.string "shell"
t.string "uname"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "stdin_data"
t.string "stdin_timing"
t.string "stdout_data"
t.string "stdout_timing"
t.string "user_token"
t.text "description"
t.boolean "featured", :default => false
t.boolean "featured", default: false
t.string "username"
t.integer "likes_count", :default => 0, :null => false
t.integer "likes_count", default: 0, null: false
t.text "snapshot"
t.integer "comments_count", :default => 0, :null => false
t.boolean "time_compression", :default => true, :null => false
t.integer "views_count", :default => 0, :null => false
t.integer "comments_count", default: 0, null: false
t.boolean "time_compression", default: true, null: false
t.integer "views_count", default: 0, null: false
t.string "stdout_frames"
end
add_index "asciicasts", ["created_at"], :name => "index_asciicasts_on_created_at"
add_index "asciicasts", ["featured"], :name => "index_asciicasts_on_featured"
add_index "asciicasts", ["likes_count"], :name => "index_asciicasts_on_likes_count"
add_index "asciicasts", ["recorded_at"], :name => "index_asciicasts_on_recorded_at"
add_index "asciicasts", ["user_id"], :name => "index_asciicasts_on_user_id"
add_index "asciicasts", ["user_token"], :name => "index_asciicasts_on_user_token"
add_index "asciicasts", ["views_count"], :name => "index_asciicasts_on_views_count"
add_index "asciicasts", ["created_at"], name: "index_asciicasts_on_created_at", using: :btree
add_index "asciicasts", ["featured"], name: "index_asciicasts_on_featured", using: :btree
add_index "asciicasts", ["likes_count"], name: "index_asciicasts_on_likes_count", using: :btree
add_index "asciicasts", ["recorded_at"], name: "index_asciicasts_on_recorded_at", using: :btree
add_index "asciicasts", ["user_id"], name: "index_asciicasts_on_user_id", using: :btree
add_index "asciicasts", ["user_token"], name: "index_asciicasts_on_user_token", using: :btree
add_index "asciicasts", ["views_count"], name: "index_asciicasts_on_views_count", using: :btree
create_table "comments", :force => true do |t|
t.text "body", :null => false
t.integer "user_id", :null => false
t.integer "asciicast_id", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
create_table "comments", force: true do |t|
t.text "body", null: false
t.integer "user_id", null: false
t.integer "asciicast_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "comments", ["asciicast_id", "created_at"], :name => "index_comments_on_asciicast_id_and_created_at"
add_index "comments", ["asciicast_id"], :name => "index_comments_on_asciicast_id"
add_index "comments", ["user_id"], :name => "index_comments_on_user_id"
add_index "comments", ["asciicast_id", "created_at"], name: "index_comments_on_asciicast_id_and_created_at", using: :btree
add_index "comments", ["asciicast_id"], name: "index_comments_on_asciicast_id", using: :btree
add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree
create_table "likes", :force => true do |t|
t.integer "asciicast_id", :null => false
t.integer "user_id", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
create_table "likes", force: true do |t|
t.integer "asciicast_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "likes", ["asciicast_id"], :name => "index_likes_on_asciicast_id"
add_index "likes", ["user_id", "asciicast_id"], :name => "index_likes_on_user_id_and_asciicast_id"
add_index "likes", ["user_id"], :name => "index_likes_on_user_id"
add_index "likes", ["asciicast_id"], name: "index_likes_on_asciicast_id", using: :btree
add_index "likes", ["user_id", "asciicast_id"], name: "index_likes_on_user_id_and_asciicast_id", using: :btree
add_index "likes", ["user_id"], name: "index_likes_on_user_id", using: :btree
create_table "user_tokens", :force => true do |t|
t.integer "user_id", :null => false
t.string "token", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
create_table "user_tokens", force: true do |t|
t.integer "user_id", null: false
t.string "token", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "user_tokens", ["token"], :name => "index_user_tokens_on_token"
add_index "user_tokens", ["user_id"], :name => "index_user_tokens_on_user_id"
add_index "user_tokens", ["token"], name: "index_user_tokens_on_token", using: :btree
add_index "user_tokens", ["user_id"], name: "index_user_tokens_on_user_id", using: :btree
create_table "users", :force => true do |t|
t.string "provider", :null => false
t.string "uid", :null => false
create_table "users", force: true do |t|
t.string "provider", null: false
t.string "uid", null: false
t.string "email"
t.string "name"
t.string "avatar_url"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.string "nickname", :null => false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "nickname", null: false
end
add_index "users", ["provider", "uid"], :name => "index_users_on_provider_and_uid", :unique => true
add_index "users", ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true, using: :btree
end

@ -0,0 +1,21 @@
require 'spec_helper'
describe CellDecorator do
let(:decorator) { described_class.new(cell) }
let(:cell) { double('cell', :brush => brush) }
let(:brush) { double('brush') }
describe '#css_class' do
let(:brush_presenter) { double('brush_presenter', :to_css_class => 'kls') }
subject { decorator.css_class }
before do
allow(BrushPresenter).to receive(:new).with(brush) { brush_presenter }
end
it { should eq('kls') }
end
end

@ -4,6 +4,7 @@ describe SnapshotDecorator do
let(:decorator) { described_class.new(snapshot) }
let(:snapshot) { double('snapshot', :width => 2, :height => 2) }
let(:optimizer) { double('optimizer') }
let(:cells) { [
[:a, :b],
[:c, :d]
@ -15,16 +16,16 @@ describe SnapshotDecorator do
before do
allow(snapshot).to receive(:cell) { |x, y| cells[y][x] }
allow(LineOptimizer).to receive(:new).with([:a, :b]) {
double('optimizer', :optimize => [:ab])
}
allow(LineOptimizer).to receive(:new) { optimizer }
allow(optimizer).to receive(:optimize).with([:a, :b]) { [:ab] }
allow(optimizer).to receive(:optimize).with([:c, :d]) { [:c, :d] }
allow(LineOptimizer).to receive(:new).with([:c, :d]) {
double('optimizer', :optimize => [:c, :d])
}
allow(CellDecorator).to receive(:new).with(:ab) { :AB }
allow(CellDecorator).to receive(:new).with(:c) { :C }
allow(CellDecorator).to receive(:new).with(:d) { :D }
end
it { should eq([ [:ab], [:c, :d] ]) }
it { should eq([ [:AB], [:C, :D] ]) }
end
end

@ -1,4 +1,5 @@
require 'spec_helper'
require 'tempfile'
describe Asciicast do
@ -72,38 +73,30 @@ describe Asciicast do
end
end
describe '#update_snapshot' do
let(:asciicast) { create(:asciicast) }
let(:snapshot) { [[[{ :foo => 'bar' }]]] }
it 'persists the snapshot' do
asciicast.update_snapshot(snapshot)
snapshot = Asciicast.find(asciicast.id).snapshot
expect(snapshot).to eq([[[{ 'foo' => 'bar' }]]])
end
end
describe '#stdout' do
let(:asciicast) { stub_model(Asciicast) }
let(:data_file) { double('data_file', :decompressed_path => '/foo') }
let(:timing_file) { double('timing_file', :decompressed_path => '/bar') }
let(:stdout) { double('stdout') }
let(:asciicast) { Asciicast.new }
let(:data_uploader) { double('data_uploader', :decompressed_path => '/foo') }
let(:timing_uploader) { double('timing_uploader', :decompressed_path => '/bar') }
let(:stdout) { double('stdout', :lazy => lazy_stdout) }
let(:lazy_stdout) { double('lazy_stdout') }
subject { asciicast.stdout }
before do
allow(asciicast).to receive(:stdout_data) { data_file }
allow(asciicast).to receive(:stdout_timing) { timing_file }
allow(BufferedStdout).to receive(:new) { stdout }
allow(StdoutDataUploader).to receive(:new) { data_uploader }
allow(StdoutTimingUploader).to receive(:new) { timing_uploader }
end
it 'creates a new BufferedStdout instance' do
asciicast.stdout
subject
expect(BufferedStdout).to have_received(:new).with('/foo', '/bar')
end
it 'returns created Stdout instance' do
expect(asciicast.stdout).to be(stdout)
it 'returns lazy instance of stdout' do
expect(subject).to be(lazy_stdout)
end
end
end

@ -267,4 +267,12 @@ describe Brush do
end
end
describe '#as_json' do
let(:attributes) { { fg: 1, bold: true, trolololo: 'OX' } }
subject { brush.as_json }
it { should eq({ fg: 1, bold: true }) }
end
end

@ -59,16 +59,14 @@ describe Cell do
end
end
describe '#css_class' do
let(:brush_presenter) { double('brush_presenter', :to_css_class => 'kls') }
subject { cell.css_class }
describe '#as_json' do
subject { cell.as_json }
before do
allow(BrushPresenter).to receive(:new).with(brush) { brush_presenter }
allow(brush).to receive(:as_json) { { fg: 1, bold: true } }
end
it { should eq('kls') }
it { should eq(['a', { fg: 1, bold: true }]) }
end
end

@ -0,0 +1,45 @@
require 'spec_helper'
describe Cursor do
let(:cursor) { described_class.new(1, 2, true) }
describe '#diff' do
let(:other) { described_class.new(3, 4, false) }
subject { cursor.diff(other) }
it { should eq({ x: 1, y: 2, visible: true }) }
context "when x is the same" do
let(:other) { described_class.new(1, 4, false) }
it 'skips x from the hash' do
expect(subject).not_to have_key(:x)
end
end
context "when y is the same" do
let(:other) { described_class.new(3, 2, false) }
it 'skips y from the hash' do
expect(subject).not_to have_key(:y)
end
end
context "when visible is the same" do
let(:other) { described_class.new(3, 4, true) }
it 'skips visible from the hash' do
expect(subject).not_to have_key(:visible)
end
end
context "when other is nil" do
let(:other) { nil }
it { should eq({ x: 1, y: 2, visible: true }) }
end
end
end

@ -0,0 +1,32 @@
require 'spec_helper'
describe FrameDiffList do
let(:frame_diff_list) { described_class.new(frames) }
let(:frames) { [[1.5, frame_1], [0.5, frame_2]] }
let(:frame_1) { double('frame_1', :diff => diff_1) }
let(:frame_2) { double('frame_2', :diff => diff_2) }
let(:diff_1) { double('diff_1') }
let(:diff_2) { double('diff_2') }
describe '#each' do
subject { frame_diff_list.to_a }
it 'maps each frame to its diff' do
expect(subject).to eq([[1.5, diff_1], [0.5, diff_2]])
end
it 'diffs the first frame with nil' do
subject
expect(frame_1).to have_received(:diff).with(nil)
end
it 'diffs the subsequent frames with the previous ones' do
subject
expect(frame_2).to have_received(:diff).with(frame_1)
end
end
end

@ -0,0 +1,48 @@
require 'spec_helper'
describe FrameDiff do
let(:frame_diff) { described_class.new(line_changes, cursor_changes) }
let(:line_changes) { { 0 => line_0, 2 => line_2 } }
let(:cursor_changes) { { x: 1 } }
let(:line_0) { double('line_0') }
let(:line_2) { double('line_2') }
let(:line_optimizer) { double('line_optimizer') }
let(:optimized_line_0) { double('optimized_line_0') }
let(:optimized_line_2) { double('optimized_line_2') }
describe '#as_json' do
subject { frame_diff.as_json }
before do
allow(LineOptimizer).to receive(:new) { line_optimizer }
allow(line_optimizer).to receive(:optimize).
with(line_0) { optimized_line_0 }
allow(line_optimizer).to receive(:optimize).
with(line_2) { optimized_line_2 }
end
it 'includes line changes and cursor changes' do
expect(subject).to eq({ :lines => { 0 => optimized_line_0,
2 => optimized_line_2 },
:cursor => cursor_changes })
end
context "when there are no line changes" do
let(:line_changes) { {} }
it 'skips the lines hash' do
expect(subject).not_to have_key(:lines)
end
end
context "when there are no cursor changes" do
let(:cursor_changes) { {} }
it 'skips the cursor hash' do
expect(subject).not_to have_key(:cursor)
end
end
end
end

@ -0,0 +1,46 @@
require 'spec_helper'
describe Frame do
let(:frame) { described_class.new(snapshot, cursor) }
let(:snapshot) { double('snapshot', :diff => snapshot_diff) }
let(:cursor) { double('cursor', :diff => cursor_diff) }
let(:snapshot_diff) { double('snapshot_diff') }
let(:cursor_diff) { double('cursor_diff') }
describe '#diff' do
let(:other) { double('other', :snapshot => other_snapshot,
:cursor => other_cursor) }
let(:other_snapshot) { double('other_snapshot') }
let(:other_cursor) { double('other_cursor') }
let(:frame_diff) { double('frame_diff') }
subject { frame.diff(other) }
before do
allow(FrameDiff).to receive(:new).
with(snapshot_diff, cursor_diff) { frame_diff }
end
it 'returns a FrameDiff instance built from snapshot and cursor diffs' do
expect(subject).to be(frame_diff)
end
context "when other is nil" do
let(:other) { nil }
it 'diffs its snapshot with nil' do
subject
expect(snapshot).to have_received(:diff).with(nil)
end
it 'diffs its cursor with nil' do
subject
expect(cursor).to have_received(:diff).with(nil)
end
end
end
end

@ -65,23 +65,30 @@ describe Grid do
let(:grid) { described_class.new([line_a, line_b, line_c]) }
let(:line_aa) { [:A, :b, :c] }
let(:line_cc) { [:g, :H, :i] }
let(:line_d) { [:A, :b, :c] }
let(:line_e) { [:g, :H, :i] }
let(:other) { described_class.new([line_aa, line_b, line_cc]) }
let(:other) { described_class.new([line_d, line_b, line_e]) }
subject { grid.diff(other) }
it { should eq({ 0 => [:A, :b, :c], 2 => [:g, :H, :i] }) }
end
it 'returns only the lines that have changed from the other grid' do
should eq({ 0 => line_a, 2 => line_c })
end
context "when other is nil" do
let(:other) { nil }
describe '#trailing_empty_lines' do
let(:grid) { described_class.new(data) }
let(:data) { [ [:a], [''], [] ] }
it 'returns all the lines' do
should eq({ 0 => line_a, 1 => line_b, 2 => line_c })
end
end
end
subject { grid.trailing_empty_lines }
describe '#as_json' do
subject { grid.as_json }
it { should eq(2) }
it { should eq([%w[a b c], %w[d e f], %w[g h i], %w[j k l]]) }
end
end

@ -2,52 +2,30 @@ require 'spec_helper'
describe Snapshot do
let(:snapshot) { described_class.new(grid) }
let(:grid) { double('grid', :width => 10, :height => 5,
:trailing_empty_lines => 2) }
describe '#width' do
subject { snapshot.width }
it { should eq(10) }
end
describe '#height' do
subject { snapshot.height }
it { should eq(5) }
end
describe '#cell' do
subject { snapshot.cell(1, 2) }
before do
allow(grid).to receive(:cell).with(1, 2) { :a }
end
it { should eq(:a) }
end
let(:snapshot) { described_class.build(data) }
let(:data) { [
[['a', fg: 1], ['b', fg: 2]],
[['a', fg: 3], ['b', fg: 4]],
[['a', fg: 5], ['b', fg: 6]],
[[' ', {}] , ['' , {}]]
] }
describe '#thumbnail' do
let(:thumbnail) { snapshot.thumbnail(2, 4) }
before do
allow(grid).to receive(:crop) { Grid.new([[:a, :b], [:c, :d],
[:e, :f], [:g, :h]]) }
end
let(:thumbnail) { snapshot.thumbnail(1, 2) }
it 'returns a thumbnail of requested width' do
expect(thumbnail.width).to eq(2)
expect(thumbnail.width).to eq(1)
end
it 'returns a thumbnail of requested height' do
expect(thumbnail.height).to eq(4)
expect(thumbnail.height).to eq(2)
end
it 'crops the grid at the bottom left corner' do
thumbnail
expect(grid).to have_received(:crop).with(0, 0, 2, 4)
expect(thumbnail.as_json).to eq([
[['a', fg: 3]],
[['a', fg: 5]]
])
end
end

@ -15,11 +15,4 @@ describe Stdout do
end
end
describe '#each_until' do
it 'yields for each frame with delay and data until <seconds>' do
expect { |b| stdout.each_until(1.7, &b) }.
to yield_successive_args([0.5, 'foobar'], [1.0, "bazqux\xC5"])
end
end
end

@ -7,12 +7,10 @@ describe Terminal do
let(:terminal) { Terminal.new(20, 10) }
let(:tsm_screen) { double('tsm_screen', :draw => nil) }
let(:tsm_vte) { double('tsm_vte', :input => nil) }
let(:snapshot) { double('snapshot') }
before do
allow(TSM::Screen).to receive(:new).with(20, 10) { tsm_screen }
allow(TSM::Vte).to receive(:new).with(tsm_screen) { tsm_vte }
allow(Snapshot).to receive(:build).with([:array]) { snapshot }
end
describe '#feed' do
@ -44,8 +42,12 @@ describe Terminal do
inverse?: true, blink?: true))
end
it 'returns an instance of Snapshot' do
expect(subject).to be_kind_of(Snapshot)
end
it "returns each screen cell with its character attributes" do
expect(subject).to eq([
expect(subject.as_json).to eq([
[
['f', fg: 1],
['o', bg: 2]
@ -65,9 +67,28 @@ describe Terminal do
end
it 'gets replaced with "?"' do
expect(subject).to eq([[['?', fg: 1]]])
expect(subject.as_json).to eq([[['?', fg: 1]]])
end
end
end
describe '#cursor' do
let(:tsm_screen) { double('tsm_screen', :cursor_x => 3, :cursor_y => 5,
:cursor_visible? => false) }
subject { terminal.cursor }
it 'gets its x position from the screen' do
expect(subject.x).to eq(3)
end
it 'gets its y position from the screen' do
expect(subject.y).to eq(5)
end
it 'gets its visibility from the screen' do
expect(subject.visible).to eq(false)
end
end
end

@ -49,7 +49,7 @@ describe AsciicastCreator do
it 'enqueues snapshot capture job' do
subject
expect(SnapshotWorker).to have_queued_job(asciicast.id)
expect(AsciicastWorker).to have_queued_job(asciicast.id)
end
it 'returns the created asciicast' do

@ -0,0 +1,31 @@
require 'spec_helper'
describe AsciicastFramesFileUpdater do
let(:updater) { described_class.new(file_writer) }
let(:file_writer) { double('file_writer') }
describe '#update' do
let(:asciicast) { create(:asciicast) }
let(:film) { double('film', :frames => frames) }
let(:frames) { [1, 2] }
subject { updater.update(asciicast) }
before do
allow(Film).to receive(:new).with(asciicast.stdout, kind_of(Terminal)) {
film
}
allow(file_writer).to receive(:write_enumerable) do |file, frames|
file << frames.to_json
end
end
it 'updates stdout_frames file on asciicast' do
subject
expect(asciicast.stdout_frames.read).to eq('[1,2]')
end
end
end

@ -0,0 +1,32 @@
require 'spec_helper'
describe AsciicastProcessor do
let(:processor) { described_class.new }
describe '#process' do
let(:asciicast) { double('asciicast') }
let(:snapshot_updater) { double('snapshot_updater', :update => nil) }
let(:frames_file_updater) { double('frames_file_updater', :update => nil) }
subject { processor.process(asciicast) }
before do
allow(AsciicastSnapshotUpdater).to receive(:new) { snapshot_updater }
allow(AsciicastFramesFileUpdater).to receive(:new) { frames_file_updater }
end
it 'generates a snapshot' do
subject
expect(snapshot_updater).to have_received(:update).with(asciicast)
end
it 'generates animation frames' do
subject
expect(frames_file_updater).to have_received(:update).with(asciicast)
end
end
end

@ -0,0 +1,41 @@
require 'spec_helper'
describe AsciicastSnapshotUpdater do
let(:updater) { described_class.new }
describe '#update' do
let(:asciicast) { double('asciicast', :duration => 5.0, :stdout => stdout,
:update_attribute => nil) }
let(:stdout) { double('stdout') }
let(:terminal) { double('terminal') }
let(:film) { double('film', :snapshot_at => 'foo') }
subject { updater.update(asciicast) }
before do
allow(asciicast).to receive(:with_terminal).and_yield(terminal)
allow(Film).to receive(:new).with(stdout, terminal) { film }
subject
end
it "generates the snapshot at half of asciicast's duration" do
expect(film).to have_received(:snapshot_at).with(2.5)
end
it "updates asciicast's snapshot to the terminal's snapshot" do
expect(asciicast).to have_received(:update_attribute).
with(:snapshot, 'foo')
end
context "when snapshot time given" do
subject { updater.update(asciicast, 4.3) }
it "generates the snapshot at the given time" do
expect(film).to have_received(:snapshot_at).with(4.3)
end
end
end
end

@ -0,0 +1,38 @@
require 'spec_helper'
describe Film do
let(:film) { described_class.new(stdout, terminal) }
let(:terminal) { FakeTerminal.new }
describe '#snapshot_at' do
let(:stdout) { [[0.5, 'ab'], [1.0, 'cd'], [2.0, 'ef']] }
subject { film.snapshot_at(1.7) }
it "returns the snapshot of the terminal" do
expect(subject).to eq('abcd')
end
end
describe '#frames' do
let(:stdout) { [[0.5, 'ab'], [1.0, 'cd']] }
let(:frame_1) { double('frame_1') }
let(:frame_2) { double('frame_2') }
let(:frame_diff_list) { double('frame_diff_list') }
subject { film.frames }
before do
allow(Frame).to receive(:new).with('ab', 2) { frame_1 }
allow(Frame).to receive(:new).with('abcd', 4) { frame_2 }
allow(FrameDiffList).to receive(:new).
with([[0.5, frame_1], [1.0, frame_2]]) { frame_diff_list }
end
it 'returns delay and frame tuples wrapped with FrameDiffList' do
expect(subject).to be(frame_diff_list)
end
end
end

@ -0,0 +1,28 @@
require 'spec_helper'
describe JsonFileWriter do
let(:writer) { described_class.new }
describe '#write_enumerable' do
let(:file) { StringIO.new }
let(:enumerable) { [item_1, item_2] }
let(:item_1) { double('item_1', :to_json => 'a') }
let(:item_2) { double('item_2', :to_json => 'b') }
subject { writer.write_enumerable(file, enumerable) }
before do
subject
end
it 'writes the enumerable to the file in json format' do
expect(file.string).to eq('[a,b]')
end
it 'closes the file' do
expect(file).to be_closed
end
end
end

@ -2,7 +2,7 @@ require 'spec_helper'
describe LineOptimizer do
let(:line_optimizer) { described_class.new(line) }
let(:line_optimizer) { described_class.new }
def brush(attrs)
Brush.new(attrs)
@ -17,7 +17,7 @@ describe LineOptimizer do
Cell.new('e', brush(fg: 3))
] }
subject { line_optimizer.optimize }
subject { line_optimizer.optimize(line) }
it { should eq([
Cell.new('ab', brush(fg: 1)),

@ -1,39 +0,0 @@
# encoding: utf-8
require 'spec_helper'
describe SnapshotCreator do
let(:snapshot_creator) { SnapshotCreator.new }
describe '#create' do
let(:stdout) { double('stdout', :each_until => nil) }
let(:terminal) { double('terminal', :feed => nil, :snapshot => snapshot,
:release => nil) }
let(:snapshot) { double('snapshot') }
subject { snapshot_creator.create(80, 24, stdout, 31.4) }
before do
allow(Terminal).to receive(:new).with(80, 24) { terminal }
allow(stdout).to receive(:each_until).and_yield(1.2, "\xBCółć")
end
it 'uses Terminal to generate a snapshot' do
subject
expect(terminal).to have_received(:feed).with("\xBCółć")
end
it 'gets the bytes from stdout for half duration (whole seconds)' do
subject
expect(stdout).to have_received(:each_until).with(15)
end
it 'returns the snapshot from the Terminal' do
expect(subject).to be(snapshot)
end
end
end

@ -0,0 +1,19 @@
class FakeTerminal
def initialize
@data = ''
end
def feed(data)
@data << data
end
def snapshot
@data
end
def cursor
@data.size
end
end

@ -0,0 +1,23 @@
require 'spec_helper'
describe AsciicastWorker do
let(:worker) { described_class.new }
describe '#perform' do
let(:asciicast) { double('asciicast') }
let(:asciicast_processor) { double('asciicast_processor', :process => nil) }
before do
allow(Asciicast).to receive(:find).with(123) { asciicast }
allow(AsciicastProcessor).to receive(:new).with(no_args) { asciicast_processor }
end
it 'processes given asciicast with AsciicastProcessor' do
worker.perform(123)
expect(asciicast_processor).to have_received(:process).with(asciicast)
end
end
end

@ -1,35 +0,0 @@
require 'spec_helper'
describe SnapshotWorker do
let(:worker) { SnapshotWorker.new }
describe '#perform' do
let(:snapshot_creator) { double('snapshot_creator', :create => snapshot) }
let(:snapshot) { double('snapshot') }
let(:asciicast) { double('asciicast', :terminal_columns => 9,
:terminal_lines => 5,
:duration => 4.3,
:stdout => stdout,
:update_snapshot => nil) }
let(:stdout) { double('stdout') }
before do
allow(Asciicast).to receive(:find).with(123) { asciicast }
allow(SnapshotCreator).to receive(:new).with(no_args) { snapshot_creator }
end
it 'uses AsciicastSnapshotCreator to generate a snapshot' do
worker.perform(123)
expect(snapshot_creator).to have_received(:create).with(9, 5, stdout, 4.3)
end
it 'updates the snapshot on the asciicast' do
worker.perform(123)
expect(asciicast).to have_received(:update_snapshot).with(snapshot)
end
end
end
Loading…
Cancel
Save