CodeceptJSを動かしてみる
Seleniumとテスティングフレームワークについて調べながらTweetしていたところ、CodeceptJSを薦められるってことがありまして。...ネタか?と思ったりもしたものの、思想的にも作り的にも面白く、今回テストの自動化にあたり、狙っている領域にうまくはまりそうだったので、動かしてみることにしました。
軽く動かすには、公式ページのQuickstartの「Using Selenium WebDriver」まんまでOK。
SleniumやJasmine + jQueryあたりでの自動化だと、UI要素の指定がネックになるなぁと感じていたところで、UI要素を大体で指定できる「Semantic Locators」がかなり良い感じ。出来上がるコードもUI操作が明確で普通に読みやすく、使いやすそうです。
環境
- Windows 10 1809
- Google Chrome 76.0.3809.100(Official Build) (64 ビット)
- Node.js v10.16.2
- Java OpenJDK 11 64-bit (11+28)
- ChromeDriver(単独で動かした場合)
セットアップ
- 作業用に適当にフォルダを準備(hello-codeceptjs)して、とりあえず
npm init
npm install codeceptjs webdriverio
でCodeceptJSとWebDriverIOをインストール- ChromeDriverの実行方法を選んで準備する
npm install @wdio/selenium-standalone-service
を実行して@wdio/selenium-standalone-service
を使用する- 自動的にChromeDriver(Selenium Standalone Server)が実行される
- ただ、テストが終了してもChromeDriverプロセスが終了しない...(v5.12.1)
npm instaell selenium-standalone
を実行してselenium-standalone
を直接使うnpx selenium-standalone install
を実行してドライバー類のダウンロードが必要- テスト実行前に
npx selenium-standalone start
を実行して、あらかじめselenium-standalone
を動かしておく - 実動作は
@wdio/selenium-standalone-service
と同じだけど、こちらはChromeDribverがテスト実行後にちゃんと終了する
- Selenium Standalone Serverをダウンロードして実行する
- 事前にChromeDriverをダウンロードして、パスが通っているところに置く/パスを通す
- e.g.
C:\Users\kkAyataka\.local\.bin
- e.g.
java -jar selenium-server-standalone-3.141.59.jar
- 事前にChromeDriverをダウンロードして、パスが通っているところに置く/パスを通す
npm install -g lite-server
で簡易のWebサーバーを用意する
初期化
CodeceptJSは各種ジェネレーター用のコマンドがそろっていて、コードのひな型を生成してくれる。初期化はnpx codeceptjs init
で、設定用JS等が自動的に生成される。
途中でWebDriver
とchrome
を選ぶ。デフォルトで日本語(ja-JP
)もある。
$ npx codeceptjs init Welcome to CodeceptJS initialization tool It will prepare and configure a test environment for you Installing to D:\Projects\hello-world\hello-codeceptjs ? Where are your tests located? ./*_test.js ? What helpers do you want to use? WebDriver ? Where should logs, screenshots, and reports to be stored? ./output ? Would you like to extend the "I" object with custom steps? Yes ? Where would you like to place custom steps? ./steps_file.js ? Do you want to choose localization for tests? ja-JP Configure helpers... ? [WebDriver] Base url of site to be tested http://localhost ? [WebDriver] Browser in which testing will be performed chrome
npx codeceptjs gt
で空のテストを作成する。ファイル名の「_test」は自動で付く。
出来上がるコードがシンプルなので、使用する必要性は感じないけど、現状では不要かまでは判断つかない。
$ npx codeceptjs gt Creating a new test... ---------------------- ? Filename of the test index ? Feature which is being tested Index
テスト用HTML
操作対象のHTMLは前回と同じものを使う。
#button
をクリックすると#stage
のテキストが変わるというもの。
CodeceptJSで#button
をクリックし、#stage
の内容をチェックする。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <script> function onLoad() { const btn = document.getElementById('button'); const stage = document.getElementById('stage'); // #buttonで#stageの内容を書き換える btn.addEventListener('click', (e) => { stage.innerHTML = 'Hello'; }); } </script> <body onload="onLoad()"> <h1>Hello, World</h1> <button type="button" id="button">Button</button> <div id="stage" >Initial</div> </body> </html>
index_test.js
I.see
がアサーション。かなりアバウトに書けて、「Initial」という文字列がHTML内にあるかどうかで判定しているっぽい。
確かに多くのケースはこれでカバーできそうで、割り切りがうまい。
ボタンの判定もSemantic Locatorと言って、 厳密に指定しなくてもイイ感じに見つけて動作する。 全てのケースをカバーできるわけでは無いけど、自動化の際UIコンポーネントの指定の煩雑さ/難しさは課題になるので、この考え方は面白い。
Feature('Index test'); Scenario('test stage text', (I) => { I.amOnPage('http://localhost:3000'); I.see('Initial'); I.click('button'); I.see('Hello'); });
実行
とりあえず、lite-server
を動かして...
$ lite-server .
selenium-standalone
も動かす。
$ npx selenium-standalone start
で、テスト実行。日本語に設定していると日本語で出力される。簡単な翻訳なんだけどだいぶ読みやすい。
$ npx codeceptjs run --steps CodeceptJS v2.3.0 Using test root "D:\Projects\hello-world\hello-codeceptjs" Index test -- test stage text 私は ページを移動する "http://localhost:3000" 私は テキストがあるか確認する "Initial" 私は クリックする "button" 私は テキストがあるか確認する "Hello" √ OK in 466ms
参考
Seleniumを使用してChromeをコントロールする
SeleniumとChromeでHTMLをコントロールしてみる。使用環境はNode.js。
環境
- Windows 10 1809
- Google Chrome 76.0.3809.100(Official Build) (64 ビット)
- Node.js v10.16.2
- Selenium 4.0.0-alpha.4
- ChromeDriver 76.0.3809.68
セットアップ
- 作業用に適当にフォルダを準備
npm install selenium-webdriver
でselenium-webdriverをインストール- ChromeDriverをダウンロードして、パスが通っているところに置く/パスを通す
- 案外悩ましい...
- Home:
C:\Users\kkAyataka\.bin
- C:
C:\ChromeDriver
- npm:
C:\Users\kkAyataka\AppData\Roaming\npm
- Home:
- 案外悩ましい...
npm install -g lite-server
で簡易のWebサーバーを用意する- この辺はお好み。
HTML
操作対象のHTML。#button
をクリックすると#stage
のテキストが変わるというもの。
Seleniumで#button
をクリックし、#stage
の内容を読んでみる。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <script> function onLoad() { const btn = document.getElementById('button'); const stage = document.getElementById('stage'); // #buttonで#stageの内容を書き換える btn.addEventListener('click', (e) => { stage.innerHTML = 'Hello'; }); } </script> <body onload="onLoad()"> <h1>Hello, World</h1> <button type="button" id="button">Button</button> <div id="stage" >Initial</div> </body> </html>
main.js(Seleniumを用いたNode.jsアプリ)
リファレンス等いろいろ参照して、以下で動いたよ、というサンプル。
const webdriver = require('selenium-webdriver'); const driver = new webdriver.Builder() .forBrowser('chrome') .build(); // lite-serverで動かしているURL(ポート)を指定 driver.get('http://localhost:3000/'); // DOMオブジェクト const btn = driver.findElement(webdriver.By.css('#button')); const stage = driver.findElement(webdriver.By.css('#stage')); stage.getText() // #stageのテキストを取得 .then((r) => { console.log(r); // Initial return btn.click(); // #buttonのクリック }) .then(() => stage.getText()) // もう一回Get .then((r) => { console.log(r); // Hello }) .then(() => { driver.quit(); });
実行
とりあえず、lite-serverを動かして...
$ lite-server .
Node.jsを実行する。クリック前のテキストと、クリック後のテキストがコンソールに表示される。 動作的にはウェブブラウザ(Chrome)が立ち上がって、コードの通りに動作して閉じる。 一瞬で動いてしまうけど、一応目視でも確認できる。
$ node main.js DevTools listening on ws://127.0.0.1:57609/devtools/browser/8137a479-55b0-4422-81cc-405fcec17c6b Initial Hello
参考
C++向けライブラリの一部実装をSwiftで行おうとして諦めた話
MacでC++のプログラミングをしているとCoreFoundationでは機能が足りないことがある。これまではObjective-Cの関数をC言語スタイルの関数にラップして使用するということをよくやっていた。
今回どうせならObjective-CではなくSwiftでやれないかと試したけど、結果的にあまり上手くないなぁというところに行き着いたので、試行錯誤をまとめておく。
- C/C++から直接Swiftを呼び出す方法は(公式には)なさげ
- staticライブラリはアプリのビルド段階で上手く行かない
- dynamicライブラリは作れるけど、libswiftCore.dylibやらが必要になって上手くない
Swiftを使うと言ってもC++ -> Objective-C -> SwiftとしたのでObjective-Cのレイヤーは抜けてない。将来的にどうなっていくのかはちょっと気になる。
環境
@objc publicが必要
まずはObjecive-CからSwiftを呼び出そうとしていきなり詰まる。Swift 4.2では@objc public
を明示的につけないと見つけてもくれない。
@objc public class SwiftClass { @objc public func hello() -> String { return "hello" } }
Swift 3だとなくても動く。正確に理解していないからといったらそれまでだけど、古い情報がイロイロ引っかかって、そのまま動かないのは結構辛かった。
inoutは使えない
既存のAPIを参考にして書いても通らないし、Swift単体のコードとも違うので、すぐにわからなかった。
// Objective-Cに公開するメソッドにはinoutは要らない(つけられない) @objc func getPath(buf: inout UnsafeMutablePointer<CChar>?, bufSize: UnsafeMutablePointer<CInt>?) { ... }
もうちょっとエラーが語ってくれてもなぁ。慣れの問題でもあろうけど、Objective-C向けかSwift向けかってのを意識する必要があるのか。
Method cannot be marked @objc because the type of the parameter 1 cannot be represented in Objective-C
dylib版だと動くけど、libswiftCore.dylib等が必要
staticライブラはどうにもリンクが通らないので、dynamic版にしたところこちらは上手くいった。が、実行するとエラー。
dyld: Library not loaded: @rpath/libswiftCore.dylib
確かに調べてみると、Swiftで作ったアプリもバンドルの中にがっつり組み込まれている。
これは単純にアプリケーションバンドルと同じように、@rpathが届く範囲にファイルをおいてやれば解決する。実際に試したところ上手く動く。...んだけど、わざわざ動かすのにファイルが必要になってくるのは上手くないので、ここで詰み。Swift使えてもデメリットが大きく、現時点では上手くない。
ちなみにXcodeで「Always Embed Swift Standard Libraries」をYesにしておくと自動的にコピーしてくれるらしんだけど、プロジェクトタイプによってはツールが動作せず、Warningが出てコピーされない。
libswiftCoreにはstatic版もあるんだけど...
Xcode.appの中をあさると一応static版も見つかる。だけど使おうにもXcodeから変更する方法が無いっぽい(あまり調べてない)。
$ find /Applications/Xcode.app/Contents/Developer/Toolchains -name "libswiftCore.*" ... /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static/macosx/libswiftCore.a ... /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCore.dylib
Swift単体のコマンドラインツールはdylibが不要
Swift単体のコマンドラインツールはdylibいらんしなぁ...というので、ビルドログを調べてみるとstatic版(swift_static)を参照していた。
Ld /Users/ayataka/Projects/SwiftCmd/DerivedData/SwiftCmd/Build/Products/Debug/SwiftCmd normal x86_64 (in target: SwiftCmd) cd /Users/ayataka/Projects/SwiftCmd export MACOSX_DEPLOYMENT_TARGET=10.14 ... -Xlinker -no_deduplicate -fobjc-arc -fobjc-link-runtime -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static/macosx ...
dylibのビルドの方も同じようにみてみると、こちらはdylib版を参照してる。ここの参照を変更することができれば...と思わなくはないけど。Xcodeでなくswiftcとclangを直に使ったらできるのかも。
Ld /Users/ayataka/Projects/SwiftCmd/DerivedData/SwiftCmd/Build/Products/Debug/libDynamic.dylib normal x86_64 (in target: Dynamic) cd /Users/ayataka/Projects/SwiftCmd export MACOSX_DEPLOYMENT_TARGET=10.14 ... -Xlinker -no_deduplicate -fobjc-arc -fobjc-link-runtime -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ...
まあ、ライブラリ側でlibswiftCoreにあるようなオブジェクトを個別に持つのは非効率。Swiftのコマンドラインツールが内包してしまうのも、プログラムそのものであれば重複を気にすることもないので、理にはかなった動作だとは思うけど。
sd-loginでログインセッションのStateの監視をする
loginctl
やsystemd/sd-login.h
のAPIを用いてログインセッションなどの現在状態が取得できる。加えて、monitor APIが用意されていて、各種変更の検知/通知の受け取りが可能となっている。
sd_login_monitor_new
で監視オブジェクトを生成poll
でイベントを待ち受ける
印象としてはかなり荒い単位で通知がくる。poll
が返ったら自分が監視対象としたい値を取得し、現在どんな値か、変化があったかを自分で
判別する必要がある。
環境
準備
systemdの開発用パッケージが必要なのでインストールする。
$ sudo yum install systemd-devel
ビルドするときはsystemdをリンクする。
$ g++ -Wall main.cpp -lsystemd
ログインStateの監視
sd_login_monitor_new
の第一引数で監視する値を絞れる。ここではユーザーのログイン状態を監視するのに"uid"
を渡している。
この値は他の関数群と関係していそうで、"uid"
ならsd_uid_get_***
系の関数で取れる値を監視できるんじゃなかろうかと思う(が確証なし)。
#include <systemd/sd-login.h> #include <poll.h> #include <unistd.h> #include <cstdlib> #include <iostream> int main() { // create monitor instance sd_login_monitor *mon = 0; const int ret = sd_login_monitor_new("uid", &mon); if (ret >= 0 && mon) { // set up pollfd struct pollfd pfd = {}; pfd.fd = sd_login_monitor_get_fd(mon); pfd.events = sd_login_monitor_get_events(mon); const uid_t uid = getuid(); for (;;) { // reset the wakeup state sd_login_monitor_flush(mon); // wait infinitely const int r = poll(&pfd, (nfds_t)1, -1); if (r > 0 && pfd.revents != 0) { std::cout << "poll: " << r << ", revents: " << pfd.revents << std::endl; // uid state char *state = 0; const int ret = sd_uid_get_state(uid, &state); if (ret >= 0 && state) { std::cout << "State is: " << state << std::endl; free(state); } } } sd_login_monitor_unref(mon); } }
プログラムを実行して、「ユーザースイッチ -> ユーザー切り替えを行わずに再度ログイン」したところ、以下のように出力された。監視対象の状態が多いので、(イメージよりも)不意に呼ばれたりする。
$ ./a.out poll: 1, revents: 1 State is: online poll: 1, revents: 1 State is: online poll: 1, revents: 1 State is: online poll: 1, revents: 1 State is: active poll: 1, revents: 1 State is: active
タイムアウト時間の設定値
ドキュメントを見るとsd_login_monitor_get_timeout
で返る値から計算するようになっているけど、これはイマイチ動作確認できず。試した環境だと計算上-1になって、結局無限に待つことになった。
参考
wの元utmp
Linuxのログイン情報の取得について調べているときに、w
コマンドの実装について気になった。巡り巡って/var/run/utmp
とgetutent
にたどり着くんだけど、ファイルについてはw
コマンドのman pageに記載があった。
ファイルが特定できたときに、まあ、とりあえずとcat /var/run/utmp
打ったらコンソールが化けました。バイナリやんか...tmuxから打って大変なことになった。
getutxentでログインユーザーの情報をえる
getutent
とgetutxent
があるがドキュメントの通りgetutxent
を使う。
#include <utmpx.h> #include <iostream> int main() { setutxent(); struct utmpx * ut = 0; while((ut = getutxent()) != NULL) { std::cout << ut->ut_user << ", type:" << ut->ut_type << ", session:" << ut->ut_session << ", line:" << ut->ut_line << ", id:" << ut->ut_id << std::endl; } endutxent(); }
ビルドして実行すると以下のように出力される。ut_sessionはなんでか0でした。ログイン中のユーザー名やpidなどが取得できる。
$ ./a.out reboot, type:2, session:0, line:~, id:~~ runlevel, type:1, session:0, line:~, id:~~ ayataka, type:7, session:0, line::0, id: ayataka, type:7, session:0, line:pts/0, id:/0 test, type:7, session:0, line::1, id: test, type:8, session:0, line:pts/4, id:/4 test, type:7, session:0, line:pts/6, id:/6 ayataka, type:7, session:0, line:pts/8, id:/8
systemdのsd-loginでGUIセッション情報を取得する
loginctl
で色々とセッション情報が取得できるのはわかったけど、じゃあプログラムから参照する方法はなんだと調べて行ったらsystemdのsd-loginに行き着いた。
ライセンスはLGPL v2.1。GitHubの表示がGPL 2.0になっているけどよくよく見ると初期のころ(?)にLGPLに変更されている。公式ページの方で確認できる。
環境
準備とビルド
開発用パッケージが必要なのでインストールする。
$ sudo yum install systemd-devel
ビルドするときはsystemdをリンクする。
$ g++ -Wall main.cpp -lsystemd
セッションのアクティブ状態を取得する
セッションIDが必要だけどgetsid
の値とは別物。sd-loginのAPIを使って、(ここでは)pidから取得している。
sd_session_is_active
の値をファイルに書き出しつつ、ユーザーを変更すると、得られる値が変化することが観察できる。
#include <systemd/sd-login.h> #include <sys/types.h> #include <unistd.h> #include <cstdlib> #include <iostream> int main() { const pid_t pid = getpid(); char *session = 0; const int err = sd_pid_get_session(pid, &session); if (err >= 0 && session) { std::cout << "session: " << session << ", is_active: " << sd_session_is_active(session) << std::endl; free(session); } }
ユーザーIDからセッションのStateを取得する
もう一つ。こちらはユーザーIDからStateの値を取得する。ログイン中でアクティブならactive、ログインしたままユーザーを変更するとonlineが返る。
こちらもStateの値をファイルに書き出しつつ、ユーザーを変更することで値の変化を観察できる。
#include <systemd/sd-login.h> #include <sys/types.h> #include <unistd.h> #include <cstdlib> #include <iostream> int main() { const uid_t uid = getuid(); char *state = 0; const int err = sd_uid_get_state(uid, &state); if (err >= 0 && state) { // forgraound user's state is "active" // background user's state is "online" std::cout << "uid: " << uid << ", state: " << state << std::endl; free(state); } }
参考
loginctlでGUIセッションの情報を取得する
LinuxでGUI表示中のユーザーかどうか(セッションがアクティブかどうか)を調べる方法がないかと調べて行ったらloginctl
に行き着いた。Linuxの(というかsystemdのか?)セッションに詳しくないのでフレーズの使い方が微妙なんだけど、概ね必要な情報は取れてたのでまとめておく。
loginctl list-sessions
でGUIログイン中のセッションリストを取得できるloginctl show-session [ID...]
で指定したセッションの詳細が表示できる/run/systemd/sessions/
以下のファイルからshow-session
の情報が取得できる- が、「# This is private data. Do not parse.」
環境
- CentOS 7.6.1810
- systemd 219 (loginctl --version)
systemdに依存しているので、systemdのシステムなら通じるはず。逆にsystemdではないシステムでは全く通じない。
system-logindのloginctlコマンド
loginctl
コマンドで情報を取得できる。サブコマンド形式になっていて、セッションに関するものは以下。このうちlist-sessions
とshow-session
あたりで見ていく。
$ loginctl -h ... Session Commands: list-sessions List sessions session-status [ID...] Show session status show-session [ID...] Show properties of sessions or the manager activate [ID] Activate a session lock-session [ID...] Screen lock one or more sessions unlock-session [ID...] Screen unlock one or more sessions lock-sessions Screen lock all current sessions unlock-sessions Screen unlock all current sessions terminate-session ID... Terminate one or more sessions kill-session ID... Send signal to processes of a session ...
loginctl list-sessions
でセッションのリストを表示する
セッションのリストを表示する。以下はayatakaユーザーとtestユーザーで同時にログインしている状態。
$ loginctl list-sessions SESSION UID USER SEAT 1 1000 ayataka seat0 47 1001 test seat0 c8 42 gdm seat0
loginctl show-session
でセッション情報を取得する
list-sessions
でセッションIDを調べたら、show-session
で詳細情報を取得する。この中のActiveがyesだったりStateがactiveだと表示中。非表示中だとnoとかonlineとかになる。
詳細な情報とか意味は公式ドキュメントで。今回は細かいところは理解していない。
$ loginctl show-session 1 Id=1 User=1000 Name=ayataka Timestamp=Sat 2019-03-02 21:56:39 JST TimestampMonotonic=151218846 VTNr=1 Seat=seat0 Display=:0 Remote=no Service=gdm-password Scope=session-1.scope Leader=30172 Audit=1 Type=x11 Class=user Active=yes # <- yes State=active # <- active IdleHint=no IdleSinceHint=1551587104930805 IdleSinceHintMonotonic=19749233850 LockedHint=no
/run/systemd/sessions/
以下のファイル
セッションの情報は/run/systemd/sessions/
以下にセッションIDごとにファイルが存在していて、一応そこから参照できる。ただし「# This is private data. Do not parse.」。
$ cat /run/systemd/sessions/1 # This is private data. Do not parse. UID=1000 USER=ayataka ACTIVE=1 STATE=active REMOTE=0 STOPPING=0 TYPE=x11 CLASS=user SCOPE=session-1.scope FIFO=/run/systemd/sessions/1.ref SEAT=seat0 DISPLAY=:0 SERVICE=gdm-password VTNR=1 LEADER=30172 AUDIT=1 REALTIME=1551531399727614 MONOTONIC=151218846
ログアウト済みのセッション情報
実は記事作成中、list-sessions
の結果は以下のようになっていた。testユーザーが2つある...
$ loginctl list-sessions SESSION UID USER SEAT 1 1000 ayataka seat0 47 1001 test seat0 c8 42 gdm seat0 29 1001 test seat0 # <- なんぞこれ?
中身を表示したところ、どうやら一度ログインしてログアウトした時の情報が残ってるっぽい。
$ loginctl show-session 29 Id=29 User=1001 Name=test Timestamp=Sun 2019-03-03 09:57:15 JST TimestampMonotonic=12154020744 VTNr=2 Seat=seat0 Display=:1 Remote=no Service=gdm-password Scope=session-29.scope Leader=35515 Audit=29 Type=x11 Class=user Active=no State=closing # <- closing IdleHint=no IdleSinceHint=1551586915841192 IdleSinceHintMonotonic=19560144236 LockedHint=no