Class: ShellTest::ShellMethods::Session

Inherits:
Object
  • Object
show all
Includes:
Utils
Defined in:
lib/shell_test/shell_methods/session.rb

Overview

Session is an engine for running shell sessions.

Constant Summary collapse

DEFAULT_SHELL =
'/bin/sh'
DEFAULT_STTY =
'-echo -onlcr'
DEFAULT_MAX_RUN_TIME =
1

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils

spawn

Constructor Details

#initialize(options = {}) ⇒ Session

Returns a new instance of Session.



38
39
40
41
42
43
44
45
46
# File 'lib/shell_test/shell_methods/session.rb', line 38

def initialize(options={})
  @shell = options[:shell] || DEFAULT_SHELL
  @stty  = options[:stty]  || DEFAULT_STTY
  @timer = options[:timer] || Timer.new
  @max_run_time = options[:max_run_time] || DEFAULT_MAX_RUN_TIME
  @steps   = [[nil, nil, nil, nil]]
  @log     = []
  @status  = nil
end

Instance Attribute Details

#logObject (readonly)

A log of the output at each step (set during run)



33
34
35
# File 'lib/shell_test/shell_methods/session.rb', line 33

def log
  @log
end

#max_run_timeObject (readonly)

The maximum run time for the session



26
27
28
# File 'lib/shell_test/shell_methods/session.rb', line 26

def max_run_time
  @max_run_time
end

#shellObject (readonly)

The session shell



17
18
19
# File 'lib/shell_test/shell_methods/session.rb', line 17

def shell
  @shell
end

#statusObject (readonly)

A Process::Status for the session (set by run)



36
37
38
# File 'lib/shell_test/shell_methods/session.rb', line 36

def status
  @status
end

#stepsObject (readonly)

An array of entries like [prompt, input, max_run_time, callback] that indicate each step of a session. See the on method for adding steps.



30
31
32
# File 'lib/shell_test/shell_methods/session.rb', line 30

def steps
  @steps
end

#sttyObject (readonly)

Aguments string passed stty on run



20
21
22
# File 'lib/shell_test/shell_methods/session.rb', line 20

def stty
  @stty
end

#timerObject (readonly)

The session timer, used by agents to determine timeouts



23
24
25
# File 'lib/shell_test/shell_methods/session.rb', line 23

def timer
  @timer
end

Instance Method Details

#on(prompt, input = nil, max_run_time = nil, &callback) ⇒ Object

Define a step. At each step:

  1. The session waits until the prompt is matched

  2. The input is written to the shell (if given)

  3. The output passed to the callback (if given)

If the next prompt (or an EOF if there is no next prompt) is not reached within max_run_time then a ReadError occurs. Special considerations:

  • The prompt can be a regular expression, a string, or a Symbol (set to the same ENV value - ex :PS1)

  • A nil max_run_time indicates no maximum run time - which more accurately means the input can go until the overall max_run_time for the session runs out.

  • The output passed to the callback will include the string matched by the next prompt, if present.

Returns self.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/shell_test/shell_methods/session.rb', line 77

def on(prompt, input=nil, max_run_time=nil, &callback) # :yields: output
  if prompt.kind_of?(Symbol)
    prompt = ENV[prompt.to_s]
  end

  if prompt.nil?
    raise ArgumentError, "no prompt specified"
  end

  # Stagger assignment of step arguments so that the callback will be
  # recieve the output of the input. Not only is this more intuitive, it
  # ensures the last step will read to EOF (which expedites waiting on
  # the session to terminate).
  last = steps.last
  last[0] = prompt
  last[1] = input
  last[2] = max_run_time

  steps << [nil, nil, nil, callback]
  self
end

#parse(script, options = {}, &block) ⇒ Object

Parses a terminal snippet into steps that a Session can run, and adds those steps to self. The snippet should utilize ps1 and ps2 as set on self. An exit command is added unless the :noexit option is set to true.

session = Session.new
session.parse %{
$ echo abc
abc
}
session.run.result   # => "$ echo abc\nabc\n$ exit\nexit\n"

Steps are registered with a callback block, if given, to recieve the expected and actual outputs during run. Normally the callback is used to validate that the run is going as planned.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/shell_test/shell_methods/session.rb', line 153

def parse(script, options={}, &block)
  args = split(script)
  args.shift # ignore script before first prompt

  if options[:noexit]
    args.pop
  else
    args.last << ps1 if args.last
    args.concat [ps1, "exit\n", nil, nil]
  end

  while !args.empty?
    prompt = args.shift
    input  = args.shift
    max_run_time = args.shift
    output = args.shift
    callback = make_callback(output, &block)

    on(prompt, input, max_run_time, &callback)
  end

  self
end

#ps1Object

The shell PS1, as configured in ENV.



49
50
51
# File 'lib/shell_test/shell_methods/session.rb', line 49

def ps1
  ENV['PS1']
end

#ps2Object

The shell PS2, as configured in ENV.



54
55
56
# File 'lib/shell_test/shell_methods/session.rb', line 54

def ps2
  ENV['PS2']
end

#resultObject

Returns what would appear to the user at the current point in the session (with granularity of an input/output step).

Currently result ONLY works as intended when stty is set to turn off input echo and output carriage returns, either with ‘-echo -onlcr’ (the default) or ‘raw’. Otherwise the inputs can appear twice in the result and there will be inconsistent end-of-lines.



251
252
253
# File 'lib/shell_test/shell_methods/session.rb', line 251

def result
  log.join
end

#runObject

Runs each of steps within a shell session and collects the inputs/outputs into log. After run the exit status of the session is set into status.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/shell_test/shell_methods/session.rb', line 223

def run
  spawn do |agent|
    timeout  = nil
    steps.each do |prompt, input, max_run_time, callback|
      buffer = agent.expect(prompt, timeout)
      log << buffer

      if callback
        callback.call buffer
      end

      if input
        log << input
        agent.write(input)
      end

      timeout = max_run_time
    end
  end
end

#spawnObject

Spawns a PTY shell session and yields an Agent to the block. The session is logged to log and the final exit status set into status (any previous values are overwritten).



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/shell_test/shell_methods/session.rb', line 180

def spawn
  @log = []
  @status = super(shell) do |master, slave|
    agent = Agent.new(master, slave, timer)
    timer.start(max_run_time)

    if stty
      # It would be lovely to work this into steps somehow, or to set
      # the stty externally like:
      #
      #   system("stty #{stty} < '#{master.path}'")
      #
      # Unfortunately the former complicates result and the latter
      # doesn't work.  In tests the stty settings DO get set but they
      # don't refresh in the pty.
      log << agent.on(ps1, "stty #{stty}\n")
      log << agent.on(ps1, "echo $?\n")
      log << agent.on(ps1, "\n")

      unless log.last == "0\n#{ps1}"
        raise "stty failure\n#{summary}"
      end

      log.clear
    end

    begin
      yield agent
    rescue Agent::ReadError
      log << $!.buffer
      $!.message << "\n#{summary}"
      raise
    end

    timer.stop
    agent.close
  end
  self
end

#split(str) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/shell_test/shell_methods/session.rb', line 99

def split(str)
  scanner = StringScanner.new(str)
  scanner.scan_until(/(#{Regexp.escape(ps1)})/)
  scanner.pos -= scanner[1].to_s.length

  args = []
  while output = scanner.scan_until(/(#{Regexp.escape(ps1)}|#{Regexp.escape(ps2)}|\{\{(.*?)\}\})/)
    match = scanner[1]
    input = scanner[2] ? "#{scanner[2]}\n" : scanner.scan_until(/\n/)

    max_run_time = -1
    input.sub!(/\#\s*\[(\d+(?:\.\d+)?)\].*$/) do
      max_run_time = $1.to_f
      nil
    end

    case match
    when ps1
      prompt = match
      if max_run_time == -1
        max_run_time = nil
      end
    when ps2
      prompt = match
    else
      output = output.chomp(match)
      prompt = output.split("\n").last
    end

    args << output
    args << prompt
    args << input
    args << max_run_time
  end

  args << scanner.rest
  args
end

#summary(format = nil) ⇒ Object

Formats the status of self into a string. A format string can be provided - it is evaluated using ‘%’ using arguments: [shell, elapsed_time, result]



258
259
260
261
262
263
264
265
# File 'lib/shell_test/shell_methods/session.rb', line 258

def summary(format=nil)
  (format || %Q{
%s (elapsed: %.2fs max: %.2fs)
=========================================================
%s
=========================================================
}) % [shell, timer.elapsed_time, max_run_time, result]
end