kkAyatakaのメモ帳。

誰かの役に立つかもしれない備忘録。

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(単独で動かした場合)
    • Node.jsのselenium-standaloneを使用する場合は不要だけど、単独で動かす場合に必要
    • ChromeDriver 76.0.3809.68
    • Selenium Standalone Server 3.141.59

セットアップ

  • 作業用に適当にフォルダを準備(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
      • java -jar selenium-server-standalone-3.141.59.jar
  • npm install -g lite-serverで簡易のWebサーバーを用意する

初期化

CodeceptJSは各種ジェネレーター用のコマンドがそろっていて、コードのひな型を生成してくれる。初期化はnpx codeceptjs initで、設定用JS等が自動的に生成される。

途中でWebDriverchromeを選ぶ。デフォルトで日本語(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をコントロールする

SeleniumChromeでHTMLをコントロールしてみる。使用環境はNode.js。

環境

セットアップ

  • 作業用に適当にフォルダを準備
  • npm install selenium-webdriverselenium-webdriverをインストール
  • ChromeDriverをダウンロードして、パスが通っているところに置く/パスを通す
    • 案外悩ましい...
      • Home: C:\Users\kkAyataka\.bin
      • C: C:\ChromeDriver
      • npm: C:\Users\kkAyataka\AppData\Roaming\npm
  • 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で行おうとして諦めた話

MacC++のプログラミングをしていると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で作ったアプリもバンドルの中にがっつり組み込まれている。

f:id:kkAyataka:20190319190912p:plain

これは単純にアプリケーションバンドルと同じように、@rpathが届く範囲にファイルをおいてやれば解決する。実際に試したところ上手く動く。...んだけど、わざわざ動かすのにファイルが必要になってくるのは上手くないので、ここで詰み。Swift使えてもデメリットが大きく、現時点では上手くない。

ちなみにXcodeで「Always Embed Swift Standard Libraries」をYesにしておくと自動的にコピーしてくれるらしんだけど、プロジェクトタイプによってはツールが動作せず、Warningが出てコピーされない。

f:id:kkAyataka:20190320172623p:plain
Xcode-AlwaysEmbedSwiftStandardLibraries

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の監視をする

loginctlsystemd/sd-login.hAPIを用いてログインセッションなどの現在状態が取得できる。加えて、monitor APIが用意されていて、各種変更の検知/通知の受け取りが可能となっている。

  • sd_login_monitor_newで監視オブジェクトを生成
  • pollでイベントを待ち受ける

印象としてはかなり荒い単位で通知がくる。pollが返ったら自分が監視対象としたい値を取得し、現在どんな値か、変化があったかを自分で 判別する必要がある。

環境

  • CentOS 7.6.1810
  • systemd 219 (loginctl —version)
  • gcc 4.8.5

準備

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/utmpgetutentにたどり着くんだけど、ファイルについてはwコマンドのman pageに記載があった。

  • getutxent APIでutmpの中身を読むことでログイン情報を得る
  • getutxentPOSIX APIとして提供されている

ファイルが特定できたときに、まあ、とりあえずとcat /var/run/utmp打ったらコンソールが化けました。バイナリやんか...tmuxから打って大変なことになった。

getutxentでログインユーザーの情報をえる

getutentgetutxentがあるがドキュメントの通り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に行き着いた。

  • systemd/sd-login.hAPIloginctlから得られる情報をAPI経由で取得できる
  • systemd-develのインストールとlibsystemdへのリンクが必要

ライセンスはLGPL v2.1。GitHubの表示がGPL 2.0になっているけどよくよく見ると初期のころ(?)にLGPLに変更されている。公式ページの方で確認できる。

loginctlのコマンドを触ってみた話はこちら。

環境

  • CentOS 7.6.1810
  • systemd 219 (loginctl —version)
  • gcc 4.8.5

準備とビルド

開発用パッケージが必要なのでインストールする。

$ 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セッションの情報を取得する

LinuxGUI表示中のユーザーかどうか(セッションがアクティブかどうか)を調べる方法がないかと調べて行ったらloginctlに行き着いた。Linuxの(というかsystemdのか?)セッションに詳しくないのでフレーズの使い方が微妙なんだけど、概ね必要な情報は取れてたのでまとめておく。

  • loginctl list-sessionsGUIログイン中のセッションリストを取得できる
  • loginctl show-session [ID...]で指定したセッションの詳細が表示できる
  • /run/systemd/sessions/以下のファイルからshow-sessionの情報が取得できる
    • が、「# This is private data. Do not parse.」

systemdのAPIを触ってみた話はこちら。

環境

  • CentOS 7.6.1810
  • systemd 219 (loginctl --version)

systemdに依存しているので、systemdのシステムなら通じるはず。逆にsystemdではないシステムでは全く通じない。

system-logindのloginctlコマンド

loginctlコマンドで情報を取得できる。サブコマンド形式になっていて、セッションに関するものは以下。このうちlist-sessionsshow-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