ttytest 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d0175fa2266c1f56f709051162f3419862a24355
4
- data.tar.gz: 949439a6f922cd8b71c8c92e10a9721161c152b7
3
+ metadata.gz: 91adbf5abccf48f1626c908ebbccc8ad13bcc67b
4
+ data.tar.gz: f244bd9c2970b4af1704ac84f6bf1a6fb2b8975e
5
5
  SHA512:
6
- metadata.gz: 8af22aa0fb0f3896c113c7e5cb5d929c82e642c44d93e03f5b75729fc3d1e2a2eb5f796f6e09300f6d21bc4d5bc1b70e6a7c3676e0dd0f5bc4ff7d6e91a52c8d
7
- data.tar.gz: 7871021b99ebef74812db882e8ada619135697c114c6df31c19ed282dec13a155237d6162c467cab6f440887ebbf76d43a93eedd9812d3eef139ca47acf00c65
6
+ metadata.gz: fe2ec46d18232b2e0e16c54d827a8efba1628d114cc4c42a78254a114cfd9eb8e9ec009074d298de95fafb8820a2ee283b816b1cce59ab0f1eea4070b57e0025
7
+ data.tar.gz: 5e23a41408ff1cc4840e33a4049258434b16f17c3614994c4850df1e9adafda0dc9c1cdb57430035b3ebdf1d504b75d43df8301e32a53ebe7d2706ed03fe0f5b
data/.gitignore CHANGED
@@ -1,2 +1,4 @@
1
1
  Gemfile.lock
2
2
  pkg/
3
+ .yardoc/
4
+ doc/
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
- TTYtest is an integration test framework for interactive console applications. It's like [capybara](https://github.com/teamcapybara/capybara) for the terminal.
1
+ TTYtest is an acceptance test framework for interactive console applications. It's like [capybara](https://github.com/teamcapybara/capybara) for the terminal.
2
2
 
3
3
  It works by running commands inside a tmux session, capturing the pane, and comparing the content. The assertions will wait a specified amount of time (default 2 seconds) for the expected content to appear.
4
4
 
5
5
  [![Build Status](https://travis-ci.org/jhawthorn/ttytest.svg?branch=master)](https://travis-ci.org/jhawthorn/ttytest)
6
+ [![Gem Version](https://badge.fury.io/rb/ttytest.svg)](https://rubygems.org/gems/ttytest)
6
7
 
7
8
  ## Requirements
8
9
 
@@ -11,6 +12,8 @@ It works by running commands inside a tmux session, capturing the pane, and comp
11
12
 
12
13
  ## Usage
13
14
 
15
+ ### Example
16
+
14
17
  ``` ruby
15
18
  @tty = TTYtest.new_terminal(%{PS1='$ ' /bin/sh}, width: 80, height: 24)
16
19
  @tty.assert_row(0, '$')
@@ -18,8 +21,11 @@ It works by running commands inside a tmux session, capturing the pane, and comp
18
21
 
19
22
  @tty.send_keys(%{echo "Hello, world"\n})
20
23
 
21
- @tty.assert_row(0, '$ echo "Hello, world"')
22
- @tty.assert_row(1, 'Hello, world')
24
+ @tty.assert_contents <<TTY
25
+ $ echo "Hello, world"
26
+ Hello, world
27
+ $
28
+ TTY
23
29
  @tty.assert_cursor_position(x: 2, y: 2)
24
30
 
25
31
  p @tty.rows # => ["$ echo \"Hello, world\"", "Hello, world", "$", "", "", "", ...]
@@ -27,6 +33,17 @@ p @tty.rows # => ["$ echo \"Hello, world\"", "Hello, world", "$", "", "", "", ..
27
33
 
28
34
  See also [fzy's integration test](https://github.com/jhawthorn/fzy/blob/master/test/integration/integration_test.rb) for a full example.
29
35
 
36
+ ### Assertions
37
+
38
+ The main way to use TTYtest is through assertions. When called on a `TTYtest::Terminal`, each of these will be retried (for up to 2 seconds by default).
39
+
40
+ Available assertions:
41
+ * `assert_row(row_number, expected_text)`
42
+ * `assert_cursor_position(x: x, y: y)`
43
+ * `assert_cursor_visible`
44
+ * `assert_cursor_hidden`
45
+ * `assert_contents(lines_of_terminal)`
46
+
30
47
  ## TravisCI
31
48
 
32
49
  TTYtest can run on [TravisCI](https://travis-ci.org/), but the version of tmux made available with their default ubuntu 12.04 environment is too old. However the TravisCI ubuntu 14.04 "trusty" image provides tmux 1.8, which works great.
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
+ require "yard"
3
4
 
4
5
  Rake::TestTask.new(:test) do |t|
5
6
  t.libs << "test"
@@ -7,6 +8,10 @@ Rake::TestTask.new(:test) do |t|
7
8
  t.test_files = FileList['test/**/*_test.rb']
8
9
  end
9
10
 
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = ['lib/**/*.rb']
13
+ end
14
+
10
15
  task :console do
11
16
  system 'irb -Ilib -rttytest'
12
17
  end
@@ -8,6 +8,12 @@ module TTYtest
8
8
  attr_accessor :default_max_wait_time
9
9
 
10
10
  extend Forwardable
11
+ # @!method new_terminal(command, width: 80, height: 24)
12
+ # Create a new terminal through the current driver.
13
+ # @param [String] command a valid shell command to run
14
+ # @param [Integer] width width of the new terminal
15
+ # @param [Integer] height height of the new terminal
16
+ # @return [Terminal] a new terminal running the specified command
11
17
  def_delegators :driver, :new_terminal
12
18
  end
13
19
 
@@ -1,10 +1,17 @@
1
1
  module TTYtest
2
+ # Represents the complete state of a {TTYtest::Terminal} at the time it was captured (contents, cursor position, etc).
3
+ # @attr_reader [Integer] width the number of columns in the captured terminal
4
+ # @attr_reader [Integer] height the number of rows in the captured terminal
5
+ # @attr_reader [Integer] cursor_x the cursor's column (starting at 0) in the captured terminal
6
+ # @attr_reader [Integer] cursor_y the cursor's row (starting at 0) in the captured terminal
2
7
  class Capture
3
8
  include TTYtest::Matchers
4
9
 
5
10
  attr_reader :cursor_x, :cursor_y
6
11
  attr_reader :width, :height
7
12
 
13
+ # Used internally by drivers when called by {Terminal#capture}
14
+ # @api private
8
15
  def initialize(contents, cursor_x: 0, cursor_y: 0, width: nil, height: nil, cursor_visible: true)
9
16
  @rows = (contents+"\nEND").split("\n")[0...-1].map do |row|
10
17
  row || ""
@@ -16,26 +23,33 @@ module TTYtest
16
23
  @cursor_visible = cursor_visible
17
24
  end
18
25
 
26
+ # @return [Array<String>] An array of each row's contend from the captured terminal
19
27
  def rows
20
28
  @rows
21
29
  end
22
30
 
23
- def row(row)
24
- rows[row]
31
+ # @param [Integer] the row to return
32
+ # @return [String] the content of the row from the captured terminal
33
+ def row(row_number)
34
+ rows[row_number]
25
35
  end
26
36
 
37
+ # @return [true,false] Whether the cursor is visible in the captured terminal
27
38
  def cursor_visible?
28
39
  @cursor_visible
29
40
  end
30
41
 
42
+ # @return [true,false] Whether the cursor is hidden in the captured terminal
31
43
  def cursor_hidden?
32
44
  !cursor_visible?
33
45
  end
34
46
 
47
+ # @return [Capture] returns self
35
48
  def capture
36
49
  self
37
50
  end
38
51
 
52
+ # @return [String] All rows of the captured terminal, separated by newlines
39
53
  def to_s
40
54
  rows.join("\n")
41
55
  end
@@ -1,12 +1,21 @@
1
1
  module TTYtest
2
2
  module Matchers
3
+ # Asserts the contents of a single row
4
+ # @param [Integer] row_number the row (starting from 0) to test against
5
+ # @param [String] expected the expected value of the row. Any trailing whitespace is ignored
6
+ # @raise [MatchError] if the row doesn't match
3
7
  def assert_row(row_number, expected)
8
+ expected = expected.rstrip
4
9
  actual = row(row_number)
5
10
  if actual != expected
6
11
  raise MatchError, "expected row #{row_number} to be #{expected.inspect} but got #{actual.inspect}\nEntire screen:\n#{to_s}"
7
12
  end
8
13
  end
9
14
 
15
+ # Asserts that the cursor is in the expected position
16
+ # @param [Integer] x cursor x (row) position, starting from 0
17
+ # @param [Integer] y cursor y (column) position, starting from 0
18
+ # @raise [MatchError] if the cursor position doesn't match
10
19
  def assert_cursor_position(x:, y:)
11
20
  expected = [x, y]
12
21
  actual = [cursor_x, cursor_y]
@@ -15,19 +24,24 @@ module TTYtest
15
24
  end
16
25
  end
17
26
 
27
+ # @raise [MatchError] if the cursor is hidden
18
28
  def assert_cursor_visible
19
29
  if !cursor_visible?
20
30
  raise MatchError, "expected cursor to be visible was hidden\nEntire screen:\n#{to_s}"
21
31
  end
22
32
  end
23
33
 
34
+ # @raise [MatchError] if the cursor is visible
24
35
  def assert_cursor_hidden
25
36
  if !cursor_hidden?
26
37
  raise MatchError, "expected cursor to be hidden was visible\nEntire screen:\n#{to_s}"
27
38
  end
28
39
  end
29
40
 
30
- def assert_matches(expected)
41
+ # Asserts the full contents of the terminal
42
+ # @param [String] expected the full expected contents of the terminal. Trailing whitespace on each line is ignored
43
+ # @raise [MatchError] if the terminal doesn't match the expected content
44
+ def assert_contents(expected)
31
45
  expected_rows = expected.split("\n")
32
46
  diff = []
33
47
  matched = true
@@ -46,6 +60,7 @@ module TTYtest
46
60
  raise MatchError, "screen did not match expected content:\n--- expected\n+++ actual\n#{diff.join("\n")}"
47
61
  end
48
62
  end
63
+ alias_method :assert_matches, :assert_contents
49
64
 
50
65
  METHODS = public_instance_methods
51
66
  end
@@ -3,20 +3,52 @@ require 'ttytest/matchers'
3
3
  require 'ttytest/capture'
4
4
 
5
5
  module TTYtest
6
+ # @attr [Integer] max_wait_time the maximum amount of time (in seconds) to retry assertions before failing.
6
7
  class Terminal
7
8
  extend Forwardable
8
9
 
10
+ attr_accessor :max_wait_time
11
+
12
+ # @api private
13
+ # @see TTYtest.new_terminal
9
14
  def initialize(driver_terminal, max_wait_time: nil)
10
15
  @driver_terminal = driver_terminal
11
- @max_wait_time = max_wait_time
16
+ @max_wait_time = max_wait_time || TTYtest.default_max_wait_time
12
17
  end
13
18
 
14
- def max_wait_time
15
- @max_wait_time || TTYtest.default_max_wait_time
16
- end
19
+ # @!method send_keys(*keys)
20
+ # Simulate typing keys into the terminal
21
+ # @param [String] keys keys to send to the terminal
22
+ # @!method capture
23
+ # Capture the current state of the terminal
24
+ # @return [Capture] instantaneous state of the terminal when called
25
+ def_delegators :@driver_terminal, :send_keys, :capture
17
26
 
18
- def_delegators :@driver_terminal, :send_keys, :send_raw, :capture
19
- def_delegators :capture, :rows, :row, :width, :height, :cursor_visible?, :cursor_hidden?
27
+ # @!method rows
28
+ # @return [Array<String>]
29
+ # @see Capture#rows
30
+ # @!method row(row)
31
+ # @return [String]
32
+ # @see Capture#row
33
+ # @!method width
34
+ # @see Capture#width
35
+ # @return [Integer]
36
+ # @!method height
37
+ # @see Capture#height
38
+ # @return [Integer]
39
+ # @!method cursor_x
40
+ # @see Capture#cursor_x
41
+ # @return [Integer]
42
+ # @!method cursor_y
43
+ # @see Capture#cursor_y
44
+ # @return [Integer]
45
+ # @!method cursor_visible?
46
+ # @see Capture#cursor_visible?
47
+ # @return [true,false]
48
+ # @!method cursor_hidden?
49
+ # @see Capture#cursor_hidden?
50
+ # @return [true,false]
51
+ def_delegators :capture, :rows, :row, :width, :height, :cursor_x, :cursor_y, :cursor_visible?, :cursor_hidden?
20
52
 
21
53
  TTYtest::Matchers::METHODS.each do |matcher_name|
22
54
  define_method matcher_name do |*args|
@@ -12,26 +12,33 @@ module TTYtest
12
12
  COMMAND = 'tmux'
13
13
  SOCKET_NAME = 'ttytest'
14
14
  REQUIRED_TMUX_VERSION = '1.8'
15
- CONF_PATH = File.expand_path('../tmux.conf', __FILE__)
15
+ DEFAULT_CONFING_FILE_PATH = File.expand_path('../tmux.conf', __FILE__)
16
16
  SLEEP_INFINITY = 'read x < /dev/fd/1'
17
17
 
18
18
  class TmuxError < StandardError; end
19
19
 
20
- def initialize(debug: false, command: COMMAND, socket_name: SOCKET_NAME)
20
+ def initialize(
21
+ debug: false,
22
+ command: COMMAND,
23
+ socket_name: SOCKET_NAME,
24
+ config_file_path: DEFAULT_CONFING_FILE_PATH
25
+ )
21
26
  @debug = debug
22
27
  @tmux_cmd = command
23
28
  @socket_name = socket_name
29
+ @config_file_path = config_file_path
24
30
  end
25
31
 
26
32
  def new_terminal(cmd, width: 80, height: 24)
27
33
  cmd = "#{cmd}\n#{SLEEP_INFINITY}"
28
34
 
29
35
  session_name = "ttytest-#{SecureRandom.uuid}"
30
- tmux(*%W[-f #{CONF_PATH} new-session -s #{session_name} -d -x #{width} -y #{height} #{cmd}])
36
+ tmux(*%W[-f #{@config_file_path} new-session -s #{session_name} -d -x #{width} -y #{height} #{cmd}])
31
37
  session = Session.new(self, session_name)
32
38
  Terminal.new(session)
33
39
  end
34
40
 
41
+ # @api private
35
42
  def tmux(*args)
36
43
  ensure_available
37
44
  puts "tmux(#{args.inspect[1...-1]})" if debug?
@@ -45,18 +52,18 @@ module TTYtest
45
52
  @available ||= (Gem::Version.new(tmux_version) >= Gem::Version.new(REQUIRED_TMUX_VERSION))
46
53
  end
47
54
 
55
+ private
56
+
48
57
  def debug?
49
58
  @debug
50
59
  end
51
60
 
52
- private
53
-
54
61
  def ensure_available
55
62
  if !available?
56
63
  if !tmux_version
57
- raise TmuxError, "tmux doesn't seem to be unstalled" unless available?
64
+ raise TmuxError, "Running `tmux -V` to determine version failed. Is tmux installed?"
58
65
  else
59
- raise TmuxError, "tmux version #{tmux_version} does not meet requirement >= #{REQUIRED_TMUX_VERSION}" unless available?
66
+ raise TmuxError, "tmux version #{tmux_version} does not meet requirement >= #{REQUIRED_TMUX_VERSION}"
60
67
  end
61
68
  end
62
69
  end
@@ -3,8 +3,7 @@
3
3
  module TTYtest
4
4
  module Tmux
5
5
  class Session
6
- attr_reader :driver, :name
7
-
6
+ # @api private
8
7
  def initialize(driver, name)
9
8
  @driver = driver
10
9
  @name = name
@@ -12,14 +11,20 @@ module TTYtest
12
11
  ObjectSpace.define_finalizer(self, self.class.finalize(driver, name))
13
12
  end
14
13
 
14
+ # @api private
15
15
  def self.finalize(driver, name)
16
16
  proc { driver.tmux(*%W[kill-session -t #{name}]) }
17
17
  end
18
18
 
19
19
  def capture
20
20
  contents = driver.tmux(*%W[capture-pane -t #{name} -p])
21
- str = driver.tmux(*%W[display-message -t #{name} -p #\{cursor_x},#\{cursor_y},#\{cursor_flag},#\{pane_width},#\{pane_height}])
22
- x, y, cursor_flag, width, height = str.split(',')
21
+ str = driver.tmux(*%W[display-message -t #{name} -p #\{cursor_x},#\{cursor_y},#\{cursor_flag},#\{pane_width},#\{pane_height},#\{pane_dead},#\{pane_dead_status},])
22
+ x, y, cursor_flag, width, height, pane_dead, pane_dead_status, _newline = str.split(',')
23
+
24
+ if pane_dead == "1"
25
+ raise Driver::TmuxError, "Tmux pane has died\nCommand exited with status: #{pane_dead_status}\nEntire screen:\n#{contents}"
26
+ end
27
+
23
28
  TTYtest::Capture.new(
24
29
  contents.chomp("\n"),
25
30
  cursor_x: x.to_i,
@@ -33,6 +38,10 @@ module TTYtest
33
38
  def send_keys(*keys)
34
39
  driver.tmux(*%W[send-keys -t #{name} -l], *keys)
35
40
  end
41
+
42
+ private
43
+
44
+ attr_reader :driver, :name
36
45
  end
37
46
  end
38
47
  end
@@ -1 +1,3 @@
1
1
  set -g status off
2
+ set -g remain-on-exit on
3
+ set -g default-shell /bin/sh
@@ -1,3 +1,3 @@
1
1
  module TTYtest
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "bundler", "~> 1.7"
27
27
  spec.add_development_dependency "rake", "~> 10.0"
28
28
  spec.add_development_dependency "minitest", "~> 5.0"
29
+ spec.add_development_dependency "yard"
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ttytest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-31 00:00:00.000000000 Z
11
+ date: 2017-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: TTYtest allows running applications inside of a terminal emulator (like
56
70
  tmux) and making assertions on the output.
57
71
  email:
@@ -67,7 +81,6 @@ files:
67
81
  - Rakefile
68
82
  - lib/ttytest.rb
69
83
  - lib/ttytest/capture.rb
70
- - lib/ttytest/dummy.rb
71
84
  - lib/ttytest/matchers.rb
72
85
  - lib/ttytest/terminal.rb
73
86
  - lib/ttytest/tmux/driver.rb
@@ -1,13 +0,0 @@
1
- module TTYtest
2
- class Dummy
3
- attr_accessor :contents
4
-
5
- def initialize
6
- @contents = "\n"*23
7
- end
8
-
9
- def capture
10
- Capture.new(contents)
11
- end
12
- end
13
- end