uh-wm 0.0.2.pre → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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