yamatanooroti 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +96 -0
- data/lib/yamatanooroti.rb +89 -0
- data/lib/yamatanooroti/version.rb +3 -0
- data/lib/yamatanooroti/vterm.rb +70 -0
- data/lib/yamatanooroti/windows.rb +436 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3f7c72bc6c902dd8b5cdf5d02183954cba1180960c702e5642c208b074e2ccd5
|
4
|
+
data.tar.gz: d5dfea77ef4a055e0e278fa7447d96e1c473ce7aa4108062c99e3fc1d3e3d499
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d4242ac34c419417a1e8035968b26f4587d6d5c332852a73dad9d413b0b06447b1cd0969272b344bd3bc4a26fc4934e2a156cfc740d44fe53344f4d1f130048a
|
7
|
+
data.tar.gz: 1f2679e02d8183d15c5ded5baa273d6ca1b4b299335145a4141066c3f315a0277623ca64caa1e0f5bdffe205280ce2b9b27cfe7b1f728c71e03f8293d7211b1c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 aycabta
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Yamatanooroti
|
2
|
+
|
3
|
+
Yamatanooroti is a multi-platform real(?) terminal testing framework.
|
4
|
+
|
5
|
+
Supporting envronments:
|
6
|
+
|
7
|
+
- vterm gem
|
8
|
+
- Windows command prompt
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
You can test the executed result and its rendering on the automatically detected environment that you have at the time, by code below:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'yamatanooroti'
|
16
|
+
|
17
|
+
class MyTest < Yamatanooroti::TestCase
|
18
|
+
def setup
|
19
|
+
start_terminal(5, 30, ['irb', '-f', '--multiline'])
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_example
|
23
|
+
write(":a\n")
|
24
|
+
close
|
25
|
+
assert_screen(['irb(main):001:0> :a', '=> :a', 'irb(main):002:0>', '', ''])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
This code detects some real(?) terminal environments:
|
31
|
+
|
32
|
+
- vterm gem (you should install beforehand)
|
33
|
+
- Windows (you should run on command prompt)
|
34
|
+
|
35
|
+
If any real(?) terminal environments not found, it will fail with a message:
|
36
|
+
|
37
|
+
```
|
38
|
+
$ rake
|
39
|
+
Traceback (most recent call last):
|
40
|
+
(snip traceback)
|
41
|
+
/path/to/yamatanooroti/lib/yamatanooroti.rb:71:in `inherited': Any real(?) terminal environments not found. (LoadError)
|
42
|
+
Supporting real(?) terminals:
|
43
|
+
- vterm gem
|
44
|
+
- Windows
|
45
|
+
rake aborted!
|
46
|
+
Command failed with status (1)
|
47
|
+
|
48
|
+
Tasks: TOP => default => test
|
49
|
+
(See full trace by running task with --trace)
|
50
|
+
```
|
51
|
+
|
52
|
+
### Advanced Usage
|
53
|
+
|
54
|
+
If you want to specify vterm environment that needs vterm gem, you can use `Yamatanooroti::VTermTestCase`:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'yamatanooroti'
|
58
|
+
|
59
|
+
class MyTest < Yamatanooroti::VTermTestCase
|
60
|
+
def setup
|
61
|
+
start_terminal(5, 30, ['irb', '-f', '--multiline'])
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_example
|
65
|
+
write(":a\n")
|
66
|
+
close
|
67
|
+
assert_screen(['irb(main):001:0> :a', '=> :a', 'irb(main):002:0>', '', ''])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
If you haven't installed vterm gem, this code will fail with a message <q>You need vterm gem for Yamatanooroti::VTermTestCase (LoadError)</q>.
|
73
|
+
|
74
|
+
Likewise, you can specify Windows command prompt test by `Yamatanooroti::WindowsTestCase`.
|
75
|
+
|
76
|
+
## Method Reference
|
77
|
+
|
78
|
+
### `start_terminal(height, width, command)`
|
79
|
+
|
80
|
+
Starts terminal internally that is sized `height` and `width` with `command` to test the result. The `command` should be an array of strings with a path of command and zero or more options. This should be called in `setup` method.
|
81
|
+
|
82
|
+
### `write(str)`
|
83
|
+
|
84
|
+
Writes `str` like inputting by a keyboard to the started terminal.
|
85
|
+
|
86
|
+
### `close`
|
87
|
+
|
88
|
+
Closes the terminal to take the internal rendering result. You must call it before call assertions.
|
89
|
+
|
90
|
+
### `assert_screen(expected_lines)`
|
91
|
+
|
92
|
+
Asserts the rendering result of the terminal with `expected_lines` that should be an `Array` or a `String` of lines. The `Array` contains blank lines and doesn't contain newline characters, and the `String` contains newline characters at end of each line and doesn't contain continuous last blank lines.
|
93
|
+
|
94
|
+
## License
|
95
|
+
|
96
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'test-unit'
|
2
|
+
|
3
|
+
class Yamatanooroti
|
4
|
+
def self.load_vterm
|
5
|
+
require 'vterm'
|
6
|
+
require 'yamatanooroti/vterm'
|
7
|
+
rescue LoadError
|
8
|
+
raise LoadError.new('You need vterm gem for Yamatanooroti::VTermTestCase')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load_windows
|
12
|
+
unless win?
|
13
|
+
raise LoadError.new('You need Windows environment for Yamatanooroti::WindowsTestCase')
|
14
|
+
end
|
15
|
+
require 'yamatanooroti/windows'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.const_missing(id)
|
19
|
+
case id
|
20
|
+
when :VTermTestCase
|
21
|
+
load_vterm
|
22
|
+
Yamatanooroti::VTermTestCase
|
23
|
+
when :VTermTestCaseModule
|
24
|
+
load_vterm
|
25
|
+
Yamatanooroti::VTermTestCaseModule
|
26
|
+
when :WindowsTestCase
|
27
|
+
load_windows
|
28
|
+
Yamatanooroti::WindowsTestCase
|
29
|
+
when :WindowsTestCaseModule
|
30
|
+
load_windows
|
31
|
+
Yamatanooroti::WindowsTestCaseModule
|
32
|
+
else
|
33
|
+
raise StandardError.new("Unknown class #{id.to_s}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.win?
|
38
|
+
RbConfig::CONFIG['host_os'].match?(/mswin|msys|mingw|cygwin|bccwin|wince|emc/)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.has_vterm_gem?
|
42
|
+
begin
|
43
|
+
require 'vterm'
|
44
|
+
rescue LoadError
|
45
|
+
false
|
46
|
+
else
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Yamatanooroti::TestCase < Test::Unit::TestCase
|
53
|
+
@@runners = []
|
54
|
+
|
55
|
+
def self.inherited(klass)
|
56
|
+
super
|
57
|
+
if ancestors.first == Yamatanooroti::TestCase
|
58
|
+
if Yamatanooroti.has_vterm_gem?
|
59
|
+
test_klass = Class.new(klass)
|
60
|
+
klass.const_set(:TestVTerm, test_klass)
|
61
|
+
test_klass.include Yamatanooroti::VTermTestCaseModule
|
62
|
+
@@runners << test_klass
|
63
|
+
end
|
64
|
+
if Yamatanooroti.win?
|
65
|
+
test_klass = Class.new(klass)
|
66
|
+
klass.const_set(:TestWindows, test_klass)
|
67
|
+
test_klass.include Yamatanooroti::WindowsTestCaseModule
|
68
|
+
@@runners << test_klass
|
69
|
+
end
|
70
|
+
if @@runners.empty?
|
71
|
+
raise LoadError.new(<<~EOM)
|
72
|
+
Any real(?) terminal environments not found.
|
73
|
+
Supporting real(?) terminals:
|
74
|
+
- vterm gem
|
75
|
+
- Windows
|
76
|
+
EOM
|
77
|
+
end
|
78
|
+
def klass.method_added(name)
|
79
|
+
super
|
80
|
+
if ancestors[1] == Yamatanooroti::TestCase
|
81
|
+
@@runners.each do |test_klass|
|
82
|
+
test_klass.define_method(name, instance_method(name))
|
83
|
+
end
|
84
|
+
remove_method name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'test-unit'
|
2
|
+
require 'vterm'
|
3
|
+
require 'pty'
|
4
|
+
|
5
|
+
module Yamatanooroti::VTermTestCaseModule
|
6
|
+
def start_terminal(height, width, command, wait: 0.1)
|
7
|
+
@wait = wait
|
8
|
+
|
9
|
+
@pty_output, @pty_input, @pid = PTY.spawn(*command)
|
10
|
+
|
11
|
+
@vterm = VTerm.new(height, width)
|
12
|
+
@vterm.set_utf8(true)
|
13
|
+
|
14
|
+
@screen = @vterm.screen
|
15
|
+
@screen.reset(true)
|
16
|
+
|
17
|
+
sync
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(str)
|
21
|
+
sync
|
22
|
+
@pty_input.write(str)
|
23
|
+
sync
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
sync
|
28
|
+
@pty_input.close
|
29
|
+
sync
|
30
|
+
end
|
31
|
+
|
32
|
+
private def sync
|
33
|
+
loop do
|
34
|
+
sleep @wait
|
35
|
+
chunk = @pty_output.read_nonblock(1024)
|
36
|
+
@vterm.write(chunk)
|
37
|
+
chunk = @vterm.read
|
38
|
+
@pty_input.write(chunk)
|
39
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
40
|
+
break
|
41
|
+
rescue Errno::EIO # EOF
|
42
|
+
break
|
43
|
+
rescue IO::EAGAINWaitReadable # emtpy buffer
|
44
|
+
break
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def assert_screen(expected_lines)
|
49
|
+
actual_lines = []
|
50
|
+
rows, cols = @vterm.size
|
51
|
+
rows.times do |r|
|
52
|
+
actual_lines << ''
|
53
|
+
cols.times do |c|
|
54
|
+
cell = @screen.cell_at(r, c)
|
55
|
+
actual_lines.last << cell.char if cell.char
|
56
|
+
end
|
57
|
+
actual_lines.last.gsub!(/ *$/, '')
|
58
|
+
end
|
59
|
+
case expected_lines
|
60
|
+
when Array
|
61
|
+
assert_equal(expected_lines, actual_lines)
|
62
|
+
when String
|
63
|
+
assert_equal(expected_lines, actual_lines.join("\n").sub(/\n*\z/, "\n"))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Yamatanooroti::VTermTestCase < Test::Unit::TestCase
|
69
|
+
include Yamatanooroti::VTermTestCaseModule
|
70
|
+
end
|
@@ -0,0 +1,436 @@
|
|
1
|
+
require 'test-unit'
|
2
|
+
require 'fiddle/import'
|
3
|
+
require 'fiddle/types'
|
4
|
+
|
5
|
+
module Yamatanooroti::WindowsDefinition
|
6
|
+
extend Fiddle::Importer
|
7
|
+
dlload 'kernel32.dll', 'psapi.dll', 'user32.dll'
|
8
|
+
include Fiddle::Win32Types
|
9
|
+
|
10
|
+
FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
|
11
|
+
|
12
|
+
typealias 'SHORT', 'short'
|
13
|
+
typealias 'HPCON', 'HANDLE'
|
14
|
+
typealias 'HPCON', 'HANDLE'
|
15
|
+
typealias 'HRESULT', 'HANDLE'
|
16
|
+
typealias 'LPVOID', 'void*'
|
17
|
+
typealias 'SIZE_T', 'size_t'
|
18
|
+
typealias 'LPWSTR', 'void*'
|
19
|
+
typealias 'LPBYTE', 'void*'
|
20
|
+
typealias 'LPCWSTR', 'void*'
|
21
|
+
typealias 'LPPROC_THREAD_ATTRIBUTE_LIST', 'void*'
|
22
|
+
typealias 'PSIZE_T', 'void*'
|
23
|
+
typealias 'DWORD_PTR', 'void*'
|
24
|
+
typealias 'LPCVOID', 'void*'
|
25
|
+
typealias 'LPDWORD', 'void*'
|
26
|
+
typealias 'LPOVERLAPPED', 'void*'
|
27
|
+
typealias 'WCHAR', 'SHORT'
|
28
|
+
typealias 'LPCWCH', 'void*'
|
29
|
+
typealias 'LPSTR', 'void*'
|
30
|
+
typealias 'LPCCH', 'void*'
|
31
|
+
typealias 'LPBOOL', 'void*'
|
32
|
+
typealias 'LPWORD', 'void*'
|
33
|
+
typealias 'ULONG_PTR', 'ULONG*'
|
34
|
+
typealias 'LONG', 'int'
|
35
|
+
|
36
|
+
Fiddle::SIZEOF_HANDLE = Fiddle::SIZEOF_LONG
|
37
|
+
Fiddle::SIZEOF_HPCON = Fiddle::SIZEOF_LONG
|
38
|
+
Fiddle::SIZEOF_HRESULT = Fiddle::SIZEOF_LONG
|
39
|
+
Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG
|
40
|
+
Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT
|
41
|
+
|
42
|
+
COORD = struct [
|
43
|
+
'SHORT X',
|
44
|
+
'SHORT Y'
|
45
|
+
]
|
46
|
+
typealias 'COORD', 'DWORD32'
|
47
|
+
|
48
|
+
SMALL_RECT = struct [
|
49
|
+
'SHORT Left',
|
50
|
+
'SHORT Top',
|
51
|
+
'SHORT Right',
|
52
|
+
'SHORT Bottom'
|
53
|
+
]
|
54
|
+
typealias 'SMALL_RECT*', 'DWORD64*'
|
55
|
+
typealias 'PSMALL_RECT', 'SMALL_RECT*'
|
56
|
+
|
57
|
+
SECURITY_ATTRIBUTES = struct [
|
58
|
+
'DWORD nLength',
|
59
|
+
'LPVOID lpSecurityDescriptor',
|
60
|
+
'BOOL bInheritHandle'
|
61
|
+
]
|
62
|
+
typealias 'PSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*'
|
63
|
+
typealias 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*'
|
64
|
+
|
65
|
+
STARTUPINFOW = struct [
|
66
|
+
'DWORD cb',
|
67
|
+
'LPWSTR lpReserved',
|
68
|
+
'LPWSTR lpDesktop',
|
69
|
+
'LPWSTR lpTitle',
|
70
|
+
'DWORD dwX',
|
71
|
+
'DWORD dwY',
|
72
|
+
'DWORD dwXSize',
|
73
|
+
'DWORD dwYSize',
|
74
|
+
'DWORD dwXCountChars',
|
75
|
+
'DWORD dwYCountChars',
|
76
|
+
'DWORD dwFillAttribute',
|
77
|
+
'DWORD dwFlags',
|
78
|
+
'WORD wShowWindow',
|
79
|
+
'WORD cbReserved2',
|
80
|
+
'LPBYTE lpReserved2',
|
81
|
+
'HANDLE hStdInput',
|
82
|
+
'HANDLE hStdOutput',
|
83
|
+
'HANDLE hStdError'
|
84
|
+
]
|
85
|
+
typealias 'LPSTARTUPINFOW', 'STARTUPINFOW*'
|
86
|
+
|
87
|
+
PROCESS_INFORMATION = struct [
|
88
|
+
'HANDLE hProcess',
|
89
|
+
'HANDLE hThread',
|
90
|
+
'DWORD dwProcessId',
|
91
|
+
'DWORD dwThreadId'
|
92
|
+
]
|
93
|
+
typealias 'PPROCESS_INFORMATION', 'PROCESS_INFORMATION*'
|
94
|
+
typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION*'
|
95
|
+
|
96
|
+
INPUT_RECORD_WITH_KEY_EVENT = struct [
|
97
|
+
'WORD EventType',
|
98
|
+
'BOOL bKeyDown',
|
99
|
+
'WORD wRepeatCount',
|
100
|
+
'WORD wVirtualKeyCode',
|
101
|
+
'WORD wVirtualScanCode',
|
102
|
+
'WCHAR UnicodeChar',
|
103
|
+
## union 'CHAR AsciiChar',
|
104
|
+
'DWORD dwControlKeyState'
|
105
|
+
]
|
106
|
+
|
107
|
+
CHAR_INFO = struct [
|
108
|
+
'WCHAR UnicodeChar',
|
109
|
+
'WORD Attributes'
|
110
|
+
]
|
111
|
+
typealias 'PCHAR_INFO', 'CHAR_INFO*'
|
112
|
+
|
113
|
+
PROCESSENTRY32W = struct [
|
114
|
+
'DWORD dwSize',
|
115
|
+
'DWORD cntUsage',
|
116
|
+
'DWORD th32ProcessID',
|
117
|
+
'ULONG_PTR th32DefaultHeapID',
|
118
|
+
'DWORD th32ModuleID',
|
119
|
+
'DWORD cntThreads',
|
120
|
+
'DWORD th32ParentProcessID',
|
121
|
+
'LONG pcPriClassBase',
|
122
|
+
'DWORD dwFlags',
|
123
|
+
'WCHAR szExeFile[260]'
|
124
|
+
]
|
125
|
+
typealias 'LPPROCESSENTRY32W', 'PROCESSENTRY32W*'
|
126
|
+
|
127
|
+
CONSOLE_FONT_INFOEX = struct [
|
128
|
+
'ULONG cbSize',
|
129
|
+
'DWORD nFont',
|
130
|
+
'DWORD32 dwFontSize',
|
131
|
+
'UINT FontFamily',
|
132
|
+
'UINT FontWeight',
|
133
|
+
'WCHAR FaceName[32]'
|
134
|
+
]
|
135
|
+
typealias 'PCONSOLE_FONT_INFOEX', 'CONSOLE_FONT_INFOEX*'
|
136
|
+
|
137
|
+
STD_INPUT_HANDLE = -10
|
138
|
+
STD_OUTPUT_HANDLE = -11
|
139
|
+
STD_ERROR_HANDLE = -12
|
140
|
+
ATTACH_PARENT_PROCESS = -1
|
141
|
+
KEY_EVENT = 0x0001
|
142
|
+
CT_CTYPE3 = 0x04
|
143
|
+
C3_HIRAGANA = 0x0020
|
144
|
+
C3_HALFWIDTH = 0x0040
|
145
|
+
C3_FULLWIDTH = 0x0080
|
146
|
+
C3_IDEOGRAPH = 0x0100
|
147
|
+
TH32CS_SNAPPROCESS = 0x00000002
|
148
|
+
PROCESS_ALL_ACCESS = 0x001FFFFF
|
149
|
+
|
150
|
+
# HANDLE GetStdHandle(DWORD nStdHandle);
|
151
|
+
extern 'HANDLE GetStdHandle(DWORD);', :stdcall
|
152
|
+
# BOOL CloseHandle(HANDLE hObject);
|
153
|
+
extern 'BOOL CloseHandle(HANDLE);', :stdcall
|
154
|
+
|
155
|
+
# BOOL FreeConsole(void);
|
156
|
+
extern 'BOOL FreeConsole(void);', :stdcall
|
157
|
+
# BOOL AllocConsole(void);
|
158
|
+
extern 'BOOL AllocConsole(void);', :stdcall
|
159
|
+
# BOOL AttachConsole(DWORD dwProcessId);
|
160
|
+
extern 'BOOL AttachConsole(DWORD);', :stdcall
|
161
|
+
# BOOL WINAPI SetConsoleScreenBufferSize(HANDLE hConsoleOutput, COORD dwSize);
|
162
|
+
extern 'BOOL SetConsoleScreenBufferSize(HANDLE, COORD);', :stdcall
|
163
|
+
# BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow);
|
164
|
+
extern 'BOOL SetConsoleWindowInfo(HANDLE, BOOL, PSMALL_RECT);', :stdcall
|
165
|
+
# BOOL WriteConsoleInputW(HANDLE hConsoleInput, const INPUT_RECORD *lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsWritten);
|
166
|
+
extern 'BOOL WriteConsoleInputW(HANDLE, const INPUT_RECORD*, DWORD, LPDWORD);', :stdcall
|
167
|
+
# BOOL ReadConsoleOutputW(HANDLE hConsoleOutput, PCHAR_INFO lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, PSMALL_RECT lpReadRegion);
|
168
|
+
extern 'BOOL ReadConsoleOutputW(HANDLE, PCHAR_INFO, COORD, COORD, PSMALL_RECT);', :stdcall
|
169
|
+
# BOOL WINAPI SetCurrentConsoleFontEx(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx);
|
170
|
+
extern 'BOOL SetCurrentConsoleFontEx(HANDLE, BOOL, PCONSOLE_FONT_INFOEX);', :stdcall
|
171
|
+
|
172
|
+
# BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
|
173
|
+
extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall
|
174
|
+
# HANDLE CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
|
175
|
+
extern 'HANDLE CreateToolhelp32Snapshot(DWORD, DWORD);', :stdcall
|
176
|
+
# BOOL Process32First(HANDLE hSnapshot, LPPROCESSENTRY32W lppe);
|
177
|
+
extern 'BOOL Process32FirstW(HANDLE, LPPROCESSENTRY32W);', :stdcall
|
178
|
+
# BOOL Process32Next(HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
|
179
|
+
extern 'BOOL Process32NextW(HANDLE, LPPROCESSENTRY32W);', :stdcall
|
180
|
+
# DWORD GetCurrentProcessId();
|
181
|
+
extern 'DWORD GetCurrentProcessId();', :stdcall
|
182
|
+
# HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
|
183
|
+
extern 'HANDLE OpenProcess(DWORD, BOOL, DWORD);', :stdcall
|
184
|
+
# BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);
|
185
|
+
extern 'BOOL TerminateProcess(HANDLE, UINT);', :stdcall
|
186
|
+
#BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
|
187
|
+
extern 'BOOL TerminateThread(HANDLE, DWORD);', :stdcall
|
188
|
+
|
189
|
+
# int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar);
|
190
|
+
extern 'int MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, int);', :stdcall
|
191
|
+
# int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, _In_NLS_string_(cchWideChar)LPCWCH lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCCH lpDefaultChar, LPBOOL lpUsedDefaultChar);
|
192
|
+
extern 'int WideCharToMultiByte(UINT, DWORD, LPCWCH, int, LPSTR, int, LPCCH, LPBOOL);', :stdcall
|
193
|
+
#BOOL GetStringTypeW(DWORD dwInfoType, LPCWCH lpSrcStr, int cchSrc, LPWORD lpCharType);
|
194
|
+
extern 'BOOL GetStringTypeW(DWORD, LPCWCH, int, LPWORD);', :stdcall
|
195
|
+
|
196
|
+
typealias 'LPTSTR', 'void*'
|
197
|
+
typealias 'HLOCAL', 'HANDLE'
|
198
|
+
extern 'DWORD FormatMessage(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPTSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall
|
199
|
+
extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall
|
200
|
+
extern 'DWORD GetLastError();', :stdcall
|
201
|
+
FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
|
202
|
+
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
203
|
+
LANG_NEUTRAL = 0x00
|
204
|
+
SUBLANG_DEFAULT = 0x01
|
205
|
+
extern 'int GetSystemMetrics(int);', :stdcall
|
206
|
+
SM_CXMIN = 28
|
207
|
+
SM_CYMIN = 29
|
208
|
+
end
|
209
|
+
|
210
|
+
module Yamatanooroti::WindowsTestCaseModule
|
211
|
+
DL = Yamatanooroti::WindowsDefinition
|
212
|
+
|
213
|
+
private def setup_console(height, width)
|
214
|
+
|
215
|
+
result = DL.FreeConsole
|
216
|
+
error_message(result, 'FreeConsole')
|
217
|
+
result = DL.AllocConsole
|
218
|
+
error_message(result, 'AllocConsole')
|
219
|
+
@output_handle = DL.GetStdHandle(DL::STD_OUTPUT_HANDLE)
|
220
|
+
|
221
|
+
=begin
|
222
|
+
font = DL::CONSOLE_FONT_INFOEX.malloc
|
223
|
+
(font.to_ptr + 0)[0, DL::CONSOLE_FONT_INFOEX.size] = "\x00" * DL::CONSOLE_FONT_INFOEX.size
|
224
|
+
font.cbSize = DL::CONSOLE_FONT_INFOEX.size
|
225
|
+
font.nFont = 0
|
226
|
+
font_size = 72
|
227
|
+
font.dwFontSize = font_size * 65536 + font_size
|
228
|
+
font.FontFamily = 0
|
229
|
+
font.FontWeight = 0
|
230
|
+
font.FaceName[0] = "\x00".ord
|
231
|
+
result = DL.SetCurrentConsoleFontEx(@output_handle, 0, font)
|
232
|
+
error_message(result, 'SetCurrentConsoleFontEx')
|
233
|
+
=end
|
234
|
+
|
235
|
+
rect = DL::SMALL_RECT.malloc
|
236
|
+
rect.Left = 0
|
237
|
+
rect.Top = 0
|
238
|
+
rect.Right = width - 1
|
239
|
+
rect.Bottom = height - 1
|
240
|
+
result = DL.SetConsoleWindowInfo(@output_handle, 1, rect)
|
241
|
+
error_message(result, 'SetConsoleWindowInfo')
|
242
|
+
|
243
|
+
size = DL.GetSystemMetrics(DL::SM_CYMIN) * 65536 + DL.GetSystemMetrics(DL::SM_CXMIN)
|
244
|
+
result = DL.SetConsoleScreenBufferSize(@output_handle, size)
|
245
|
+
error_message(result, 'SetConsoleScreenBufferSize')
|
246
|
+
|
247
|
+
size = height * 65536 + width
|
248
|
+
result = DL.SetConsoleScreenBufferSize(@output_handle, size)
|
249
|
+
error_message(result, 'SetConsoleScreenBufferSize')
|
250
|
+
end
|
251
|
+
|
252
|
+
private def mb2wc(str)
|
253
|
+
size = DL.MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0)
|
254
|
+
converted_str = String.new("\x00" * (size * 2), encoding: 'ASCII-8BIT')
|
255
|
+
DL.MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, converted_str.bytesize)
|
256
|
+
converted_str
|
257
|
+
end
|
258
|
+
|
259
|
+
private def wc2mb(str)
|
260
|
+
size = DL.WideCharToMultiByte(65001, 0, str, str.bytesize, '', 0, 0, 0)
|
261
|
+
converted_str = "\x00" * (size * 2)
|
262
|
+
DL.WideCharToMultiByte(65001, 0, str, str.bytesize, converted_str, converted_str.bytesize, 0, 0)
|
263
|
+
converted_str
|
264
|
+
end
|
265
|
+
|
266
|
+
private def full_width?(c)
|
267
|
+
return false if c.nil? or c.empty?
|
268
|
+
wc = mb2wc(c)
|
269
|
+
type = Fiddle::Pointer.malloc(Fiddle::SIZEOF_WORD, DL::FREE)
|
270
|
+
DL.GetStringTypeW(DL::CT_CTYPE3, wc, wc.bytesize, type)
|
271
|
+
char_type = type[0, Fiddle::SIZEOF_WORD].unpack('S').first
|
272
|
+
if char_type.anybits?(DL::C3_FULLWIDTH)
|
273
|
+
true
|
274
|
+
elsif char_type.anybits?(DL::C3_HALFWIDTH)
|
275
|
+
false
|
276
|
+
elsif char_type.anybits?(DL::C3_HIRAGANA)
|
277
|
+
true
|
278
|
+
elsif char_type.anybits?(DL::C3_IDEOGRAPH)
|
279
|
+
true
|
280
|
+
else
|
281
|
+
false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
private def launch(command)
|
286
|
+
command = %Q{cmd.exe /q /c "#{command.gsub('"', '\\"')}"}
|
287
|
+
converted_command = mb2wc(command)
|
288
|
+
@pi = DL::PROCESS_INFORMATION.malloc
|
289
|
+
(@pi.to_ptr + 0)[0, DL::PROCESS_INFORMATION.size] = "\x00" * DL::PROCESS_INFORMATION.size
|
290
|
+
@startup_info_ex = DL::STARTUPINFOW.malloc
|
291
|
+
(@startup_info_ex.to_ptr + 0)[0, DL::STARTUPINFOW.size] = "\x00" * DL::STARTUPINFOW.size
|
292
|
+
result = DL.CreateProcessW(
|
293
|
+
Fiddle::NULL, converted_command,
|
294
|
+
Fiddle::NULL, Fiddle::NULL, 0, 0, Fiddle::NULL, Fiddle::NULL,
|
295
|
+
@startup_info_ex, @pi
|
296
|
+
)
|
297
|
+
error_message(result, 'CreateProcessW')
|
298
|
+
sleep @wait
|
299
|
+
rescue => e
|
300
|
+
pp e
|
301
|
+
end
|
302
|
+
|
303
|
+
private def error_message(result, method_name)
|
304
|
+
return if not result.zero?
|
305
|
+
err = DL.GetLastError
|
306
|
+
string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
307
|
+
DL.FormatMessage(
|
308
|
+
DL::FORMAT_MESSAGE_ALLOCATE_BUFFER | DL::FORMAT_MESSAGE_FROM_SYSTEM,
|
309
|
+
Fiddle::NULL,
|
310
|
+
err,
|
311
|
+
0x0,
|
312
|
+
string,
|
313
|
+
0,
|
314
|
+
Fiddle::NULL
|
315
|
+
)
|
316
|
+
log "ERROR(#{method_name}): #{err.to_s}: #{string.ptr.to_s}"
|
317
|
+
DL.LocalFree(string)
|
318
|
+
end
|
319
|
+
|
320
|
+
private def log(str)
|
321
|
+
puts str
|
322
|
+
open('aaa', 'a') do |fp|
|
323
|
+
fp.puts str
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def write(str)
|
328
|
+
sleep @wait
|
329
|
+
str.tr!("\n", "\r")
|
330
|
+
records = Fiddle::Pointer.malloc(DL::INPUT_RECORD_WITH_KEY_EVENT.size * str.size * 2, DL::FREE)
|
331
|
+
str.chars.each_with_index do |c, i|
|
332
|
+
record_index = i * 2
|
333
|
+
r = DL::INPUT_RECORD_WITH_KEY_EVENT.new(records + DL::INPUT_RECORD_WITH_KEY_EVENT.size * record_index)
|
334
|
+
r.EventType = DL::KEY_EVENT
|
335
|
+
r.bKeyDown = 1
|
336
|
+
r.wRepeatCount = 0
|
337
|
+
r.wVirtualKeyCode = 0
|
338
|
+
r.wVirtualScanCode = 0
|
339
|
+
r.UnicodeChar = c.unpack('U').first
|
340
|
+
r.dwControlKeyState = 0
|
341
|
+
record_index = i * 2 + 1
|
342
|
+
r = DL::INPUT_RECORD_WITH_KEY_EVENT.new(records + DL::INPUT_RECORD_WITH_KEY_EVENT.size * record_index)
|
343
|
+
r.EventType = DL::KEY_EVENT
|
344
|
+
r.bKeyDown = 0
|
345
|
+
r.wRepeatCount = 0
|
346
|
+
r.wVirtualKeyCode = 0
|
347
|
+
r.wVirtualScanCode = 0
|
348
|
+
r.UnicodeChar = c.unpack('U').first
|
349
|
+
r.dwControlKeyState = 0
|
350
|
+
end
|
351
|
+
written_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE)
|
352
|
+
result = DL.WriteConsoleInputW(DL.GetStdHandle(DL::STD_INPUT_HANDLE), records, str.size * 2, written_size)
|
353
|
+
error_message(result, 'WriteConsoleInput')
|
354
|
+
end
|
355
|
+
|
356
|
+
private def free_resources
|
357
|
+
h_snap = DL.CreateToolhelp32Snapshot(DL::TH32CS_SNAPPROCESS, 0)
|
358
|
+
pe = DL::PROCESSENTRY32W.malloc
|
359
|
+
(pe.to_ptr + 0)[0, DL::PROCESSENTRY32W.size] = "\x00" * DL::PROCESSENTRY32W.size
|
360
|
+
pe.dwSize = DL::PROCESSENTRY32W.size
|
361
|
+
result = DL.Process32FirstW(h_snap, pe)
|
362
|
+
error_message(result, "Process32First")
|
363
|
+
result = DL.FreeConsole()
|
364
|
+
#error_message(result, "FreeConsole")
|
365
|
+
result = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS)
|
366
|
+
error_message(result, 'AttachConsole')
|
367
|
+
loop do
|
368
|
+
#log "a #{pe.th32ParentProcessID.inspect} -> #{pe.th32ProcessID.inspect} #{wc2mb(pe.szExeFile.pack('S260')).unpack('Z*').pack('Z*')}"
|
369
|
+
if pe.th32ParentProcessID == DL.GetCurrentProcessId
|
370
|
+
h_child_proc = DL.OpenProcess(DL::PROCESS_ALL_ACCESS, 0, pe.th32ProcessID)
|
371
|
+
if (h_child_proc)
|
372
|
+
result = DL.TerminateProcess(h_child_proc, 0)
|
373
|
+
error_message(result, "TerminateProcess")
|
374
|
+
result = DL.CloseHandle(h_child_proc)
|
375
|
+
error_message(result, "CloseHandle")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
break if DL.Process32NextW(h_snap, pe).zero?
|
379
|
+
end
|
380
|
+
result = DL.TerminateThread(@pi.hThread, 0)
|
381
|
+
error_message(result, "TerminateThread")
|
382
|
+
end
|
383
|
+
|
384
|
+
def close
|
385
|
+
sleep @wait
|
386
|
+
# read first before kill the console process including output
|
387
|
+
char_info_matrix = Fiddle::Pointer.to_ptr("\x00" * (DL::CHAR_INFO.size * (@height * @width)))
|
388
|
+
region = DL::SMALL_RECT.malloc
|
389
|
+
region.Left = 0
|
390
|
+
region.Top = 0
|
391
|
+
region.Right = @width
|
392
|
+
region.Bottom = @height
|
393
|
+
result = DL.ReadConsoleOutputW(@output_handle, char_info_matrix, @height * 65536 + @width, 0, region)
|
394
|
+
error_message(result, "ReadConsoleOutputW")
|
395
|
+
@result = []
|
396
|
+
prev_c = nil
|
397
|
+
@height.times do |y|
|
398
|
+
line = ''
|
399
|
+
@width.times do |x|
|
400
|
+
index = @width * y + x
|
401
|
+
char_info = DL::CHAR_INFO.new(char_info_matrix + DL::CHAR_INFO.size * index)
|
402
|
+
mb = [char_info.UnicodeChar].pack('U')
|
403
|
+
if prev_c == mb and full_width?(mb)
|
404
|
+
prev_c = nil
|
405
|
+
else
|
406
|
+
line << mb
|
407
|
+
prev_c = mb
|
408
|
+
end
|
409
|
+
end
|
410
|
+
@result << line.gsub(/ *$/, '')
|
411
|
+
end
|
412
|
+
|
413
|
+
free_resources
|
414
|
+
end
|
415
|
+
|
416
|
+
def assert_screen(expected_lines)
|
417
|
+
case expected_lines
|
418
|
+
when Array
|
419
|
+
assert_equal(expected_lines, @result)
|
420
|
+
when String
|
421
|
+
assert_equal(expected_lines, @result.join("\n").sub(/\n*\z/, "\n"))
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def start_terminal(height, width, command, wait: 1)
|
426
|
+
@height = height
|
427
|
+
@width = width
|
428
|
+
@wait = wait
|
429
|
+
setup_console(height, width)
|
430
|
+
launch(command.join(' '))
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
class Yamatanooroti::WindowsTestCase < Test::Unit::TestCase
|
435
|
+
include Yamatanooroti::WindowsTestCaseModule
|
436
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yamatanooroti
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- aycabta
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: test-unit
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: " Yamatanooroti is a multi-platform real(?) terminal test framework.\n"
|
42
|
+
email:
|
43
|
+
- aycabta@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- lib/yamatanooroti.rb
|
51
|
+
- lib/yamatanooroti/version.rb
|
52
|
+
- lib/yamatanooroti/vterm.rb
|
53
|
+
- lib/yamatanooroti/windows.rb
|
54
|
+
homepage: https://github.com/aycabta/yamatanooroti
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata: {}
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.5'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubygems_version: 3.1.2
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: Multi-platform real(?) terminal test framework
|
77
|
+
test_files: []
|