yamatanooroti 0.0.1
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.
- 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: []
|