diff --git a/app/decorators/cell_decorator.rb b/app/decorators/cell_decorator.rb new file mode 100644 index 0000000..bdd3035 --- /dev/null +++ b/app/decorators/cell_decorator.rb @@ -0,0 +1,9 @@ +class CellDecorator < ApplicationDecorator + + delegate_all + + def css_class + BrushPresenter.new(brush).to_css_class + end + +end diff --git a/app/decorators/snapshot_decorator.rb b/app/decorators/snapshot_decorator.rb index 05334ab..477abb7 100644 --- a/app/decorators/snapshot_decorator.rb +++ b/app/decorators/snapshot_decorator.rb @@ -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 diff --git a/app/models/asciicast.rb b/app/models/asciicast.rb index cd40dff..4b08a0c 100644 --- a/app/models/asciicast.rb +++ b/app/models/asciicast.rb @@ -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 diff --git a/app/models/brush.rb b/app/models/brush.rb index a894419..1fd0ce4 100644 --- a/app/models/brush.rb +++ b/app/models/brush.rb @@ -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 diff --git a/app/models/cell.rb b/app/models/cell.rb index fb32bdf..df887dc 100644 --- a/app/models/cell.rb +++ b/app/models/cell.rb @@ -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 diff --git a/app/models/cursor.rb b/app/models/cursor.rb new file mode 100644 index 0000000..027aee5 --- /dev/null +++ b/app/models/cursor.rb @@ -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 diff --git a/app/models/frame.rb b/app/models/frame.rb new file mode 100644 index 0000000..b6faa1d --- /dev/null +++ b/app/models/frame.rb @@ -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 diff --git a/app/models/frame_diff.rb b/app/models/frame_diff.rb new file mode 100644 index 0000000..7cd5cc9 --- /dev/null +++ b/app/models/frame_diff.rb @@ -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 diff --git a/app/models/frame_diff_list.rb b/app/models/frame_diff_list.rb new file mode 100644 index 0000000..6d718d9 --- /dev/null +++ b/app/models/frame_diff_list.rb @@ -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 diff --git a/app/models/grid.rb b/app/models/grid.rb index 2660a3c..597075e 100644 --- a/app/models/grid.rb +++ b/app/models/grid.rb @@ -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 diff --git a/app/models/snapshot.rb b/app/models/snapshot.rb index f38395f..2e58186 100644 --- a/app/models/snapshot.rb +++ b/app/models/snapshot.rb @@ -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 diff --git a/app/models/stdout.rb b/app/models/stdout.rb index af5954d..9c3a435 100644 --- a/app/models/stdout.rb +++ b/app/models/stdout.rb @@ -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) diff --git a/app/models/terminal.rb b/app/models/terminal.rb index cf7ba33..aca5c5a 100644 --- a/app/models/terminal.rb +++ b/app/models/terminal.rb @@ -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 diff --git a/app/services/asciicast_creator.rb b/app/services/asciicast_creator.rb index 27fdfd0..81270f1 100644 --- a/app/services/asciicast_creator.rb +++ b/app/services/asciicast_creator.rb @@ -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 diff --git a/app/services/asciicast_frames_file_updater.rb b/app/services/asciicast_frames_file_updater.rb new file mode 100644 index 0000000..f8b38a9 --- /dev/null +++ b/app/services/asciicast_frames_file_updater.rb @@ -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 diff --git a/app/services/asciicast_processor.rb b/app/services/asciicast_processor.rb new file mode 100644 index 0000000..4de35ba --- /dev/null +++ b/app/services/asciicast_processor.rb @@ -0,0 +1,8 @@ +class AsciicastProcessor + + def process(asciicast) + AsciicastSnapshotUpdater.new.update(asciicast) + AsciicastFramesFileUpdater.new.update(asciicast) + end + +end diff --git a/app/services/asciicast_snapshot_updater.rb b/app/services/asciicast_snapshot_updater.rb new file mode 100644 index 0000000..665f07e --- /dev/null +++ b/app/services/asciicast_snapshot_updater.rb @@ -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 diff --git a/app/services/film.rb b/app/services/film.rb new file mode 100644 index 0000000..d2452f6 --- /dev/null +++ b/app/services/film.rb @@ -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 diff --git a/app/services/json_file_writer.rb b/app/services/json_file_writer.rb new file mode 100644 index 0000000..6a4f664 --- /dev/null +++ b/app/services/json_file_writer.rb @@ -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 diff --git a/app/services/line_optimizer.rb b/app/services/line_optimizer.rb index 6174d83..b60c8e8 100644 --- a/app/services/line_optimizer.rb +++ b/app/services/line_optimizer.rb @@ -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 diff --git a/app/services/snapshot_creator.rb b/app/services/snapshot_creator.rb deleted file mode 100644 index 6fd4f43..0000000 --- a/app/services/snapshot_creator.rb +++ /dev/null @@ -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 diff --git a/app/workers/asciicast_worker.rb b/app/workers/asciicast_worker.rb new file mode 100644 index 0000000..fe373d2 --- /dev/null +++ b/app/workers/asciicast_worker.rb @@ -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 diff --git a/app/workers/snapshot_worker.rb b/app/workers/snapshot_worker.rb deleted file mode 100644 index 9a678ca..0000000 --- a/app/workers/snapshot_worker.rb +++ /dev/null @@ -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 diff --git a/db/migrate/20130828162232_add_stdout_frames_to_asciicast.rb b/db/migrate/20130828162232_add_stdout_frames_to_asciicast.rb new file mode 100644 index 0000000..26d140e --- /dev/null +++ b/db/migrate/20130828162232_add_stdout_frames_to_asciicast.rb @@ -0,0 +1,5 @@ +class AddStdoutFramesToAsciicast < ActiveRecord::Migration + def change + add_column :asciicasts, :stdout_frames, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 9817c00..99a97d4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/decorators/cell_decorator_spec.rb b/spec/decorators/cell_decorator_spec.rb new file mode 100644 index 0000000..45f5c2c --- /dev/null +++ b/spec/decorators/cell_decorator_spec.rb @@ -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 diff --git a/spec/decorators/snapshot_decorator_spec.rb b/spec/decorators/snapshot_decorator_spec.rb index 3985e66..bae8014 100644 --- a/spec/decorators/snapshot_decorator_spec.rb +++ b/spec/decorators/snapshot_decorator_spec.rb @@ -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 diff --git a/spec/models/asciicast_spec.rb b/spec/models/asciicast_spec.rb index 5c5da8e..08a4995 100644 --- a/spec/models/asciicast_spec.rb +++ b/spec/models/asciicast_spec.rb @@ -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 diff --git a/spec/models/brush_spec.rb b/spec/models/brush_spec.rb index 649cb8a..ab2f891 100644 --- a/spec/models/brush_spec.rb +++ b/spec/models/brush_spec.rb @@ -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 diff --git a/spec/models/cell_spec.rb b/spec/models/cell_spec.rb index 1703cfd..73cae95 100644 --- a/spec/models/cell_spec.rb +++ b/spec/models/cell_spec.rb @@ -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 diff --git a/spec/models/cursor_spec.rb b/spec/models/cursor_spec.rb new file mode 100644 index 0000000..7e64dd1 --- /dev/null +++ b/spec/models/cursor_spec.rb @@ -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 diff --git a/spec/models/frame_diff_list_spec.rb b/spec/models/frame_diff_list_spec.rb new file mode 100644 index 0000000..6fe0bd0 --- /dev/null +++ b/spec/models/frame_diff_list_spec.rb @@ -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 diff --git a/spec/models/frame_diff_spec.rb b/spec/models/frame_diff_spec.rb new file mode 100644 index 0000000..23f0501 --- /dev/null +++ b/spec/models/frame_diff_spec.rb @@ -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 diff --git a/spec/models/frame_spec.rb b/spec/models/frame_spec.rb new file mode 100644 index 0000000..6b43b21 --- /dev/null +++ b/spec/models/frame_spec.rb @@ -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 diff --git a/spec/models/grid_spec.rb b/spec/models/grid_spec.rb index 5337d39..f2ad1c9 100644 --- a/spec/models/grid_spec.rb +++ b/spec/models/grid_spec.rb @@ -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 diff --git a/spec/models/snapshot_spec.rb b/spec/models/snapshot_spec.rb index e1d87c8..7a486d7 100644 --- a/spec/models/snapshot_spec.rb +++ b/spec/models/snapshot_spec.rb @@ -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 diff --git a/spec/models/stdout_spec.rb b/spec/models/stdout_spec.rb index 6c90f89..817060e 100644 --- a/spec/models/stdout_spec.rb +++ b/spec/models/stdout_spec.rb @@ -15,11 +15,4 @@ describe Stdout do end end - describe '#each_until' do - it 'yields for each frame with delay and data until ' do - expect { |b| stdout.each_until(1.7, &b) }. - to yield_successive_args([0.5, 'foobar'], [1.0, "bazqux\xC5"]) - end - end - end diff --git a/spec/models/terminal_spec.rb b/spec/models/terminal_spec.rb index aa9a0c5..850fd9f 100644 --- a/spec/models/terminal_spec.rb +++ b/spec/models/terminal_spec.rb @@ -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 diff --git a/spec/services/asciicast_creator_spec.rb b/spec/services/asciicast_creator_spec.rb index 497ec97..85fae25 100644 --- a/spec/services/asciicast_creator_spec.rb +++ b/spec/services/asciicast_creator_spec.rb @@ -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 diff --git a/spec/services/asciicast_frames_file_updater_spec.rb b/spec/services/asciicast_frames_file_updater_spec.rb new file mode 100644 index 0000000..f4fee69 --- /dev/null +++ b/spec/services/asciicast_frames_file_updater_spec.rb @@ -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 diff --git a/spec/services/asciicast_processor_spec.rb b/spec/services/asciicast_processor_spec.rb new file mode 100644 index 0000000..369f052 --- /dev/null +++ b/spec/services/asciicast_processor_spec.rb @@ -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 diff --git a/spec/services/asciicast_snapshot_updater_spec.rb b/spec/services/asciicast_snapshot_updater_spec.rb new file mode 100644 index 0000000..d4c2916 --- /dev/null +++ b/spec/services/asciicast_snapshot_updater_spec.rb @@ -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 diff --git a/spec/services/film_spec.rb b/spec/services/film_spec.rb new file mode 100644 index 0000000..7c4e8d9 --- /dev/null +++ b/spec/services/film_spec.rb @@ -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 diff --git a/spec/services/json_file_writer_spec.rb b/spec/services/json_file_writer_spec.rb new file mode 100644 index 0000000..d303aaa --- /dev/null +++ b/spec/services/json_file_writer_spec.rb @@ -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 diff --git a/spec/services/line_optimizer_spec.rb b/spec/services/line_optimizer_spec.rb index 4c9db6b..dbbdb3c 100644 --- a/spec/services/line_optimizer_spec.rb +++ b/spec/services/line_optimizer_spec.rb @@ -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)), diff --git a/spec/services/snapshot_creator_spec.rb b/spec/services/snapshot_creator_spec.rb deleted file mode 100644 index b0e85e9..0000000 --- a/spec/services/snapshot_creator_spec.rb +++ /dev/null @@ -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 diff --git a/spec/support/fakes.rb b/spec/support/fakes.rb new file mode 100644 index 0000000..d7ada01 --- /dev/null +++ b/spec/support/fakes.rb @@ -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 diff --git a/spec/workers/asciicast_worker_spec.rb b/spec/workers/asciicast_worker_spec.rb new file mode 100644 index 0000000..1ede9db --- /dev/null +++ b/spec/workers/asciicast_worker_spec.rb @@ -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 diff --git a/spec/workers/snapshot_worker_spec.rb b/spec/workers/snapshot_worker_spec.rb deleted file mode 100644 index e457a96..0000000 --- a/spec/workers/snapshot_worker_spec.rb +++ /dev/null @@ -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