diff --git a/tests/unittests/StartEditorProcessForTest.h b/tests/unittests/StartEditorProcessForTest.h new file mode 100644 index 0000000000..23d532b5b3 --- /dev/null +++ b/tests/unittests/StartEditorProcessForTest.h @@ -0,0 +1,48 @@ +/*! @file */ +/* + Copyright (C) 2018-2020 Sakura Editor Organization + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is + not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif /* #ifndef NOMINMAX */ + +#include +#include + +#include + +/*! + * テストコード専用wWinMain呼出のラッパー関数 + * + * 単体テストから wWinMain を呼び出すためのラッパー関数です。 + * + * wWinMain は呼出元のグローバル変数を汚してしまうため、 + * ASSERT_EXIT, ASSERT_DEATH などを使って別プロセスで実行するようにしてください。 + * + * この関数をコントロールプロセスの起動に使用しないでください。 + * googletestでは、ASSERT_EXITで起動したプロセスの完全な終了を待機できないようです。 + * コントロールプロセスが終了する前に他のテストが実行されると期待した動作にならない場合があります。 + */ +int StartEditorProcessForTest( const std::wstring_view& strCommandLine ); diff --git a/tests/unittests/code-main.cpp b/tests/unittests/code-main.cpp new file mode 100644 index 0000000000..cf801bdac7 --- /dev/null +++ b/tests/unittests/code-main.cpp @@ -0,0 +1,128 @@ +/*! @file */ +/* + Copyright (C) 2018-2020 Sakura Editor Organization + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is + not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif /* #ifndef NOMINMAX */ + +#include +#include + +#include +#include +#include +#include +#include + +#include "debug/Debug2.h" +#include "StartEditorProcessForTest.h" + +/*! + * テストコード専用wWinMain呼出のラッパー関数 + * + * 単体テストから wWinMain を呼び出すためのラッパー関数です。 + * + * コマンドラインでプロファイルが指定されていない場合、空指定を付加します。 + */ +int StartEditorProcessForTest( const std::wstring_view& strCommandLine ) +{ + + // 実行中モジュールのインスタンスハンドルを取得する + HINSTANCE hInstance = ::GetModuleHandle( NULL ); + + // WinMainを起動するためのコマンドラインを組み立てる + std::wstring strCmdBuff( strCommandLine ); + + // コマンドラインに -PROF 指定がない場合は付加する + if( !std::regex_search( strCmdBuff, std::wregex( L"-PROF\\b", std::wregex::icase ) ) ){ + strCmdBuff += L" -PROF=\"\""; + } + + // wWinMainを起動する + return wWinMain( hInstance, NULL, &*strCmdBuff.begin(), SW_SHOWDEFAULT ); +} + +/*! + * 必要な場合にwWinMainを起動して終了する。 + * + * コマンドラインに -PROF 指定がない場合、呼出元に制御を返す。 + * コマンドラインに -PROF 指定がある場合、wWinMainを呼出してプログラムを終了する。 + */ +static void InvokeWinMainIfNeeded( char** ppArgsBegin, char** ppArgsEnd ) +{ + // コマンドライン引数がない場合 + if( ppArgsBegin == ppArgsEnd ){ + return; + } + + // コマンドラインに -PROF 指定がない場合 + if( ppArgsEnd == std::find_if( ppArgsBegin, ppArgsEnd, []( const char* arg ){ return std::regex_search( arg, std::regex( "-PROF\\b", std::regex::icase ) ); } ) ){ + return; + } + + // 最初の引数はプログラム名なので無視する + ppArgsBegin++; + + // wWinMainを起動するためのコマンドラインを組み立てる(バッファ長はざっくり定義。) + wchar_t szCmdBuf[4096]; + std::wstring strCommandLine; + std::for_each( ppArgsBegin, ppArgsEnd, [&strCommandLine, &szCmdBuf]( const auto* arg ){ + ::swprintf_s( szCmdBuf, L"%hs ", arg ); + strCommandLine += szCmdBuf; + } ); + + // 末尾の空白を削る(引数0個はここに来ないのでチェックしない) + strCommandLine.assign( strCommandLine.data(), strCommandLine.length() - 1 ); + + // 実行中モジュールのインスタンスハンドルを取得する + HINSTANCE hInstance = ::GetModuleHandleW( NULL ); + + // ログ出力 + WCHAR *pszCommandLine = &*strCommandLine.begin(); + printf( "%s(%d): launching process [%ls]\n", __FILE__, __LINE__, pszCommandLine ); + + // wWinMainを起動する + int ret = wWinMain( hInstance, NULL, pszCommandLine, SW_SHOWDEFAULT ); + + // ログ出力(途中でexitした場合は出力されない) + printf( "%s(%d): leaving process [%ls] => %d\n", __FILE__, __LINE__, pszCommandLine, ret ); + + // プログラムを終了する(呼出元に制御は返らない) + exit( ret ); +} + +/*! + * テストモジュールのエントリポイント + */ +int main(int argc, char **argv) { + // コマンドラインに -PROF 指定がある場合、wWinMainを起動して終了する。 + InvokeWinMainIfNeeded( argv, argv + argc ); + + // WinMainを起動しない場合、標準のgtest_main同様の処理を実行する + printf("Running main() from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/unittests/test-winmain.cpp b/tests/unittests/test-winmain.cpp new file mode 100644 index 0000000000..b317e3ebf7 --- /dev/null +++ b/tests/unittests/test-winmain.cpp @@ -0,0 +1,326 @@ +/*! @file */ +/* + Copyright (C) 2018-2020 Sakura Editor Organization + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is + not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif /* #ifndef NOMINMAX */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config/maxdata.h" +#include "basis/primitive.h" +#include "debug/Debug2.h" +#include "basis/CMyString.h" +#include "mem/CNativeW.h" +#include "env/DLLSHAREDATA.h" +#include "util/file.h" +#include "config/system_constants.h" + +#include "StartEditorProcessForTest.h" + +using namespace std::literals::string_literals; + +/*! + * HANDLE型のスマートポインタを実現するためのdeleterクラス + */ +struct handle_closer +{ + void operator()( HANDLE handle ) const + { + ::CloseHandle( handle ); + } +}; + +//! HANDLE型のスマートポインタ +typedef std::unique_ptr::type, handle_closer> handleHolder; + +/*! + * WinMain起動テストのためのフィクスチャクラス + * + * 設定ファイルを使うテストは「設定ファイルがない状態」からの始動を想定しているので + * 始動前に設定ファイルを削除するようにしている。 + * テスト実行後に設定ファイルを残しておく意味はないので終了後も削除している。 + */ +class WinMainTest : public ::testing::Test { +protected: + /*! + * 設定ファイルのパス + * + * CFileNameManager::GetIniFileNameDirectを使ってtests1.iniのパスを取得する。 + */ + WCHAR szIniFile[_MAX_PATH]; + + /*! + * テストが起動される直前に毎回呼ばれる関数 + */ + void SetUp() override { + // INIファイルのパスを取得 + WCHAR szPrivateIniFile[_MAX_PATH]; + CFileNameManager::GetIniFileNameDirect( szPrivateIniFile, szIniFile, L"" ); + + if( fexist( szIniFile ) ){ + // INIファイルを削除する + std::filesystem::remove( szIniFile ); + } + } + + /*! + * テストが実行された直後に毎回呼ばれる関数 + */ + void TearDown() override { + // INIファイルを削除する + std::filesystem::remove( szIniFile ); + } +}; + +/*! + * @brief コントロールプロセスの初期化完了を待つ + * + * CControlProcess::WaitForInitializedとして実装したいコードです。本体を変えたくないので一時定義しました。 + * 既存CProcessFactory::WaitForInitializedControlProcess()と概ね等価です。 + */ +void CControlProcess_WaitForInitialized( LPCWSTR lpszProfileName ) +{ + // 初期化完了イベントを作成する + std::wstring strInitEvent( GSTR_EVENT_SAKURA_CP_INITIALIZED ); + if( lpszProfileName && lpszProfileName[0] ){ + strInitEvent += lpszProfileName; + } + auto hEvent = ::CreateEventW( NULL, TRUE, FALSE, strInitEvent.data() ); + if (!hEvent) { + throw std::runtime_error( "create event failed." ); + } + + // イベントハンドラをスマートポインタに入れる + handleHolder eventHolder( hEvent ); + + // 初期化完了イベントを待つ + DWORD dwRet = ::WaitForSingleObject( hEvent, 10000 ); + if( WAIT_TIMEOUT == dwRet ){ + throw std::runtime_error( "waitEvent is timeout." ); + } +} + +/*! + * @brief コントロールプロセスを起動する + * + * CControlProcess::Startとして実装したいコードです。本体を変えたくないので一時定義しました。 + * 既存CProcessFactory::StartControlProcess()と概ね等価です。 + */ +void CControlProcess_Start( LPCWSTR lpszProfileName ) +{ + // スタートアップ情報 + STARTUPINFO si = { sizeof(STARTUPINFO), 0 }; + si.lpTitle = (LPWSTR)L"sakura control process"; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOWDEFAULT; + + WCHAR szExePath[MAX_PATH]; + ::GetModuleFileNameW( NULL, szExePath, _countof(szExePath) ); + + CNativeW cmemCommandLine; + cmemCommandLine.AppendStringF( L"\"%s\" -NOWIN -PROF=\"%s\"", szExePath, lpszProfileName ); + + LPWSTR pszCommandLine = cmemCommandLine.GetStringPtr(); + DWORD dwCreationFlag = CREATE_DEFAULT_ERROR_MODE; + PROCESS_INFORMATION pi; + + // コントロールプロセスを起動する + BOOL createSuccess = ::CreateProcess( + szExePath, // 実行可能モジュールパス + pszCommandLine, // コマンドラインバッファ + NULL, // プロセスのセキュリティ記述子 + NULL, // スレッドのセキュリティ記述子 + FALSE, // ハンドルの継承オプション(継承させない) + dwCreationFlag, // 作成のフラグ + NULL, // 環境変数(変更しない) + NULL, // カレントディレクトリ(変更しない) + &si, // スタートアップ情報 + &pi // プロセス情報(作成されたプロセス情報を格納する構造体) + ); + if( !createSuccess ){ + throw std::runtime_error( "create process failed." ); + } + + // 開いたハンドルは使わないので閉じておく + ::CloseHandle( pi.hThread ); + ::CloseHandle( pi.hProcess ); + + // コントロールプロセスの初期化完了を待つ + CControlProcess_WaitForInitialized( lpszProfileName ); +} + +/*! + * @brief コントロールプロセスに終了指示を出して終了を待つ + * + * CControlProcess::Terminateとして実装したいコードです。本体を変えたくないので一時定義しました。 + * 既存コードに該当する処理はありません。 + */ +void CControlProcess_Terminate( LPCWSTR lpszProfileName ) +{ + // トレイウインドウを検索する + std::wstring strCEditAppName( GSTR_CEDITAPP ); + if( lpszProfileName && lpszProfileName[0] ){ + strCEditAppName += lpszProfileName; + } + HWND hTrayWnd = ::FindWindow( strCEditAppName.data(), strCEditAppName.data() ); + if( !hTrayWnd ){ + throw std::runtime_error( "tray window is not found." ); + } + + // トレイウインドウからプロセスIDを取得する + DWORD dwControlProcessId = 0; + ::GetWindowThreadProcessId( hTrayWnd, &dwControlProcessId ); + if( !dwControlProcessId ){ + throw std::runtime_error( "dwControlProcessId can't be retrived." ); + } + + // プロセス情報の問い合せを行うためのハンドルを開く + HANDLE hControlProcess = ::OpenProcess( PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, dwControlProcessId ); + if( !hControlProcess ){ + throw std::runtime_error( "hControlProcess can't be opened." ); + } + + // プロセスハンドルをスマートポインタに入れる + handleHolder processHolder( hControlProcess ); + + // トレイウインドウを閉じる + ::SendMessage( hTrayWnd, WM_CLOSE, 0, 0 ); + + // プロセス終了を待つ + DWORD dwExitCode = 0; + if( ::GetExitCodeProcess( hControlProcess, &dwExitCode ) && dwExitCode == STILL_ACTIVE ){ + DWORD waitProcessResult = ::WaitForSingleObject( hControlProcess, INFINITE ); + if( WAIT_TIMEOUT == waitProcessResult ){ + throw std::runtime_error( "waitProcess is timeout." ); + } + } +} + +/*! + * @brief wWinMainを起動してみるテスト + * プログラムが起動する正常ルートに潜む障害を検出するためのもの。 + * コントロールプロセスを実行する。 + */ +TEST_F( WinMainTest, runWithNoWin ) +{ + // テスト用プロファイル名 + constexpr auto szProfileName = L""; + + // コントロールプロセスを起動する + CControlProcess_Start( szProfileName ); + + // コントロールプロセスに終了指示を出して終了を待つ + CControlProcess_Terminate( szProfileName ); + + // コントロールプロセスが終了すると、INIファイルが作成される + ASSERT_TRUE( fexist( szIniFile ) ); +} + +/*! + * @brief WinMainを起動してみるテスト + * プログラムが起動する正常ルートに潜む障害を検出するためのもの。 + * エディタプロセスを実行する。 + */ +TEST_F( WinMainTest, runEditorProcess ) +{ + // エディタプロセスを起動するため、テスト実行はプロセスごと分離して行う + auto separatedTestProc = [] { + std::mutex mtx; + std::condition_variable cv; + bool initialized = false; + + // エディタプロセスが起動したコントロールプロセスの終了を待機するスレッド + auto waitingThread = std::thread([&mtx, &cv, &initialized] { + // 初期化 + { + std::unique_lock lock( mtx ); + initialized = true; + cv.notify_one(); + } + + // テスト用プロファイル名 + constexpr auto szProfileName = L""; + + // コントロールプロセスの初期化完了を待つ + CControlProcess_WaitForInitialized( szProfileName ); + + // コントロールプロセスに終了指示を出して終了を待つ + CControlProcess_Terminate( szProfileName ); + }); + + // スレッドの初期化完了を待機する + std::unique_lock lock( mtx ); + cv.wait(lock, [&initialized] { return initialized; }); + + // 起動時実行マクロの中身を作る + std::wstring strStartupMacro; + strStartupMacro += L"Down();"; + strStartupMacro += L"Up();"; + strStartupMacro += L"Right();"; + strStartupMacro += L"Left();"; + strStartupMacro += L"ShowFunckey();"; //ShowFunckey 出す + strStartupMacro += L"ShowMiniMap();"; //ShowMiniMap 出す + strStartupMacro += L"ShowTab();"; //ShowTab 出す + strStartupMacro += L"SelectAll();"; + strStartupMacro += L"GoFileEnd();"; + strStartupMacro += L"GoFileTop();"; + strStartupMacro += L"ShowFunckey();"; //ShowFunckey 消す + strStartupMacro += L"ShowMiniMap();"; //ShowMiniMap 消す + strStartupMacro += L"ShowTab();"; //ShowTab 消す + strStartupMacro += L"ExitAll();"; //NOTE: このコマンドにより、エディタプロセスは起動された直後に終了する。 + + // コマンドラインを組み立てる + std::wstring strCommandLine( _T(__FILE__) L" -MTYPE=js" ); + strCommandLine += L" -M=\""s; + strCommandLine += std::regex_replace( strStartupMacro, std::wregex( L"\"" ), L"\"\"" ); + strCommandLine += L"\""s; + + // エディタプロセスを起動する + StartEditorProcessForTest( strCommandLine ); + + // エディタ終了を待機する + if( waitingThread.joinable() ){ + waitingThread.join(); + } + }; + + // テストプログラム内のグローバル変数を汚さないために、別プロセスで起動させる + ASSERT_EXIT({ separatedTestProc(); exit(0); }, ::testing::ExitedWithCode(0), ".*" ); + + // コントロールプロセスが終了すると、INIファイルが作成される + ASSERT_TRUE( fexist( szIniFile ) ); +} diff --git a/tests/unittests/tests1.vcxproj b/tests/unittests/tests1.vcxproj index 7e680f027b..e194509e6e 100644 --- a/tests/unittests/tests1.vcxproj +++ b/tests/unittests/tests1.vcxproj @@ -105,6 +105,7 @@ + @@ -121,6 +122,7 @@ + @@ -136,5 +138,8 @@ {7a6d0f29-e560-4985-835b-5f92a08eb242} + + + \ No newline at end of file diff --git a/tests/unittests/tests1.vcxproj.filters b/tests/unittests/tests1.vcxproj.filters index 1c19c8427b..f0d8289042 100644 --- a/tests/unittests/tests1.vcxproj.filters +++ b/tests/unittests/tests1.vcxproj.filters @@ -68,5 +68,16 @@ Test Files + + Other Files + + + Test Files + + + + + Other Files + \ No newline at end of file