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 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,3 @@
1
+ class Yamatanooroti
2
+ VERSION = '0.0.1'
3
+ 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: []