Scapy + Python2 + I/O多重化・ブロッキングI/O系モジュールにて、Raspberry Pi 2 Model Bをブリッジ化する

前回、Scapyの bridge_and_sniff() 関数を使って、Raspberry Pi 2 Model B をブリッジ化してみました。
Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku - メモ的な思考的な

ただ、 bridge_and_sniff() 関数を使うだけではお手軽すぎるため、今回は別の方法でブリッジ化してみます。

 
なお、誤った理解があれば、ご指摘いただけるとありがたいです。

 
目次

 

環境

前回の環境と同じですので、さらっと書いておきます。

  • Mac OS X 10.11.6
    • 疎通確認のために、PythonでHTTPサーバを立てる
  • Raspberry Pi 2 Model B
    • Raspbian
      • RASPBIAN STRETCH WITH DESKTOP
      • Version: November 2017
    • Python 2.7.13
      • Raspbianにインストールされていたものを使用
    • Scapy 2.3.3
    • 外付けUSB有線LANアダプタ(LUA4-U3-AGT)
    • SSHMacから接続可能
  • Windows10
    • curlをインストール済

 

bridge_and_sniff()関数の実装を見る

まずは、Scapyのbridge_and_sniff()関数の実装を見てみます。コードはこのあたりです。
https://github.com/secdev/scapy/blob/v2.3.3/scapy/sendrecv.py#L552

bridge_and_sniff()関数では、ネットワークというI/Oバウンドな処理を効果的に行えるよう、I/O多重化の select を使っていました。

処理の流れは

でした。

 

今回試してみる実装形式

I/Oバウンドを効果的に処理できそうなPythonの標準モジュールをみたところ、

  • I/O多重化: select
  • ブロッキングI/O系
    • マルチスレッド: threading
    • スレッドプール: concurrent.futures
  • ノンブロッキングI/O系
    • イベントループ(Python3.4〜): asyncio
    • Python3では後方互換維持のためだけに存在: asyncore

がありました。

Python2なScapyなこともあり、ノンブロッキングI/O系の実装は難しそうなため*1、今回はI/O多重化とブロッキングI/O系モジュールを使って実装してみます。

 
なお、I/Oバウンドな処理ではマルチプロセスやプロセスプールは効果的ではありません。
Pythonをとりまく並行/非同期の話

とはいえ、それらのコードも書いてみたかったので、

  • ブロッキングI/O系
    • マルチプロセス: multiprocess
    • プロセスプール: concurrent.futures

でも実装してみました。

 

I/O多重化:selectモジュールのselect()での実装

I/O多重化には

  • select
  • poll
  • epoll

などがあります。

 
まずは、Scapyのbridge_and_sniff()関数と同じく、selectモジュールの select() を試してみます。
16.1. select — I/O 処理の完了を待機する — Python 2.7.14 ドキュメント

scapy_bridge_select.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import select


def bridge():
    try:
        # レイヤ2のソケットを用意
        eth0_socket = conf.L2socket(iface='eth0')
        eth1_socket = conf.L2socket(iface='eth1')
        # 別のインタフェースからパケットを送信するための辞書
        next_socket = {
            eth0_socket: eth1_socket,
            eth1_socket: eth0_socket,
        }
        while True:
            # select()関数で使えるようになるまで待機
            readable_sockets, _, _ = select.select([eth0_socket, eth1_socket], [], [])

            for s in readable_sockets:
                # 準備できたソケットから受信
                p = s.recv()

                if p:
                    # パケット全体をターミナルへ表示
                    p.show()

                    # 受信したパケットを別のインタフェースから送信
                    next_socket[s].send(p.original)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 
ためしにrecv()で受信した時の内容を見てみると、

# パケットの型をターミナルへ表示
print '---- packet type from recv(): {}'.format(type(p))
# => <class 'scapy.layers.l2.Ether'>
print '---- original type from recv(): {}'.format(type(p.original))
# => <type 'str'>

でした。

 

I/O多重化:selectモジュールのepoll()での実装

RaspbianはLinuxベースのOSなため、I/O多重化の epoll が使えます。

Pythonではselectモジュールの epoll() 関数を使って実装します。

scapy_bridge_epoll.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import select


def bridge():
    try:
        # レイヤ2のソケットを用意
        eth0_socket = conf.L2socket(iface='eth0')
        eth1_socket = conf.L2socket(iface='eth1')

        epoll = select.epoll()
        # 読み出し可能なデータが存在する場合を登録
        # https://docs.python.jp/2/library/select.html#edge-and-level-trigger-polling-epoll-objects
        epoll.register(eth0_socket.fileno(), select.EPOLLIN)
        epoll.register(eth1_socket.fileno(), select.EPOLLIN)

        while True:
            # イベントを1秒待機
            events = epoll.poll(1)

            for fd, event in events:
                # ファイルディスクリプタがeth0のものと等しい場合
                if fd == eth0_socket.fileno():
                    p_eth0 = eth0_socket.recv()

                    if p_eth0:
                        # eth0で受信したパケットをeth1で送信
                        eth1_socket.send(p_eth0.original)

                # ファイルディスクリプタがeth1のものと等しい場合
                elif fd == eth1_socket.fileno():
                    p_eth1 = eth1_socket.recv()

                    if p_eth1:
                        # eth1で受信したパケットをeth0で送信
                        eth0_socket.send(p_eth1.original)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 
ためしに、epoll.poll()で取得したfdとeventの値を見ると、

print '--- fd type: {}'.format(type(fd))
# => fd type: <type 'int'>
print '--- event type: {}'.format(type(event))
# => event type: <type 'int'>

でした。

 

マルチスレッド:threadingモジュールでの実装

今度はマルチスレッドの threading モジュールを使ってみます。

 
threadingモジュールを使う場合、 Ctrl +C で終了できなくなりますが、今回はデーモンスレッドで動かすことで回避しました。

 
ただ、デーモンスレッドの場合、

注釈 デーモンスレッドは終了時にいきなり停止されます。デーモンスレッドで使われたリソース (開いているファイル、データベースのトランザクションなど) は適切に解放されないかもしれません。きちんと (gracefully) スレッドを停止したい場合は、スレッドを非デーモンスレッドにして、Event のような適切なシグナル送信機構を使用してください。

https://docs.python.jp/3/library/threading.html#thread-objects

に注意します。今回は関係なさそうなので、気にしないことにします。

scapy_bridge_threading.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import threading


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        bridge_eth0 = threading.Thread(target=bridge_from_eth0_to_eth1)
        bridge_eth1 = threading.Thread(target=bridge_from_eth1_to_eth0)

        # 今回はいきなり止まっても問題ないため、デーモンモードで動くようにする
        # https://docs.python.jp/3/library/threading.html#thread-objects
        bridge_eth0.daemon = True
        bridge_eth1.daemon = True

        # スレッドを開始
        bridge_eth0.start()
        bridge_eth1.start()

        # KeyboardInterruptを受け付けるよう、join()を秒指定で使う
        bridge_eth0.join(5)
        bridge_eth1.join(5)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 

マルチプロセス:multiprocessでの実装

次はマルチプロセスにて実装します。

 
繰り返しとなりますが、I/Oバウンドではマルチプロセス化しても効果的な処理になりません。今回は書いてみたかったという理由だけで実装しています。

scapy_bridge_multiprocess.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import multiprocessing


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # プロセスを用意
        bridge_eth0 = multiprocessing.Process(target=bridge_from_eth0_to_eth1)
        bridge_eth1 = multiprocessing.Process(target=bridge_from_eth1_to_eth0)

        # プロセスを開始
        bridge_eth0.start()
        bridge_eth1.start()

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

スレッドプール:concurrent.futures.ThreadPoolExecutorでの実装

Python3.2より登場した concurrent.futures モジュールの ThreadPoolExecutor を使ってスレッドプール版を実装します。
17.4. concurrent.futures – 並列タスク実行 — Python 3.6.3 ドキュメント

ただ、Python2では標準モジュールに入っていないため、バックポートされたモジュール futuresPyPIからインストールする必要があります。
https://pypi.python.org/pypi/futures

pi@raspberrypi:~/router_jisaku $ sudo pip install futures
Collecting futures
  Using cached futures-3.2.0-py2-none-any.whl
Installing collected packages: futures
Successfully installed futures-3.2.0

 
実装です。

scapy_bridge_ThreadPoolExecutor.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import concurrent.futures


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)
        executor.submit(bridge_from_eth0_to_eth1)
        executor.submit(bridge_from_eth1_to_eth0)

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

プロセスプール:concurrent.futures.ProcessPoolExecutorでの実装

こちらもバックポートされたモジュール futures を使います。

ProcessPoolExecutorでの実装ですが、

  • I/Oバウンドでは効果的な処理にならない
  • PyPIにある注釈によると、クリティカルなところでは使うべきでないとのこと

より、マルチプロセス版同様、実装してみたかっただけです。

scapy_bridge_ProcessPoolExecutor.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import concurrent.futures


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)
        executor.submit(bridge_from_eth0_to_eth1)
        executor.submit(bridge_from_eth1_to_eth0)

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

参考資料

 

ソースコード

GitHubに上げました。ディレクトscapy_python2/bridge/ にある

  • scapy_bridge_select.py
  • scapy_bridge_epoll.py
  • scapy_bridge_threading.py
  • scapy_bridge_multiprocess.py
  • scapy_bridge_ThreadPoolExecutor.py
  • scapy_bridge_ProcessPoolExecutor.py

が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku

*1:正確には、 asyncioにはバックポートプロジェクトのTrolliusがあります。ただ、実装でハマったときに大変そうなので、今回実装するのはやめておきます。https://github.com/vstinner/trollius

Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku

以前、Windows10 + Scapyを簡単にさわってみました。
Windows10にScapyをインストールする - メモ的な思考的な

 
もう少し詳しくScapyをさわってみたいと思い、何か作ろうと考えました。

作る題材を探していたところ、書籍「ルーター自作でわかるパケットの流れ」に出会いました。
ルーター自作でわかるパケットの流れ ~ソースコードで体感するネットワークのしくみ:書籍案内|技術評論社

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

 
書籍ではC言語を使ってパケットを扱い、ブリッジやルーターを自作していました。パケットを扱うならScapyでも書けるのではと思いました。

そこで、Python2 + Scapyを使って、Raspberry Pi 2 Model B をブリッジにできるか試してみました。

 
目次

 

環境

今回は 192.168.10.0/24 のローカル環境にて

  • Mac = HTTPサーバ
  • 無線LANアクセスポイント = スイッチ
  • ラズパイ = ブリッジ
  • Windows10 = ブリッジにぶら下げたクライアント

の役割を持たせました。

 

ネットワーク構成
-----------------------------------------
Mac
(`en0` : WiFi)
-----------------------------------------
|
|
-----------------------------------------
(無線)
無線LANアクセスポイント
(スイッチ)
-----------------------------------------
|
|
-----------------------------------------
(`eth0` : オンボードLANアダプタ)
Raspberry Pi 2 Model B
(`eth1` : 外付けUSB有線LANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
(オンボードLANアダプタ)
Windows10
-----------------------------------------

 

ネットワークアダプタ設定

明示的に設定したネットワークアダプタ設定です。

なお、以降の表記は

とします。

機器 アダプタ IPアドレス DGW DNS
Mac en0 192.168.10.101/24 192.168.10.1 192.168.10.1
無線LAN AP 192.168.10.1/24
ラズパイ eth0 192.168.10.50/24 192.168.10.1
ラズパイ eth1
Windows 192.168.10.201/24 192.168.10.1 192.168.10.1

 

各機器の構成

 
なお、Scapyは2つあります。

ただ、何かあった時に

  • 自分の書き方が悪い
  • scapy-python3の非互換なところ

の区別がつかないため、今回はPython2版のScapyを使いました。

 

ラズパイのセットアップ

以前ラズパイは使用していましたが、2015/7以降使っていなかったため、改めてラズパイをセットアップします。

 

メモリカードの調達

以前は、bootとrootを分けたファイルシステムを使っていました。
Raspberry Pi 2 + Raspbianで、microSDをboot、USBメモリをrootというファイルシステムにする - メモ的な思考的な

ただ、セットアップするのがめんどうなので、相性の良さそうなメモリカードを調達することにしました。

安価で入手しやすく動作報告の多いものを調べたところ、東芝MSDAR40N08G が良さそうでした。

年末年始だったので家電量販店に行きましたが、普通に売っていました。

 

Raspbianのインストール

公式のインストールガイドに従い、Raspbianをインストールしました。
Installing operating system images - Raspberry Pi Documentation

  • RASPBIAN STRETCH WITH DESKTOP のzipファイルをダウンロード
  • MacUSBメモリカードリーダーを接続し、Etcher でzipファイルをmicroSDに書き込む

 

Raspbianの初期セットアップ

ラズパイに microSDを挿入し、外付けキーボード・HDMI接続のモニタ・LANケーブル・外付けUSB有線LANアダプタをつないで、GUIで初期セットアップします。

USBマウスは手元に無かったものの、キーボード操作で何とかなりました。

 

キーボードレイアウトの変更

OADG 109A を選びました。

 

vimのインストール
pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install vim

 

ネットワークアダプタ固定IPアドレス割り当て

今回はブリッジなため、ラズパイのネットワークアダプタにはIPアドレスがいらない気がしました。

ただ、SSHを使ってMac上のターミナルからPythonコードを書きたいため、ラズパイのネットワークアダプタIPアドレスを割り当てます。

設定前のラズパイのネットワークアダプタを見ると

pi@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
...

eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
...

でした。

今回は、ラズパイのオンボードLANアダプタをMacに接続するため、 eth0固定IPアドレスを割り当てます。

 
以前ラズパイに固定IPアドレスを割り当てた時は、 /etc/network/interfaces に追加していました。
Raspberry Pi 2 Model BにRaspbianをセットアップした - メモ的な思考的な

ただ、現在はDebian Stretch (Linux 4.9 カーネル)がベースなため、設定ファイルが /etc/dhcpcd.conf に変更となっています。

pi@raspberrypi:~ $ vi /etc/dhcpcd.conf

# 末尾に以下を追加
interface eth0
static ip_address=192.168.10.50/24
static routers=192.168.10.1
static domain_name_servers=192.168.10.1 

 
設定を反映・確認します。

# 設定を反映
pi@raspberrypi:~ $ sudo service dhcpcd reload
sending signal HUP to pid 323

# 設定内容を確認
pi@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.50  netmask 255.255.255.0  broadcast 192.168.10.255

 

SSHを有効化

raspi-config にて有効化します。

pi@raspberrypi:~ $ sudo raspi-config

5 Interfacing Options > P2 SSH にて enabled にします。

 
MacからラズパイにSSHしてみましたが、問題なく動作しました。  

$ ssh pi@192.168.10.50
pi@192.168.10.50's password: (raspberryと入力)
Linux raspberrypi 4.9.59-v7+ #1047 SMP Sun Oct 29 12:19:23 GMT 2017 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Jan  1 23:37:50 2018 from 192.168.10.101

SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.

pi@raspberrypi:~ $ 

 

Scapyのインストール

ラズパイでScapyを実行する場合、 sudo python <scapyのコード> とします。

そのため、Scapyはsudoでインストールします。

# pythonのバージョンを確認
pi@raspberrypi:~ $ sudo python --version
Python 2.7.13

# Scapyのインストール
(env) pi@raspberrypi:~ $ sudo pip install scapy
Collecting scapy
  Downloading scapy-2.3.3.tgz (1.4MB)
    100% |████████████████████████████████| 1.4MB 84kB/s 
Building wheels for collected packages: scapy
  Running setup.py bdist_wheel for scapy ... done
  Stored in directory: /root/.cache/pip/wheels/bd/cf/05/d5abc9b4434f39ffe231517dfb8dab96241fef6a99459051f9
Successfully built scapy
Installing collected packages: scapy
Successfully installed scapy-2.3.3

以上でラズパイのセットアップは完了です。

 

疎通確認の準備

MacでローカルHTTPサーバを起動

ブリッジを介しても通信できるか確認するため、MacPythonのHTTPサーバを起動しておきます。

# Python2の場合
$ python -m SimpleHTTPServer

# Python3の場合
$ python -m http.server

 

Windowsから通信できないことを確認

WindowsからMacPython HTTPサーバへcurlを実行し、通信できないことを確認します。

> curl -m5 http://192.168.10.101:8000
curl: (28) Connection timed out after 5015 milliseconds

 

Scapyの「bridge_and_sniff」関数でラズパイをブリッジ化

ここまででラズパイの準備ができました。次はラズパイをScapyでブリッジにしてみます。

Scapyで便利な関数がないかを探したところ、 scapy/scapy/sendrecv.pybridge_and_sniff() 関数がありました。
https://github.com/secdev/scapy/blob/v2.3.3/scapy/sendrecv.py#L638

 
そこで、ラズパイ上に以下のファイルを作成し、ブリッジにできるかを試してみます。

scapy_bridge_and_sniff.py

# -*- coding: utf-8 -*-
from scapy.sendrecv import bridge_and_sniff


if __name__ == '__main__':
    print '>----- enable bridge -----'
    bridge_and_sniff('eth0', 'eth1')
    print '<----- disable bridge -----'

 
続いて、上記のファイルを実行します。rootでないと動作しないため、 sudo を忘れずに行います。

pi@raspberrypi:~/router_jisaku $ sudo python scapy_bridge_and_sniff.py
>----- enable bridge -----

 
動作確認のため、Windowsからcurlを実行します。

なお、今回はタイムアウトを5秒( -m 5 ) としました。ただ、通信状況によっては5秒以内には完了しないため、必要に応じてタイムアウトを延長します。

>curl -m5 http://192.168.10.101:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...
</html>

HTMLが返ってきました。通信できたようです。

 
Mac上のHTTPサーバのログにも通信ログがありました。

192.168.10.201 - - [02/Jan/2018 10:30:59] "GET / HTTP/1.1" 200 -

 
スクリプト scapy_bridge_and_sniff.py を停止した時の挙動を見てみます。

ラズパイにて Ctrl + Cスクリプトを停止します。

pi@raspberrypi:~/router_jisaku $ sudo python scapy_bridge_and_sniff.py
>----- enable bridge -----
^C<----- disable bridge -----

 
Windowscurlで確認します。

>curl -m5 http://192.168.10.101:8000
curl: (28) Connection timed out after 5000 milliseconds

接続できなくなっていました。

 
以上より、Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできました。

ただ、今回はScapyの bridge_and_sniff() 関数を使うだけだったので、もう少し他の実装を考えてみます。

 

ソースコード

GitHubに上げました。scapy_python2/bridge/scapy_bridge_and_sniff.py が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku

2017年の振り返りと2018年の目標

例年通り、2017年の振り返りと2018年の目標を書いてみます。

 

目標の振り返り

2016年の振り返りと2017年の目標 - メモ的な思考的な で立てた目標を振り返ってみます。

 

Pythonの基礎知識を身につける

一年間Pythonの仕事であったため、仕事で使う部分を中心に基礎知識が増えました。

ただ、Pythonの世界はまだまだ広いので、これに安住することなく手を動かし続けないとマズイとも感じました。

 

MacLinuxに慣れる

2017年はターミナル生活が長かったこともあり、日常生活で困らない程度には慣れてきました。

 

生活リズムを整える

転職後一年以上経過したこともあり、日々の生活リズムが整ってきました。

朝型生活になるかなと思いましたが、意外と夜の時間も使っていました。

 

その他

GitHub

f:id:thinkAmi:20171231224719p:plain

 
初めてのプルリク&マージも経験しました。
初めてのOSSへのプルリクとマージ - メモ的な思考的な

 

イベント

2017年もいろいろと参加していました。

  1. デブサミ2017の二日目に参加しました #devsumi - メモ的な思考的な
  2. #stapy #glnagano 第15回みんなのPython勉強会リモート中継 in GEEKLAB.NAGANO に参加しました - メモ的な思考的な
  3. #jawsdays JAWS DAYS 2017に参加しました - メモ的な思考的な
  4. #stapy #glnagano みんなのPython勉強会 in 長野#1に参加しました - メモ的な思考的な
  5. #stapy #glnagano みんなのPython勉強会リモート中継 in GEEKLAB.NAGANO#16 に参加しました - メモ的な思考的な
  6. オープンハードカンファレンス2017 Naganoに参加しました - メモ的な思考的な
  7. Google I/O 2017 Extended Live Viewing 信州に参加しました - メモ的な思考的な
  8. #pyconjp PyCon JP 2017に参加しました - メモ的な思考的な
  9. #stapy 第29回みんなのPython勉強会に参加しました & LTしました - メモ的な思考的な

 
また、縁あってPython Boot CampにTAとして2回参加しました。

  1. #pycamp #glnagano #nseg Python Boot Camp in 長野にTAとして参加しました - メモ的な思考的な
  2. #pycamp Python Boot Camp in 長野八ヶ岳にTAとして参加しました - メモ的な思考的な

 
その他、ほとんど記録していませんでしたが、平日の読書会にも参加しました。

  1. 第8回 SQLアンチパターン読書会に参加しました - メモ的な思考的な
  2. #nseg 第7回「オブジェクト指向設計実践ガイド」読書会に参加しました - メモ的な思考的な

 

アドベントカレンダー

Robot Framework のアドベントカレンダーを立てました & 書きました。
Robot Framework Advent Calendar 2017

アドベントカレンダー用に調べたり、自分以外に執筆してくださった方のネットワークネタが参考になったりと、立てて良かったです。

 

2018年の目標っぽいもの

今のところPython中心の生活なため、

  • Pythonを通じて、いろいろな分野の基礎固め

を目標っぽいものにします。

素振りして、何かしらの形としてアウトプットできればと。

 
というところで、今年もよろしくお願いします。

Robot FrameworkのSetupやTeardownでは、キーワードが1つしか指定できない

Robot Frameworkで、xUnit系にあるようなSetupやTeardownを使うことで、テストの実行前や実行後に必ず実施したいキーワードを指定できます。

 
ただ、SetupやTeardownではキーワードが1個しか指定できません。

そのことをいつも忘れるため、メモしておきます。

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2

 

実験

テストケース

  • SetupとTeardownのキーワードを1つにした場合
  • SetupとTeardownのキーワードを2つにした場合
  • SetupとTeardownのキーワードを1つにまとめた場合
  • SetupとTeardownのキーワードをRun Keywordsで複数同時に起動した場合

を作ってみます。

*** Keywords ***
セットアップキーワードをまとめた
    Log To Console  ${\n}セットアップ1です
    Log To Console  セットアップ2です

ティアダウンキーワードをまとめた
    Log To Console  ティアダウン1です
    Log To Console  ティアダウン2です


*** Test Cases ***

SetupとTeardownのキーワードを1つにした場合
    [Setup]  Log To Console  ${\n}セットアップです
    [Teardown]  Log To Console  ティアダウンです
    Log To Console  テスト本体です


SetupとTeardownのキーワードを2つにした場合
    [Setup]  Log To Console  ${\n}セットアップ1です  Log To Console  セットアップ2です
    [Teardown]  Log To Console  ティアダウン1です  Log To Console  ティアダウン2です
    Log To Console  テスト本体です


SetupとTeardownのキーワードを1つにまとめた場合
    [Setup]  セットアップキーワードをまとめた
    [Teardown]  ティアダウンキーワードをまとめた
    Log To Console  テスト本体です


SetupとTeardownのキーワードをRun Keywordsで複数同時に起動した場合
    [Setup]  Run Keywords  Log To Console  ${\n}セットアップ1です  AND  Log To Console  セットアップ2です
    [Teardown]  Run Keywords  Log To Console  ティアダウン1です  AND  Log To Console  ティアダウン2です
    Log To Console  テスト本体です

 
なお、Run Keywordsでは複数のキーワードを指定できます。

ただ、キーワードに引数がある場合には、キーワードの区切りに対して明示的に AND を入れる必要があります。

 
実行してみます。

$ robot test_setup_teardown.robot 
======================
Test Setup Teardown
======================
SetupとTeardownのキーワードを1つにした場合                            
セットアップです
.テスト本体です
.ティアダウンです
SetupとTeardownのキーワードを1つにした場合    | PASS |
-------------------------------
SetupとTeardownのキーワードを2つにした場合
セットアップ1です.テスト本体です
SetupとTeardownのキーワードを2つにした場合    | PASS |
-------------------------------
SetupとTeardownのキーワードを1つにまとめた場合
セットアップ1です
セットアップ2です
.テスト本体です
.ティアダウン1です
ティアダウン2です
SetupとTeardownのキーワードを1つにまとめた場合    | PASS |
--------------------------------
SetupとTeardownのキーワードをRun Keywordsで複数同時に起動した場合
セットアップ1です
セットアップ2です
.テスト本体です
.ティアダウン1です
ティアダウン2です
SetupとTeardownのキーワードをRun Keywordsで複数同時に起動した場合    | PASS |
---------------------------------
Test Setup Teardown    | PASS |
4 critical tests, 4 passed, 0 failed
4 tests total, 4 passed, 0 failed

実行結果より、 SetupとTeardownのキーワードを2つにした場合 の場合のみ、最初のキーワードしか実行されていないようです。

ただ、テスト自体はパスしてしまうため、そのことに気づきにくいです。

そのため、SetupとTeardownにはキーワードが1つしか指定できないことを覚えておいたほうが良さそうです。

 

ソースコード

GitHubに上げました。 builtin_library_samples/test_setup_teardown.robot ファイルが、今回作成したファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

Robot Frameworkでは、埋め込み引数とArgments設定のある引数を同時に使えない

Robot Frameworkでは、キーワードの仮引数を使う方法として

  • 埋め込み引数
  • [Arguments] 設定のある引数

の2つがあります。

以前、このあたりで試しました。

 
ただ、両方を使おうとするとエラーとなるため、注意が必要です。

試しに、

*** Keywords ***
埋め込みキーワード「${embed}」のみ
    log to console  ${embed}

Argumentsのみ
    [Arguments]  ${arg}
    log to console  ${arg}

キーワード「${embed}」とArgumentsの両方
    [Arguments]  ${arg}
    log to console  ${embed}
    log to console  ${arg}


*** Test Cases ***

埋め込み引数のみのキーワードを使う
    埋め込みキーワード「埋め込み」のみ

Arguments設定がある引数のみのキーワードを使う
    Argumentsのみ  arg=Argument設定

両方の引数を使う
    キーワード「埋め込み」とArgumentsの両方  arg=Argument設定

というテストケースを用意します。

 
実行してみます。

$ robot test_keyword_args.robot 
[ ERROR ] Error in test case file '/path/to/builtin_library_samples/test_keyword_args.robot': Creating keyword 'キーワード「${embed}」とArgumentsの両方' failed: Keyword cannot have both normal and embedded arguments.
========================
Test Keyword Args
========================
埋め込み引数のみのキーワードを使う    埋め込み
埋め込み引数のみのキーワードを使う    | PASS |
---------------------------------
Arguments設定がある引数のみのキーワードを使う    Argument設定
Arguments設定がある引数のみのキーワードを使う    | PASS |
---------------------------------
両方の引数を使う    | FAIL |
No keyword with name 'キーワード「埋め込み」とArgumentsの両方' found.
---------------------------------
Test Keyword Args    | FAIL |
3 critical tests, 2 passed, 1 failed
3 tests total, 2 passed, 1 failed

と、両方使った場合のみ、エラーが発生しました。

 
エラーメッセージにも

Keyword cannot have both normal and embedded arguments.

とあり、どちらかしか使えないことがわかります。

 
両者の差は

埋め込み引数には、通常のキーワードの引数で使えるような、デフォルト値や可変個の引数のサポートがありません。 キーワードを呼ぶときに、変数を指定するのはもちろん可能ですが、テストの可読性を下げてしまうかもしれません。 埋め込み引数を使えるのは、ユーザキーワードだけです。

基本の書き方 - キーワード名の中に引数を埋め込む - ユーザキーワードを定義する — RobotFramework和訳・日本語ドキュメント集

です。

そのため、両者は

  • 引数のデフォルト値や可変個の引数が必要か
  • キーワードの可読性をどうするか

を考慮して使い分ければ良さそうです。

 

ソースコード

GitHubに上げました。 builtin_library_samples/test_keyword_args.robot ファイルが、今回作成したファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

Robot Frameworkのキーワード、Run Keyword IfのELSEは大文字で書く

Robot Frameworkで、Run Keyword If でif以降の条件マッチとする場合、

  • ELSE IF
  • ELSE

を使います。

この ELSE IFELSE ですが、Builtinライブラリの説明には、

ELSE や ELSE IF は *args に指定し、厳密に ELSE, ELSE IF と書かねばなりません。

http://robotframework-ja.readthedocs.io/ja/latest/lib/BuiltIn.html#run-keyword-if http://robotframework.org/robotframework/latest/libraries/BuiltIn.html#Run%20Keyword%20If

とありました。

キーワードは UPPER CASE、camelCase、lower case のそれぞれで書いた時にも同じ挙動をしています。

なぜELSEやELSE IFだけは挙動が違うのかと思い、試してみました。

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2

 

UPPER CASEで書いた場合

*** Keywords ***
ELSEをUPPER CASEで書く
    Run Keyword If  0 == 1  Log To Console  IFの表示
    ...  ELSE  Log To Console  ELSEの表示

ELSE IFとELSEをUPPER CASEで書く
    Run Keyword If  0 == 1  Log To Console  IFの表示
    ...  ELSE IF  0 == 2  Log To Console  ELSE IF の表示
    ...  ELSE  Log To Console  ELSE IFの表示

*** Test Cases ***

それぞれの出力を比べる
    Log To Console  ${SPACE}
    ELSEをUPPER CASEで書く
    ELSE IFとELSEをUPPER CASEで書く

 
実行結果です。テストがパスしました。

$ robot test_else_upper_camel_lower_case.robot 
==========================
Test Else Upper Camel Lower Case
==========================
それぞれの出力を比べる
.ELSEの表示
.ELSE IFの表示
それぞれの出力を比べる    | PASS |

 

Pascal Caseで書いた場合

*** Keywords ***
ELSEをPascal Caseで書く
    Run Keyword If  0 == 1  Log To Console  これは表示されないはず
    ...  Else  Log To Console  Elseの表示

ELSE IFとELSEをPascal Caseで書く
    Run Keyword If  0 == 1  Log To Console  これは表示されないはず
    ...  Else If  0 == 2  Log To Console  これも表示されないはず
    ...  Else  Log To Console  Else Ifの表示

*** Test Cases ***

それぞれの出力を比べる
    Log To Console  ${SPACE}
    ELSEをPascal Caseで書く
    ELSE IFとELSEをPascal Caseで書く

 
実行結果です。

$ robot test_else_upper_camel_lower_case.robot 
...
Pascal Caseで書く    | PASS |

テストはパスしたものの、Log To Consoleの結果が出力されていません。

 
今は else以降で条件にマッチしましたが、 ifでマッチするように変えてみます。

ELSEをPascal Caseで書く
    Run Keyword If  1 == 1  Log To Console  Ifの表示
    ...  Else  Log To Console  Elseの表示

 
実行してみます。

$ robot test_else_upper_camel_lower_case.robot 
...
Pascal Caseで書く    | FAIL |
Keyword 'BuiltIn.Log To Console' expected 1 to 3 arguments, got 4.

テストが失敗しました。シンタックスエラーが発生しているようです。

 

lower caseで書いた場合

同じく、lower caseで書いてみた場合を試します。

*** Keywords ***
ELSEをlower caseで書く
    Run Keyword If  0 == 1  Log To Console  ifの表示
    ...  Else  Log To Console  elseの表示

ELSE IFとELSEをlower caseで書く
    Run Keyword If  0 == 1  Log To Console  ifの表示
    ...  else if  0 == 2  Log To Console  else ifの表示
    ...  else  Log To Console  elseの表示

*** Test Cases ***

lower caseで書く
    ELSEをlower caseで書く
    ELSE IFとELSEをlower caseで書く

 
実行してみます。

$ robot test_else_upper_camel_lower_case.robot 
...
# elseに条件がマッチする場合
lower caseで書く    | PASS |

# ifに条件がマッチする場合
lower caseで書く    | FAIL |
Keyword 'BuiltIn.Log To Console' expected 1 to 3 arguments, got 4.

Pascal Caseと同じ結果となりました。

 
これらより、Run Keyword Ifの ELSE IFELSE はUPPER CASEで書く必要があると分かりました。

 

ソースコード

GitHubに上げました。builtin_library_samples/test_else_upper_camel_lower_case.robotファイルが、今回作成したファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

RobotFrameworkで、ifのあるキーワードの書き方の違いについて

RobotFrameworkのBuiltinライブラリには、Ifを使っているキーワードとして

  • Set Variable If
  • Run Keyword If

があります。

ただ、if/else の書き方が両者で異なるため、メモとして残しておきます。

 
目次

 

環境

  • Python3.6.3
  • RobotFramework 3.0.2

 

Set Variable If

条件によって変数の値を切り替える Set Variable If です。

switchっぽい書き方です。

${result} =  Set Variable If  ${random} == 0  ${\n}Zero
...          ${random} == 1  ${\n}One
...          ${\n}Other

なお、 ... は継続行文字となります(.の後ろの半角スペース2を含む)。

 

Run Keyword If

条件によって実行するキーワードを切り替える Run Keyword If です。

If/elseっぽい書き方です。

Run Keyword If  ${random} == 0  Log To Console  Zero
...  ELSE IF    ${random} == 1  Log To Console  One
...  ELSE  Log To Console  Other

 

ソースコード

GitHubに上げました。builtin_library_samples/test_if_else.robot が今回のファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples