uh-wm 0.0.2.pre → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/Gemfile +5 -0
  6. data/Guardfile +12 -0
  7. data/LICENSE +30 -0
  8. data/README.md +68 -0
  9. data/Rakefile +40 -0
  10. data/bin/uhwm +5 -0
  11. data/config/cucumber.yaml +1 -0
  12. data/features/actions/execute.feature +9 -0
  13. data/features/actions/layout_delegation.feature +31 -0
  14. data/features/actions/quit.feature +9 -0
  15. data/features/cli/debug.feature +5 -0
  16. data/features/cli/layout.feature +15 -0
  17. data/features/cli/require.feature +5 -0
  18. data/features/cli/run_control.feature +9 -0
  19. data/features/cli/usage.feature +11 -0
  20. data/features/cli/verbose.feature +5 -0
  21. data/features/cli/version.feature +6 -0
  22. data/features/cli/worker.feature +9 -0
  23. data/features/layout/manage.feature +12 -0
  24. data/features/layout/protocol.feature +24 -0
  25. data/features/layout/unmanage.feature +10 -0
  26. data/features/manager/check_other_wm.feature +8 -0
  27. data/features/manager/input_events.feature +8 -0
  28. data/features/manager/manage.feature +14 -0
  29. data/features/manager/unmanage.feature +13 -0
  30. data/features/manager/x_errors.feature +17 -0
  31. data/features/run_control/evaluation.feature +18 -0
  32. data/features/run_control/key.feature +33 -0
  33. data/features/run_control/modifier.feature +10 -0
  34. data/features/run_control/worker.feature +9 -0
  35. data/features/session/connection.feature +5 -0
  36. data/features/session/termination.feature +13 -0
  37. data/features/steps/filesystem_steps.rb +3 -0
  38. data/features/steps/output_steps.rb +44 -0
  39. data/features/steps/run_control_steps.rb +3 -0
  40. data/features/steps/run_steps.rb +41 -0
  41. data/features/steps/x_steps.rb +53 -0
  42. data/features/support/env.rb +33 -0
  43. data/lib/uh/wm.rb +8 -0
  44. data/lib/uh/wm/actions_handler.rb +46 -0
  45. data/lib/uh/wm/cli.rb +20 -13
  46. data/lib/uh/wm/client.rb +64 -0
  47. data/lib/uh/wm/dispatcher.rb +3 -1
  48. data/lib/uh/wm/env.rb +15 -9
  49. data/lib/uh/wm/env_logging.rb +8 -0
  50. data/lib/uh/wm/logger_formatter.rb +16 -0
  51. data/lib/uh/wm/manager.rb +96 -14
  52. data/lib/uh/wm/run_control.rb +8 -3
  53. data/lib/uh/wm/runner.rb +82 -14
  54. data/lib/uh/wm/testing/acceptance_helpers.rb +140 -18
  55. data/lib/uh/wm/version.rb +1 -1
  56. data/lib/uh/wm/workers.rb +21 -0
  57. data/lib/uh/wm/workers/base.rb +27 -0
  58. data/lib/uh/wm/workers/blocking.rb +11 -0
  59. data/lib/uh/wm/workers/mux.rb +18 -0
  60. data/spec/spec_helper.rb +26 -0
  61. data/spec/support/exit_helpers.rb +6 -0
  62. data/spec/support/filesystem_helpers.rb +11 -0
  63. data/spec/uh/wm/actions_handler_spec.rb +30 -0
  64. data/spec/uh/wm/cli_spec.rb +214 -0
  65. data/spec/uh/wm/client_spec.rb +133 -0
  66. data/spec/uh/wm/dispatcher_spec.rb +76 -0
  67. data/spec/uh/wm/env_spec.rb +145 -0
  68. data/spec/uh/wm/manager_spec.rb +355 -0
  69. data/spec/uh/wm/run_control_spec.rb +102 -0
  70. data/spec/uh/wm/runner_spec.rb +186 -0
  71. data/uh-wm.gemspec +25 -0
  72. metadata +112 -9
@@ -1,7 +1,11 @@
1
+ require 'uh'
2
+
1
3
  module Uh
2
4
  module WM
3
5
  module Testing
4
6
  module AcceptanceHelpers
7
+ TIMEOUT_DEFAULT = 2
8
+
5
9
  def uhwm_run options = '-v'
6
10
  command = %w[uhwm]
7
11
  command << options if options
@@ -9,7 +13,10 @@ module Uh
9
13
  end
10
14
 
11
15
  def uhwm_ensure_stop
12
- @process and @process.terminate
16
+ if @process
17
+ x_key 'alt+shift+q'
18
+ @process.terminate
19
+ end
13
20
  end
14
21
 
15
22
  def uhwm_pid
@@ -20,33 +27,30 @@ module Uh
20
27
  @process.stdout
21
28
  end
22
29
 
23
- def uhwm_wait_output message, timeout: 1
24
- Timeout.timeout(timeout) do
25
- loop do
26
- break if case message
27
- when Regexp then @process.stdout + @process.stderr =~ message
28
- when String then assert_partial_output_interactive message
29
- end
30
- sleep 0.1
30
+ def uhwm_wait_output message
31
+ output = -> { @process.stdout + @process.stderr }
32
+ timeout_until do
33
+ case message
34
+ when Regexp then output.call =~ message
35
+ when String then output.call.include? message
31
36
  end
32
37
  end
33
- rescue Timeout::Error
34
- output = (@process.stdout + @process.stderr).lines
35
- .map { |e| " #{e}" }
36
- .join
38
+ rescue TimeoutError => e
37
39
  fail [
38
- "expected `#{message}' not seen after #{timeout} seconds in:",
39
- " ```\n#{output} ```"
40
+ "expected `#{message}' not seen after #{e.timeout} seconds in:",
41
+ " ```\n#{output.call.lines.map { |e| " #{e}" }.join} ```"
40
42
  ].join "\n"
41
43
  end
42
44
 
43
- def uhwm_run_wait_ready
44
- uhwm_run
45
+ def uhwm_run_wait_ready options = nil
46
+ if options then uhwm_run options else uhwm_run end
45
47
  uhwm_wait_output 'Connected to'
46
48
  end
47
49
 
48
50
  def with_other_wm
49
- @other_wm = ChildProcess.build('twm').tap { |o| o.start }
51
+ @other_wm = ChildProcess.build('twm')
52
+ @other_wm.start
53
+ yield
50
54
  @other_wm.stop
51
55
  end
52
56
 
@@ -54,6 +58,19 @@ module Uh
54
58
  @other_wm
55
59
  end
56
60
 
61
+ def x_client ident: :default
62
+ @x_clients ||= {}
63
+ @x_clients[ident] ||= XClient.new(ident)
64
+ end
65
+
66
+ def x_focused_window_id
67
+ Integer(`xdpyinfo`[/^focus:\s+window\s+(0x\h+)/, 1])
68
+ end
69
+
70
+ def x_input_event_masks
71
+ `xdpyinfo`[/current input event mask:\s+0x\h+([\w\s]+):/, 1].split(/\s+/).grep /Mask\z/
72
+ end
73
+
57
74
  def x_key key
58
75
  fail "cannot simulate X key `#{key}'" unless system "xdotool key #{key}"
59
76
  end
@@ -66,6 +83,111 @@ module Uh
66
83
  `sockstat -u`.lines.grep /\s+ruby.+\s+#{pid}/
67
84
  end.any?
68
85
  end
86
+
87
+ def x_window_id **options
88
+ x_client(options).window_id
89
+ end
90
+
91
+ def x_window_name
92
+ x_client.window_name
93
+ end
94
+
95
+ def x_window_map times: 1, **options
96
+ times.times { x_client(options).map }
97
+ x_client(options).sync
98
+ end
99
+
100
+ def x_window_map_state **options
101
+ `xwininfo -id #{x_window_id options}`[/Map State: (\w+)/, 1]
102
+ end
103
+
104
+ def x_window_unmap **options
105
+ x_client(options).unmap
106
+ x_client(options).sync
107
+ end
108
+
109
+ def x_window_destroy **options
110
+ x_client(options).destroy
111
+ x_client(options).sync
112
+ end
113
+
114
+ def x_clients_ensure_stop
115
+ @x_clients and @x_clients.any? and @x_clients.values.each &:terminate
116
+ end
117
+
118
+
119
+ private
120
+
121
+ def timeout_until
122
+ timeout = ENV.key?('UHWMTEST_TIMEOUT') ?
123
+ ENV['UHWMTEST_TIMEOUT'].to_i :
124
+ TIMEOUT_DEFAULT
125
+ Timeout.timeout(timeout) do
126
+ loop do
127
+ break if yield
128
+ sleep 0.1
129
+ end
130
+ end
131
+ rescue Timeout::Error
132
+ fail TimeoutError.new('execution expired', timeout)
133
+ end
134
+
135
+
136
+ class TimeoutError < ::StandardError
137
+ attr_reader :timeout
138
+
139
+ def initialize message, timeout
140
+ super message
141
+ @timeout = timeout
142
+ end
143
+ end
144
+
145
+ class XClient
146
+ attr_reader :name
147
+
148
+ def initialize name = object_id
149
+ @name = "#{self.class.name.split('::').last}/#{name}"
150
+ @geo = Geo.new(0, 0, 640, 480)
151
+ @display = Display.new.tap { |o| o.open }
152
+ end
153
+
154
+ def terminate
155
+ @display.close
156
+ end
157
+
158
+ def sync
159
+ @display.sync false
160
+ end
161
+
162
+ def window
163
+ @window ||= @display.create_window(@geo).tap do |o|
164
+ o.name = @name
165
+ end
166
+ end
167
+
168
+ def window_id
169
+ @window.id
170
+ end
171
+
172
+ def window_name
173
+ @name
174
+ end
175
+
176
+ def map
177
+ window.map
178
+ self
179
+ end
180
+
181
+ def unmap
182
+ window.unmap
183
+ self
184
+ end
185
+
186
+ def destroy
187
+ window.destroy
188
+ self
189
+ end
190
+ end
69
191
  end
70
192
  end
71
193
  end
data/lib/uh/wm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Uh
2
2
  module WM
3
- VERSION = '0.0.2.pre'
3
+ VERSION = '0.0.2'
4
4
  end
5
5
  end
@@ -0,0 +1,21 @@
1
+ module Uh
2
+ module WM
3
+ module Workers
4
+ FACTORIES = {
5
+ block: ->(options) { Blocking.new(options) },
6
+ mux: ->(options) { Mux.new(options) }
7
+ }.freeze
8
+
9
+ class << self
10
+ def types
11
+ FACTORIES.keys
12
+ end
13
+
14
+ def build type, **options
15
+ (FACTORIES[type] or fail ArgumentError, "unknown worker: `#{type}'")
16
+ .call options
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module Uh
2
+ module WM
3
+ module Workers
4
+ class Base
5
+ CALLBACKS = %w[before_wait on_timeout on_read on_read_next].freeze
6
+
7
+ def initialize **options
8
+ @ios = []
9
+ end
10
+
11
+ def watch io
12
+ @ios << io
13
+ end
14
+
15
+ CALLBACKS.each do |m|
16
+ define_method m do |*_, &block|
17
+ if block
18
+ instance_variable_set "@#{m}".to_sym, block
19
+ else
20
+ instance_variable_get "@#{m}".to_sym
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module Uh
2
+ module WM
3
+ module Workers
4
+ class Blocking < Base
5
+ def work_events
6
+ @on_read_next.call
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module Uh
2
+ module WM
3
+ module Workers
4
+ class Mux < Base
5
+ def initialize timeout: 1
6
+ super
7
+ @timeout = timeout
8
+ end
9
+
10
+ def work_events
11
+ @before_wait.call if @before_wait
12
+ if res = select(@ios, [], [], @timeout) then @on_read.call res
13
+ else @on_timeout.call if @on_timeout end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ require 'headless'
2
+
3
+ require 'uh/wm'
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |expectations|
7
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
8
+ end
9
+
10
+ config.mock_with :rspec do |mocks|
11
+ mocks.verify_partial_doubles = true
12
+ end
13
+
14
+ config.disable_monkey_patching!
15
+
16
+ config.before :all do
17
+ # Ensure current X display is not available from rspec test suite.
18
+ ENV.delete 'DISPLAY'
19
+ end
20
+
21
+ config.around :example, :xvfb do |example|
22
+ Headless.ly do
23
+ example.run
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ module ExitHelpers
2
+ def trap_exit
3
+ yield
4
+ rescue SystemExit
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ require 'tempfile'
2
+
3
+ module FileSystemHelpers
4
+ def with_file content
5
+ Tempfile.create('uhwm_rspec') do |f|
6
+ f.write content
7
+ f.rewind
8
+ yield f
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module Uh
2
+ module WM
3
+ RSpec.describe ActionsHandler do
4
+ let(:env) { Env.new(StringIO.new) }
5
+ let(:events) { Dispatcher.new }
6
+ subject(:actions) { described_class.new env, events }
7
+
8
+ describe '#evaluate' do
9
+ it 'evaluates given code' do
10
+ expect { actions.evaluate proc { throw :action_code } }
11
+ .to throw_symbol :action_code
12
+ end
13
+ end
14
+
15
+ describe '#quit' do
16
+ it 'emits the quit event' do
17
+ expect(events).to receive(:emit).with :quit
18
+ actions.quit
19
+ end
20
+ end
21
+
22
+ describe '#layout_*' do
23
+ it 'delegates messages to the layout with handle_ prefix' do
24
+ expect(env.layout).to receive :handle_screen_sel
25
+ actions.layout_screen_sel :succ
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,214 @@
1
+ require 'support/exit_helpers'
2
+
3
+ module Uh
4
+ module WM
5
+ RSpec.describe CLI do
6
+ include ExitHelpers
7
+
8
+ let(:stdout) { StringIO.new }
9
+ let(:stderr) { StringIO.new }
10
+ let(:arguments) { [] }
11
+ subject(:cli) { described_class.new arguments, stdout: stdout }
12
+
13
+ describe '.run' do
14
+ subject(:run) do
15
+ described_class.run arguments, stdout: stdout, stderr: stderr
16
+ end
17
+
18
+ # Prevent Runner from connecting a Manager and blocking.
19
+ before { allow(Runner).to receive :run }
20
+
21
+ it 'builds a new CLI with given arguments' do
22
+ expect(described_class)
23
+ .to receive(:new).with(arguments, stdout: stdout).and_call_original
24
+ run
25
+ end
26
+
27
+ it 'parses new CLI arguments' do
28
+ cli
29
+ allow(described_class).to receive(:new) { cli }
30
+ expect(cli).to receive :parse_arguments!
31
+ run
32
+ end
33
+
34
+ it 'runs new CLI' do
35
+ cli
36
+ allow(described_class).to receive(:new) { cli }
37
+ expect(cli).to receive :run
38
+ run
39
+ end
40
+
41
+ context 'with invalid arguments' do
42
+ let(:arguments) { %w[--unknown-option] }
43
+
44
+ it 'prints the usage on standard error stream' do
45
+ trap_exit { run }
46
+ expect(stderr.string).to match /\AUsage: .+/
47
+ end
48
+
49
+ it 'exits with a return status of 64' do
50
+ expect { run }.to raise_error(SystemExit) do |e|
51
+ expect(e.status).to eq 64
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when the new CLI raises a runtime error' do
57
+ before do
58
+ allow(cli).to receive(:run) { fail RuntimeError, 'some error' }
59
+ allow(described_class).to receive(:new) { cli }
60
+ end
61
+
62
+ it 'exits with a return status of 70' do
63
+ expect { run }.to raise_error(SystemExit) do |e|
64
+ expect(e.status).to eq 70
65
+ end
66
+ end
67
+
68
+ it 'formats the error' do
69
+ trap_exit { run }
70
+ expect(stderr.string)
71
+ .to match /\AUh::WM::RuntimeError: some error\n/
72
+ end
73
+
74
+ it 'does not output a backtrace' do
75
+ trap_exit { run }
76
+ expect(stderr.string).not_to include __FILE__
77
+ end
78
+
79
+ context 'when debug mode is enabled' do
80
+ let(:arguments) { %w[-d] }
81
+
82
+ it 'outputs a backtrace' do
83
+ trap_exit { run }
84
+ expect(stderr.string).to include __FILE__
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#initialize' do
91
+ it 'builds an env with given stdout' do
92
+ expect(cli.env.output).to be stdout
93
+ end
94
+
95
+ it 'syncs the output' do
96
+ expect(stdout).to receive(:sync=).with(true)
97
+ cli
98
+ end
99
+ end
100
+
101
+ describe '#run' do
102
+ it 'runs a runner with the env' do
103
+ expect(Runner).to receive(:run).with(cli.env)
104
+ cli.run
105
+ end
106
+ end
107
+
108
+ describe '#parse_arguments!' do
109
+ context 'with verbose option' do
110
+ let(:arguments) { %w[-v] }
111
+
112
+ it 'sets the env as verbose' do
113
+ cli.parse_arguments!
114
+ expect(cli.env).to be_verbose
115
+ end
116
+
117
+ it 'tells the env to log its logger level' do
118
+ expect(cli.env).to receive :log_logger_level
119
+ cli.parse_arguments!
120
+ end
121
+ end
122
+
123
+ context 'with debug option' do
124
+ let(:arguments) { %w[-d] }
125
+
126
+ it 'sets the env as debug' do
127
+ cli.parse_arguments!
128
+ expect(cli.env).to be_debug
129
+ end
130
+
131
+ it 'tells the env to log its logger level' do
132
+ expect(cli.env).to receive :log_logger_level
133
+ cli.parse_arguments!
134
+ end
135
+ end
136
+
137
+ context 'with run control option' do
138
+ let(:arguments) { %w[-f uhwmrc.rb] }
139
+
140
+ it 'assigns run control file path in the env' do
141
+ cli.parse_arguments!
142
+ expect(cli.env.rc_path).to eq 'uhwmrc.rb'
143
+ end
144
+ end
145
+
146
+ context 'with require option' do
147
+ let(:arguments) { %w[-r abbrev] }
148
+
149
+ it 'requires the given ruby feature' do
150
+ expect { cli.parse_arguments! }
151
+ .to change { $LOADED_FEATURES.grep(/abbrev/).any? }
152
+ .from(false).to(true)
153
+ end
154
+ end
155
+
156
+ context 'with layout option' do
157
+ let(:arguments) { %w[-l Object] }
158
+
159
+ it 'assigns the layout class in the env' do
160
+ cli.parse_arguments!
161
+ expect(cli.env.layout_class).to eq Object
162
+ end
163
+ end
164
+
165
+ context 'with worker option' do
166
+ let(:arguments) { %w[-w mux] }
167
+
168
+ it 'assigns the worker type in the env' do
169
+ cli.parse_arguments!
170
+ expect(cli.env.worker).to eq :mux
171
+ end
172
+ end
173
+
174
+ context 'with help option' do
175
+ let(:arguments) { %w[-h] }
176
+
177
+ it 'prints the usage banner on standard output' do
178
+ trap_exit { cli.parse_arguments! }
179
+ expect(stdout.string).to match /\AUsage: .+/
180
+ end
181
+
182
+ it 'prints options usage on standard output' do
183
+ trap_exit { cli.parse_arguments! }
184
+ expect(stdout.string).to match /\n^options:\n\s+-/
185
+ end
186
+ end
187
+
188
+ context 'with version option' do
189
+ let(:arguments) { %w[-V] }
190
+
191
+ it 'prints the version on standard output' do
192
+ trap_exit { cli.parse_arguments! }
193
+ expect(stdout.string).to eq "#{::Uh::WM::VERSION}\n"
194
+ end
195
+
196
+ it 'exits with a return status of 0' do
197
+ expect { cli.parse_arguments! }.to raise_error(SystemExit) do |e|
198
+ expect(e.status).to eq 0
199
+ end
200
+ end
201
+ end
202
+
203
+ context 'with invalid option' do
204
+ let(:arguments) { %w[--unknown-option] }
205
+
206
+ it 'raises a CLI::ArgumentError' do
207
+ expect { cli.parse_arguments! }
208
+ .to raise_error CLI::ArgumentError
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end