Robot Framework + Libdocにて、自作ライブラリのドキュメントを作成する

この記事は 「Robot Framework Advent Calendar 2017 - Qiita」 の19日目の記事です。

Robot Framework Advent Calendar 2017の7日目でRobot Frameworkの自作ライブラリを作成しました。
Robot Frameworkのライブラリを自作する - メモ的な思考的な

その時は自作ライブラリにドキュメントを付けていませんでした。

今回は、Robot Frameworkに付属している Libdoc を使い、自作ライブラリのドキュメントを作成してみます。

 
目次

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2

 
なお、対象の自作ライブラリは、以下を拡張していきます。

MyLibrary.py

from robot.api import logger


class MyLibrary:
    def hello_world(self, name='foo'):
        logger.console(f'hello, world {name} !')

 

Libdocでドキュメントを生成する

Libdoc では、ソースコード中のコメントを元にドキュメントを作成します。

最初なので hello, world的にLibdocでドキュメントを生成してみます。

# ディレクトリ構成を確認する
$ tree libdoc_sample/
libdoc_sample/
└── MyLibrary.py

0 directories, 1 file

# MyLibrary.pyファイルがあるディレクトリへ移動する
$ cd libdoc_sample/

# ヘルプで実行方法を確認する
$ python -m robot.libdoc --help
robot.libdoc -- Robot Framework library documentation generator

Version:  3.0.2 (Python 3.6.3 on darwin)

Usage:  python -m robot.libdoc [options] library output_file
...

# robot.libdocを実行すると、ドキュメントのHTMLが生成される
$ python -m robot.libdoc MyLibrary.py MyLibrary.html
/path/to/libdoc_sample/MyLibrary.html

# 中身を確認する
$ open MyLibrary.html 

 
できあがったHTMLはこんな感じです。

Robot Frameworkのライブラリドキュメントでよく見かけるフォーマットです。

f:id:thinkAmi:20171217153012p:plain:w300

 
引き続き、このドキュメントに追加していきます。

 

ドキュメントのシンタックスを決める

ドキュメントを書く前に、ドキュメントのシンタックスを決めておきます。

ユーザガイドによると、以下のシンタックスが利用できます。
5.1.3 Documentation syntax - Robot Framework User Guide

  • Robot Framework documentation syntax
  • HTML documentation syntax
  • Plain text documentation syntax
  • reStructuredText documentation syntax

 
なお、Robot Framework documentation syntax以外にする場合、 ROBOT_LIBRARY_DOC_FORMAT に使用するシンタックスを設定する必要があります。

今回はデフォルトの Robot Framework documentation syntax とします。

 

Introductionを記述

デフォルトでは

Documentation for test library <ライブラリ名>

となっている部分を記述します。

Pythonのクラスコメントを追加し、ドキュメントを生成します。

class MyLibrary:
    """マイライブラリ"""
    def hello_world(self, name='foo'):
        logger.console(f'hello, world {name} !')

 
生成されたものを見ると、Introductionが差し替わっていました。

f:id:thinkAmi:20171217153635p:plain:w150

 

キーワードのドキュメントを追加

ソースコード中のメソッドにコメントを追加します。

class MyLibrary:
    """マイライブラリ"""

    def hello_world(self, name='foo'):
        """ハローワールドを出力します"""
        logger.console(f'hello, world {name} !')

 
生成されたものを見ると、キーワードのドキュメントが追加されていました。

f:id:thinkAmi:20171217154044p:plain

 

Library scopeの変更

Library scopeの設定を変更することで、自動的に変更されます。

class MyLibrary:
    """マイライブラリ"""

    # Library scopeの設定を変更
    ROBOT_LIBRARY_SCOPE = 'TEST SUITE'

 
生成されたものです。

f:id:thinkAmi:20171217154650p:plain:w200

 

BoldやItalicでの文字装飾

コメント中に

  • Bold: *<文字列>*
  • Italic: _<文字列>_

と書くことで修飾できます。

class MyLibrary:
    """マイライブラリ

    *太字です*
    _イタリックです_
    普通です
    """
#...

 
生成されたものです。

f:id:thinkAmi:20171217155617p:plain:w250

 
なお、複数行の太字はうまくいきませんでした。

 

リスト表示

Markdownと同じように、 ハイフン - を先頭に付けることでリスト表示されます。

なお、リストのネストはなさそうです。

class MyLibrary:
    """マイライブラリ

    *太字です*
    _イタリックです_
    普通です

    - リスト1
    - リスト2
    """

 
生成されたものです。

f:id:thinkAmi:20171217172156p:plain:w250

 

URLのリンク

URLのリンクは以下の2種類となります。

  • https://google.co.jp のようなURLを書く
  • [https:/google.co.jp|Googleへ] のような記法にする

 
両者の違いを実際に見てみます。

class MyLibrary:
    """マイライブラリ

    *太字です*
    _イタリックです_
    普通です

    - リスト1
    - リスト2

    Googleへ https://google.co.jp
    こちらも [https://google.co.jp|Googleへ]
    """

 
生成されたものです。

f:id:thinkAmi:20171217172607p:plain:w300

 

キーワードへの内部リンク

バックスラッシュ (`) を使うことで、キーワードへのリンクとなります。

 

class MyLibrary:
    """マイライブラリ

    *太字です*
    _イタリックです_
    普通です

    - リスト1
    - リスト2

    Googleへ https://google.co.jp
    こちらも [https://google.co.jp|Googleへ]
    
    `Hello World` へ
    """

 
生成されたものです。

Introductionの Hello World をクリックすると、Keywordsの Hello World へとジャンプします。

クリックすると、このように青字でKeywordsがハイライトされます。

f:id:thinkAmi:20171217173046p:plain:w250

 

インラインコードスタイル

Markdownだとシングルバッククォート(`) で書きますが、Robot Frameworkではダブルバッククォート(``)で書きます。

class MyLibrary:
    """マイライブラリ

    *太字です*
    _イタリックです_
    普通です

    - リスト1
    - リスト2

    Googleへ https://google.co.jp
    こちらも [https://google.co.jp|Googleへ]

    `Hello World` へ

    ``インラインコードスタイル``

 
生成されたものです。

f:id:thinkAmi:20171217174108p:plain:w200

 

セクションへの自動リンク

セクション

  • Introduction
  • Importing
  • Shortcuts
  • Keywords

については、各単語をシングルバッククォートで囲むことで、自動的に内部リンクが作成されます。

class MyLibrary:
    """マイライブラリ

    セクションへのリンク

    - `introduction`
    - `importing`
    - `shortcuts`
    - `keywords`

 
生成されたものです。

f:id:thinkAmi:20171217174732p:plain:w250

 

カスタムセクションの作成とカスタムセクションへの内部リンク

カスタムセクションは、 = <セクション名> = で作成できます。

また、シングルバッククォートでセクション名を囲むことで、カスタムセクションへの内部リンクが作成されます。

class MyLibrary:
    """マイライブラリ

    = カスタムセクション =

    ここがカスタムセクション

    = 次のセクション =

    `カスタムセクション` へのリンク

 
生成されたものです。

f:id:thinkAmi:20171217175222p:plain:w250

 

表は以下で生成できます。

  • | で区切ると、1列追加
    • | と項目の間には半角スペース が必要
  • 列のタイトルは、 = で囲む
  • 空白セルは、| の間に半角スペースを入れる (| |)
class MyLibrary:
    """マイライブラリ

    | =タイトル= | =もう一つタイトル= |
    | 1行1列目 | 1行2列目 |
    | | 1列目が空白 |
    | 2列目が空白 | |

 
生成されたものです。

f:id:thinkAmi:20171217180012p:plain:w250

 

キーワードの引数表示について

Pythonの引数には

  • 引数なし
  • デフォルト値なしの引数
  • デフォルト値ありの引数
  • *args
  • **kwargs

があるため、それぞれを表示してみます。

class MyLibrary:
#...
    def no_args(self):
        pass
    
    def multi_args(self, one, two='2', *args, **kwargs):
        pass

 
生成されたものです。

f:id:thinkAmi:20171217180526p:plain

 
ざっと書きましたが、これで公式ドキュメントにあるようなライブラリのドキュメントが作れそうです。

 

ソースコード

GitHubに上げました。libdoc_sample ディレクリの中が今回のものです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

Mac + IntelliJ IDEA + Python pluginにおける、pycharm-debug.egg のありかについて

IntelliJ IDEA + Python pluginでリモートデバッグをする際、

  • pycharm-debug.egg
  • pycharm-debug-py3k.egg

が必要です。

ただ、どこにあるか分からなかったため、調べた時のメモを残します。

 

環境

 

ありか

PyCharmでは

  • /Applications/PyCharm.app/pycharm-debug.egg
  • /Applications/PyCharm.app/Contents/debug-eggs/pycharm-debug.egg

などにあったようです。
Where is the pycharm-debug.egg archive on mac found? - Stack Overflow

ただ、手元のIntelliJ IDEA + Python pluginでは見当たりませんでした。

 
そこで調べてみたところ、IntelliJ IDEAのヘルプに記載がありました。
Remote Debugging - Help | IntelliJ IDEA

~/Library/Application Support/IntelliJIDEAXXXX.X/python/ のようです。

 
findコマンドでも探してみます。

$ pwd
~/Library/Application Support

$ find . -name 'pycharm-*.egg'
./IntelliJIdea2016.2/python/pycharm-debug-py3k.egg
./IntelliJIdea2016.2/python/pycharm-debug.egg
./IntelliJIdea2016.3/python/pycharm-debug-py3k.egg
./IntelliJIdea2016.3/python/pycharm-debug.egg
./IntelliJIdea2017.1/python/pycharm-debug-py3k.egg
./IntelliJIdea2017.1/python/pycharm-debug.egg
./IntelliJIdea2017.2/python/pycharm-debug-py3k.egg
./IntelliJIdea2017.2/python/pycharm-debug.egg
./IntelliJIdea2017.3/python/pycharm-debug-py3k.egg
./IntelliJIdea2017.3/python/pycharm-debug.egg

 
手元にあるものだと、バージョン 2016.2 からこの場所にあるようです。

Robot FrameworkのキーワードEvaluateでPythonのコードを実行する

この記事は 「Robot Framework Advent Calendar 2017 - Qiita」 の17日目の記事です。

Robot Frameworkの組込キーワード Evaluate を使うとPythonコードを実行できます。

 
今回は Evaluate キーワードを試してみます。

 
目次

 

環境

 

文字列のフォーマット

Robot Framework単独では難しいのが文字列のフォーマットです。

一方、Pythonで書くと楽です。

今回は

  • %演算子
  • str.format()関数
  • f-strings
    • Python3.6以上

を試します。

Robotframework-DebugLibraryの rfdebug コマンドでREPLを起動し、それぞれを確認してみます。

$ rfdebug
====================
Robot Debug9 R36Le9
====================
RFDEBUG REPL
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.
> ${結果} =  Evaluate  '%06d %s' % (1, 'ゼロ埋めです')
# ${結果} = '000001 ゼロ埋めです'
> ${結果} =  Evaluate  'foo {}'.format('bar')
# ${結果} = 'foo bar'
> ${結果2} =  Evaluate  'foo {}'.format('bar')
# ${結果2} = 'foo bar'
> ${結果3} =  Evaluate  f'baz ${結果2}'
# ${結果3} = 'baz foo bar'

 
いずれも動作しました。

 

Pythonクラスのインスタンスを取得する

Evaluateのソースコードを読むと、内部ではPythoneval() 関数を使っています。
https://github.com/robotframework/robotframework/blob/3.0.2/src/robot/libraries/BuiltIn.py#L2963

また、Evaluateの引数のmoduleやnamespaceは __import__() 関数でimportされています。
https://github.com/robotframework/robotframework/blob/3.0.2/src/robot/libraries/BuiltIn.py#L2997

そのため、Pythonクラスのインスタンスを取得できそうです。

 

Robot FrameworkのLibraryで対象モジュールをimport済の場合
sys.modulesを使う

例えば、

*** Settings ***
Library  SeleniumLibrary
Library  DebugLibrary

*** TestCases ***
読み込み済モジュールに含まれるクラスをインスタンス化する
    Debug

とした時に、

  • SeleniumLibraryをimport済
  • Headless Firefoxの設定を行うために selenium.webdriver.firefox.options.Option クラスのインスタンスを取得したい

とします。

 
この場合、 sys.modulesselenium パッケージが追加されています。

 
そのため、

${options} =  Evaluate  sys.modules['selenium.webdriver.firefox.options'].Options()  sys

としてOptionsクラスのインスタンスを取得できます。

なお、引数は

  • 第1引数
    • sys.modulesselenium.webdriver.firefox.options にある Options クラスをインスタンス化する処理
  • 第2引数
    • 第1引数で使われているモジュールのうち、追加でimportが必要なモジュール名
      • 今回は sys モジュールを使用

です。

 
本当に取得できたのか、REPLで確認してみます。

# 上記例が含まれるRobot Frameworkのテストケースを実行
$ robot test_get_instance_from_already_imported_module.robot 
...

# キーワードDebugに到達したため、REPLが起動
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.

# Robot Frameworkライブラリの確認:SeleniumLibraryあり
> libs
< Imported libraries:
   BuiltIn 3.0.2
       An always available standard library with often needed keywords.
   DebugLibrary 1.0.2
       Debug Library for RobotFramework
   Easter 
   Reserved 
   SeleniumLibrary 3.0.1
       SeleniumLibrary is a web testing library for Robot Framework.
< Bultin libraries:
   BuiltIn 
   Collections 
   DateTime 
   Dialogs 
   Easter 
   OperatingSystem 
   Process 
   Remote 
   Reserved 
   Screenshot 
   String 
   Telnet 
   XML 

# sys.modulesにあるか確認:あった
> Evaluate  sys.modules['selenium.webdriver.firefox.options']  sys
< <module 'selenium.webdriver.firefox.options' from '/path/to/rfenv363/lib/python3.6/site-packages/selenium/webdriver/firefox/options.py'>

# 上記のコードを試す:インスタンスを取得できた
> ${options} =  Evaluate  sys.modules['selenium.webdriver.firefox.options'].Options()  sys
# ${options} = <selenium.webdriver.firefox.options.Options object at 0x10eee60b8>

   

Robot Frameworkを対象モジュールをimportしていない場合

例えば、

*** Settings ***
Library  DebugLibrary

*** TestCases ***
読み込まれていないモジュールに含まれるクラスをインスタンス化する
    Debug

とした時に、

とします。

 

sys.modulesを使う場合

sys.modulesを使う場合は、Libraryでimportした時と同じです。

# 上記のテストケースを実行
$ robot test_get_instance_from_no_import_module.robot 
...

# キーワードDebugに到達したため、REPLが起動
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.

# sysとdistutils.versionをimportして、LooseVersionクラスのインスタンスを取得
> Evaluate  sys.modules['distutils.version'].LooseVersion('3.0.2')  sys,distutils.version
< LooseVersion ('3.0.2')

 

__import__()関数を使う場合

Pythonでのimport同様、 __import__() 関数も使えます。
__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

ただ、

name 変数が package.module 形式であるとき、通常は、name で指名されたモジュール ではなく、最上位のパッケージ (最初のドットまでの名前) が返されます。しかしながら、空でない fromlist 引数が与えられると、 name で指名されたモジュールが返されます。

__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

のため、第1引数だけ指定すると

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  __import__('distutils.version').LooseVersion('3.0.2')
! keyword: Evaluate  __import__('distutils.version').LooseVersion('3.0.2')
! Evaluating expression '__import__('distutils.version').LooseVersion('3.0.2')' failed: AttributeError: module 'distutils' has no attribute 'LooseVersion'

と、エラーになります。

そのため、 fromlist 引数も使用します。

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  __import__('distutils.version', fromlist=['LooseVersion']).LooseVersion('3.0.2')
< LooseVersion ('3.0.2')

 

importlib.import_module()を使う場合

__import__() 関数の説明の中で

__import__() を直接使用することも推奨されず、 importlib.import_module() の方が好まれます。

__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

とあったため、 importlib.import_module() を見てみます。

 
importlib.import_module()の説明には

import_module() 関数は importlib.__import__() を単純化するラッパーとして働きます。つまり、この関数のすべての意味は importlib.__import__() から受け継いでいます。これらの2つの関数の最も重要な違いは、 import_module() が指定されたパッケージやモジュール (例えば pkg.mod) を返すのに対し、 __import__() はトップレベルのパッケージやモジュール (例えば pkg) を返すことです。

importlib.import_module(name, package=None) - 31.5. importlib — import の実装 — Python 3.6.3 ドキュメント

とありました。

試してみます。

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  importlib.import_module('distutils.version').LooseVersion('3.0.2')  importlib
< LooseVersion ('3.0.2')

同じ結果になりました。

個人的には __import__() よりも importlib.import_module() の方が分かりやすいです。

 

ソースコード

GitHubに上げました。builtin_library_samples/evaluate ディレクリの中が今回のものです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

DebugLibraryを使って、Robot Framework のREPLやテスト実行中のREPLを試す

この記事は 「Robot Framework Advent Calendar 2017 - Qiita」 の16日目の記事です。

プログラム言語で何かをちょっと試したいという時は、REPL (Read-eval-print loop) が便利です。

Robot FrameworkでもREPLがないかを探したところ、Robotframework-DebugLibraryを使えば良さそうでした。
https://github.com/xyb/robotframework-debuglibrary

そこで今回は、Robotframework-DebugLibraryを使って、Robot Framework のREPLを試してみます。

 
目次

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2
  • Robotframework-DebugLibrary 1.0.2

Robotframework-DebugLibraryのインストールは pip install robotframework-debuglibrary でOKです。

 

コンソールでREPLを試す

起動

コンソールを起動し、 rfdebug コマンドを入力すると、REPLが起動します。

$ rfdebug
=========================
Robot Debugqqpsyuvs
=========================
RFDEBUG REPL
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.
> 

 

コンソール出力

コンソール出力してみます。

> Log To Console  ハローワールド
ハローワールド

 

変数を使う

変数を使ってみます。

> ${ham} =  Set Variable  spam
# ${ham} = 'spam'
> Log To Console  ${ham}
spam

 

入力履歴

履歴も残っているようで、同じコマンドを打つと、前回のものが表示されます。

f:id:thinkAmi:20171216074446p:plain

 
右矢印キーを押すと、補完されます。

f:id:thinkAmi:20171216080823p:plain

 

入力補完

ある程度入力し、 Tab キーを押すと、入力候補が表示されます。

f:id:thinkAmi:20171216074745p:plain

 

インポートされているライブラリの確認

インポートされているライブラリを見てみます。 libs (もしくは l ) で表示されます。

> libs
< Imported libraries:
   BuiltIn 3.0.2
       An always available standard library with often needed keywords.
   DebugLibrary 1.0.2
       Debug Library for RobotFramework
   Easter 
   Reserved 
< Bultin libraries:
   BuiltIn 
   Collections 
   DateTime 
   Dialogs 
   Easter 
   OperatingSystem 
   Process 
   Remote 
   Reserved 
   Screenshot 
   String 
   Telnet 
   XML 

 

ヘルプの表示

どんなコマンドが用意されているかヘルプを表示してみます。

> help
Input Robotframework keywords, or commands listed below.
Use "libs" or "l" to see available libraries,
use "keywords" or "k" see list of library keywords,
use the TAB keyboard key to auto complete keywords.

Documented commands (type help <topic>):
========================================
EOF  exit  help  k  keywords  l  libs  pdb  s  selenium

 
Seleniumがあるみたいです。さらにヘルプを表示してみます。

> help selenium
Start a selenium 2 webdriver and open url in browser you expect.

        s(elenium)  [<url>]  [<browser>]

        default url is google.com, default browser is firefox.

 
そういえば、SeleniumLibraryをインストール済でした。

(rfenv363) $ pip list
...
robotframework-seleniumlibrary (3.0.1)

 
そこで、Seleniumを使ってみます。

> selenium
# import library  Selenium2Library
! keyword: import library  Selenium2Library
! Importing test library 'Selenium2Library' failed: ModuleNotFoundError: No module named 'Selenium2Library'
Traceback (most recent call last):
  None
PYTHONPATH:
  /path/to/rfenv363/bin
...
  /path/to/rfenv363/lib/python3.6/site-packages
# open browser  http://www.google.com/  firefox
! keyword: open browser  http://www.google.com/  firefox
! No keyword with name 'open browser' found.

Selenium2Libraryをインストールしていないので、起動に失敗したようです。 手元のSeleniumLibraryではダメなようですね。

 

REPLの終了

Ctrl + D にて終了します。

> (Ctrl + D を入力)

>>>>> Exit shell.
RFDEBUG REPL    | PASS |
----------------------------
Robot Debug Xgd1T H    | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
====================
Output:  None

 

Robot Frameworkのテストを実行中に、REPLを試す

Robot Framworkのテストを実行中に、REPLを使ってデバッグしたいことがあるかもしれません。

テストケース中にて Debug キーワードを使うことで、その場所の状態でREPLが起動します。

以下のテストケースで試してみます。

*** Settings ***
Library  DebugLibrary
Library  SeleniumLibrary

*** Keywords ***
say
    Log To Console  hello, world

ハロー
    Log To Console  はろー

testハロー
    Log To Console  testです

*** Test Cases ***
何かテストする
    Log To Console  hello
    ハロー
    ${ham} =  Set Variable  変数です
    
    # この時点の状態で、REPLが起動する
    Debug

    ${spam} =  Set Variable  書き換え可能です

 

REPLの起動

テストを実行すると、 Debug キーワードがあるところで自動的に停止します。

$ robot test_debug_library.robot 
===========================
Test Debug Library
===========================
何かテストする    hello
.はろー
..
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.
> 

 

キーワードを使ってみる

さきほどのREPLと同じです。

> Log To Console  こんにちは
こんにちは

 

変数の中身を確認する

ためしに変数名を入力してみます。

> ${ham}
! keyword: ${ham}
! FAILED: TypeError("run_keyword() missing 1 required positional argument: 'name'",)

 
キーワードを入力しないといけないみたいですね。

> Log To Console  ${ham}
変数です

テスト中の変数の中身が出ました。

 

変数の中身を書きかえる

テストケース中の変数も書きかえられます。

> ${ham} =  Set Variable  書きかえました
# ${ham} = '書きかえました'
> 

 
REPLを抜けると変数が書きかわっています。

>>>>> Exit shell.
.書きかえました

 
参考:書きかえない場合

>>>>> Exit shell.
.変数です

 

ユーザ定義のキーワードを使う

Keywordssay キーワードを定義していたので使ってみます。

> say
hello, world

動きました。

日本語キーワードも問題ないです。

> ハロー
はろー

> testハロー
testです

 

importされているライブラリのキーワード使用

importされているライブラリのキーワードも使用できます。

今回はSeleniumLibraryをimportしているので、 Open Browser https://google.co.jp chrome とするとChromeが起動します。

> Open Browser  https://google.co.jp  chrome
< 1

 

入力補完

入力補完も可能です。

このテストケースではSeleniumLibraryをimportしているため、その分も候補に表示されます。

f:id:thinkAmi:20171216083740p:plain

 
なお、入力候補間の移動は

  • 進む : Tab
  • 戻る : Shift + Tab

です。

 

REPLの終了

Ctrl + D で終了します。

exit でも大丈夫なようです。

> help exit
Exit the interpreter. You can also use the Ctrl-D shortcut.

 
REPLを終了すると、テストが続きから実行されます。

> (Ctrl + Dを入力)

>>>>> Exit shell.
.変数です
何かテストする    | PASS |
--------------------
Test Debug Library    | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed

 
テストケース中でいろいろとできるのは便利ですね。

pabot を使って、Robot Framework のテストを並行で実行する

この記事は 「Robot Framework Advent Calendar 2017 - Qiita」 の15日目の記事です。

Robot Frameworkのテストケースが増えるにつれ、すべてが終わるまでに時間がかかるようになります。

この問題を解決する方法の一つとして、テストを並行で流すということが考えられます。

そこで今回は、Robot Frameworkのテストケースを並行で実行する pabot ライブラリを紹介します。

 
目次

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2
  • pabot 0.43

 

pabotについて

pabotGitHubにて公開されています。
mkorpela/pabot: Parallel executor for Robot Framework test cases.

インストールはpipで可能です。

$ pip install -U robotframework-pabot

 
READMEには

A parallel executor for Robot Framework tests. With Pabot you can split one execution into multiple and save test execution time.

とあります。

pabotを使うことで、Robot Frameworkのテストスイートが最小の並行処理単位となります。これにより、テストを並行で実行できるようです。

 

pabotで並行テストしてみる

テストケースがまったく同じである、2つのテストファイルを用意します。

ファイル名は

  • test_parallel1.robot
  • test_parallel2.robot

とします。

今回、開始時刻と終了時刻をログ出力するテストケースを用意しました。

*** Settings ***
Library  pabot.PabotLib
Library  DateTime


*** Keywords ***
${timing}時刻を出力する
    ${start} =  Get Current Date  result_format=datetime
    Log To Console  ${\n}${timing}:${start}


*** TestCases ***
並行処理テスト
    開始時刻を出力する
    終了時刻を出力する

 
pabotを使って並行実行してみます。

コマンドラインより、 pabot <テストスイートが含まれるディレクトリ> にて実行します。

$ pabot .
2017-12-15 12:59:46.495300 [PID:92080] [0] EXECUTING Parallel Test.Test Parallel1
2017-12-15 12:59:46.496719 [PID:92081] [1] EXECUTING Parallel Test.Test Parallel2
2017-12-15 12:59:47.222611 [PID:92080] [0] PASSED Parallel Test.Test Parallel1 in 0.7 seconds
2017-12-15 12:59:47.326672 [PID:92081] [1] PASSED Parallel Test.Test Parallel2 in 0.8 seconds
Output:  /path/to/parallel_test/output.xml
Log:     /path/to/parallel_test/log.html
Report:  /path/to/parallel_test/report.html
Elapsed time: 0 minutes 1.103 seconds

通常のRobot Frameworkのテストと同じく、

  • output.xml
  • log.html
  • report.html

が出力されました。

 
テスト実行ディレクトリを見ると、新しく pabot_results ディレクトリが作成されました。

f:id:thinkAmi:20171215194800p:plain:w250

 
その中の stdout.txt ファイルを開くと、通常ターミナルに出力される内容がテキストファイルとして出力されていました。

========================
Parallel Test
========================
Parallel Test.Test Parallel1
========================
並行処理テスト
開始:2017-12-15 12:59:47.016138

終了:2017-12-15 12:59:47.018580
| PASS |
---------------------------------
Parallel Test.Test Parallel1    | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
========================
Parallel Test    | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
========================
Output:  /path/to/parallel_test/pabot_results/Parallel Test.Test Parallel1/output.xml

 

pabot.PabotLibの利用

pabotはコマンドラインで利用するだけではありません。

pabot.PabotLib としてテストケースの中で参照することで、並行処理向けの便利なキーワードが追加されます。

追加されるキーワードについては、以下のドキュメントにまとまっています。 https://cdn.rawgit.com/mkorpela/pabot/master/PabotLib.html

今回は

  • テストケース間で設定ファイルを共有する
  • テストケース間で処理の排他制御を行う

を試してみます。

 

テストケース間で設定ファイルを共有する

並行実行するテストケース間で設定ファイルを共有したい場合、キーワード Acquire Value Set を使います。

テストケースでは

  • Acquire Value Set にて、設定ファイルの参照開始
  • Get Value From Set <変数名> にて、設定ファイルの変数名を取得する
  • Release Value Set にて、設定ファイルの参照解除

と使います。

*** Settings ***
Library  pabot.PabotLib
Library  DateTime


*** Keywords ***
${timing}時刻を出力する
    ${start} =  Get Current Date  result_format=datetime
    Log To Console  ${\n}${timing}:${start}


*** TestCases ***
ハローワールドする
    # ファイルの参照開始
    ${valueset_name} =  Acquire Value Set
    開始時刻を出力する

    # 読み込んだファイルの中から、hello を取り出して出力する
    ${hello} =  Get Value From Set  hello
    Log To Console  ${hello}

    # ファイルの参照終了
    Release Value Set
    終了時刻を出力する

 
実行時には、--resourcefile オプションを使用し、設定ファイルを指定します。

また、 --pabotlib を追加することで、PabotLibのリモートサーバが起動します。これにより、共有設定ファイルの参照や、処理の排他制御が可能になります。

$ pabot --pabotlib --resourcefile valueset.dat .

 
実行結果は以下となります。

$ pabot --pabotlib --resourcefile valueset.dat .
Robot Framework remote server at 127.0.0.1:8270 started.
2017-12-15 12:59:19.913537 [PID:92010] [1] EXECUTING Shared File Test.Test Shared File2
2017-12-15 12:59:19.915366 [PID:92011] [0] EXECUTING Shared File Test.Test Shared File1
2017-12-15 12:59:20.544879 [PID:92011] [0] PASSED Shared File Test.Test Shared File1 in 0.6 seconds
2017-12-15 12:59:22.517075 [PID:92010] [1] PASSED Shared File Test.Test Shared File2 in 2.5 seconds
Output:  /path/to/shared_file_test/output.xml
Log:     /path/to/shared_file_test/log.html
Report:  /path/to/shared_file_test/report.html
Stopping PabotLib process
Robot Framework remote server at 127.0.0.1:8270 stopped.
Elapsed time: 0 minutes 3.394 seconds

 
各テストスイートのログファイルにも、Log To Consoleで指定した設定ファイルの内容が出力されています。

===========================
Shared File Test.Test Shared File1
===========================
ハローワールドする                                                    
開始:2017-12-15 12:59:20.338099
Hello, world!

終了:2017-12-15 12:59:20.345375
| PASS |

 

テストケース間で、処理の排他制御を行う

テストケースの並行実行時に、複数の処理から同時に処理できないものがある場合、キーワード Acquire Lock を使って排他制御を行います。

これにより、

  • 1つのテストケースの中の処理だけ実行
  • 他のテストケースの処理は待機

となります。

なお、 Acquire Lock の引数で渡すロック名は、どのテストスイートでも同じ値を使用します。

また、ロックが終わったら、忘れずに Release Lock を実行します。

*** Settings ***
Library  pabot.PabotLib
Library  DateTime


*** Keywords ***
${timing}時刻を出力する
    ${start} =  Get Current Date  result_format=datetime
    Log To Console  ${\n}${timing}:${start}


*** TestCases ***

ロック待ちをするテスト
    # ロック開始
    Acquire Lock   MyLock1
    開始時刻を出力する
    sleep  5s
    終了時刻を出力する
    # ロック終了
    Release Lock   MyLock1

 
同じ内容のファイルを2つ用意し、実行してみます。

$ pabot --pabotlib .
Robot Framework remote server at 127.0.0.1:8270 started.
2017-12-15 12:47:10.654487 [PID:90778] [0] EXECUTING Lock Test.Test Pabot Lock1
2017-12-15 12:47:10.657422 [PID:90779] [1] EXECUTING Lock Test.Test Pabot Lock2
2017-12-15 12:47:16.572726 [PID:90778] [0] PASSED Lock Test.Test Pabot Lock1 in 5.7 seconds
2017-12-15 12:47:21.552718 [PID:90779] [1] PASSED Lock Test.Test Pabot Lock2 in 10.5 seconds
Output:  /path/to/lock_test/output.xml
Log:     /path/to/lock_test/log.html
Report:  /path/to/lock_test/report.html
Stopping PabotLib process
Robot Framework remote server at 127.0.0.1:8270 stopped.
Elapsed time: 0 minutes 12.257 seconds

 
それぞれのテストスイートが5秒以上の差を持って終了しています。

出力されたログを見てみると、

=====================
Lock Test.Test Pabot Lock1
=====================
ロック待ちをするテスト                                                
開始:2017-12-15 12:47:11.369327

終了:2017-12-15 12:47:16.372781
| PASS |

=====================
Lock Test.Test Pabot Lock2
=====================
ロック待ちをするテスト                                                
開始:2017-12-15 12:47:16.386501

終了:2017-12-15 12:47:21.388838
| PASS |

でした。

きちんとロック待ちをしていました。

 

ソースコード

GitHubに上げました。ディレクトpabot_sample の中が今回のファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

Robot Framework + FtpLibraryで、FTPサーバと通信する

この記事は Robot Framework Advent Calendar 2017 - Qiita の14日目の記事です。

今回はRobot Frameworkを使ってFTPサーバと通信してみます。

 
目次

 

環境

  • Python 3.6.3
  • Robot Framework 3.0.2
  • FtpLibrary 1.4
    • 後述の通り、現時点のリリース版ではPython3系に対応していないため、GitHubからインストール
  • FTPサーバ
    • pyftpdlib 1.5.3

 

FTPサーバの用意

pyftpdlibによる実装

今回は、PythonFTPサーバである pyftpdlib を使います。
giampaolo/pyftpdlib: Extremely fast and scalable Python FTP server library

 
GitHubにあるREADMEに従い、実装します。

FTPサーバの設定は

とします。

import pyftpdlib.authorizers
import pyftpdlib.handlers
import pyftpdlib.servers
import pathlib
import shutil


def run_server():
    # FTPサーバのルートディレクトリを作る
    # __file__のパスを絶対パスに変換し、ルートディレクトリのパスも追加する
    ftp_server_root_path = pathlib.Path(__file__).resolve().parent.joinpath('serv')
    # ディレクトリがある場合は、一度削除しておく
    if ftp_server_root_path.exists():
        # pathlibのrmdir()ではファイルがあると削除できないので、shutilを使う
        shutil.rmtree(ftp_server_root_path.as_posix())
    # ディレクトリを作って、パーミッションは777にしておく
    ftp_server_root_path.mkdir()
    ftp_server_root_path.chmod(0o777)
    print(ftp_server_root_path)

    # 認証ユーザーを作る
    authorizer = pyftpdlib.authorizers.DummyAuthorizer()
    authorizer.add_user('user', 'password', ftp_server_root_path.as_posix(), perm='elradfmw')

    # 個々の接続を管理するハンドラを作る
    handler = pyftpdlib.handlers.FTPHandler
    handler.authorizer = authorizer

    # FTPサーバーを立ち上げる
    # Unix系だと、21番ポートはsuper userがbindできるポートなので、それ以外のポートにする
    # https://github.com/giampaolo/pyftpdlib/blob/master/docs/faqs.rst#why-do-i-get-socket-error-permission-denied-error-on-ftpd-starting
    server = pyftpdlib.servers.FTPServer(("127.0.0.1", 12345), handler)
    server.serve_forever()


if __name__ == '__main__':
    run_server()

 
ちなみに、ファイルパス操作は pathlib モジュールを使いました。Python3.4から使えます。

例えば、ディレクトリの絶対パスを取得する場合、

# os.pathを使う場合
import os
path_by_os = os.path.abspath(os.path.dirname(__file__))

# pathlibを使う場合
import pathlib
path_by_pathlib = pathlib.Path(__file__).resolve().parent

となります。個人的には、pathlibの方が見やすいと感じました。

 

pyftpdlibの起動

上記スクリプトftp_server.py として保存し、起動します。

$ python ftp_server.py 
/path/to/ftp_library_sample/serv
[I 2017-12-14 05:37:03] >>> starting FTP server on 127.0.0.1:12345, pid=34184 <<<
[I 2017-12-14 05:37:03] concurrency model: async
[I 2017-12-14 05:37:03] masquerade (NAT) address: None
[I 2017-12-14 05:37:03] passive ports: None

 

Robot FrameworkのFTPLibraryを使う

インストール

Robot FrameworkでFTPサーバと通信するためには、 Robot-Framework-FTP-Library を使います。
https://github.com/kowalpy/Robot-Framework-FTP-Library

 
このライブラリはpipでインストールできます。

ただ、2017/12/14現在、PyPIにあるものはPython3系に対応していません*1

そのため、GitHubからインストールします。
Please support python3+ · Issue #10 · kowalpy/Robot-Framework-FTP-Library

$ pip install git+https://github.com/kowalpy/Robot-Framework-FTP-Library

 

Robot Frameworkのテストケースを実装

ドキュメントに従って実装します。
https://kowalpy.github.io/Robot-Framework-FTP-Library/FtpLibrary.html

今回は、

  • FTPサーバへの接続
  • FTPサーバへのアップロード
  • FTPサーバからのダウンロード

を実装してみます。

*** Settings ***
Library  FtpLibrary


*** Variables ***
${USER}  user
${PASSWORD}  password


*** TestCases ***
FTPサーバに接続する
    ftp connect  127.0.0.1  ${USER}  ${PASSWORD}  12345
    Mkd  foo
    # Welcome Messageを表示
    ${welcome} =  Get Welcome
    # 改行して見やすくする
    Log To Console  ${\n} ${welcome}
    # カレントディレクトリを表示
    ${path} =  Dir
    Log To Console  ${\n} ${path}
    FTP CLOSE

FTPサーバにファイルをアップロードする
    ftp connect  127.0.0.1  ${USER}  ${PASSWORD}  12345
    Mkd  upload
    Cwd  upload
    Upload File  result_google_python.png  uploaded.png
    FTP CLOSE

FTPサーバからファイルをダウンロードする
    ftp connect  127.0.0.1  ${USER}  ${PASSWORD}  12345
    # uploadディレクトリとupload.pngは前のテストで作成済なので、それを利用する
    Cwd  upload
    Download File  uploaded.png  downloaded.png
    FTP CLOSE

 

テストの実行

実行前のディレクトリの様子です。

f:id:thinkAmi:20171214210533p:plain:w250

 
テストを実行します。問題なく終了しました。

$ robot test_ftp.robot 
=========================
Test Ftp                 
=========================
FTPサーバに接続する      ...
 220 pyftpdlib 1.5.3 ready.
..
 ['drwxr-xr-x   2 you group  68 Dec 14 06:00 foo']
FTPサーバに接続する    | PASS |
-------------------------
FTPサーバにファイルをアップロードする    | PASS |
-------------------------
FTPサーバからファイルをダウンロードする    | PASS |
-------------------------
Test Ftp    | PASS |
3 critical tests, 3 passed, 0 failed
3 tests total, 3 passed, 0 failed
=========================

 
実行後のディレクトリの様子です。

アップロードとダウンロードがうまくいっているようです。

f:id:thinkAmi:20171214210601p:plain:w250

 
FTPサーバ側にもログが残っていました。

[I 2017-12-14 06:00:00] >>> starting FTP server on 127.0.0.1:12345, pid=34184 <<<
[I 2017-12-14 06:00:00] concurrency model: async
[I 2017-12-14 06:00:00] masquerade (NAT) address: None
[I 2017-12-14 06:00:00] passive ports: None
[I 2017-12-14 06:01:00] 127.0.0.1:55030-[] FTP session opened (connect)
[I 2017-12-14 06:01:00] 127.0.0.1:55030-[user] USER 'user' logged in.
[I 2017-12-14 06:01:00] 127.0.0.1:55030-[user] MKD /path/to/ftp_library_sample/serv/foo 257
[I 2017-12-14 06:01:00] 127.0.0.1:55030-[user] FTP session closed (disconnect).
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[] FTP session opened (connect)
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[user] USER 'user' logged in.
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[user] MKD /path/to/ftp_library_sample/serv/upload 257
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[user] CWD /path/to/ftp_library_sample/serv/upload 250
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[user] STOR /path/to/ftp_library_sample/serv/upload/uploaded.png completed=1 bytes=213754 seconds=0.014
[I 2017-12-14 06:01:00] 127.0.0.1:55033-[user] FTP session closed (disconnect).
[I 2017-12-14 06:01:00] 127.0.0.1:55036-[] FTP session opened (connect)
[I 2017-12-14 06:01:00] 127.0.0.1:55036-[user] USER 'user' logged in.
[I 2017-12-14 06:01:00] 127.0.0.1:55036-[user] CWD /path/to/ftp_library_sample/serv/upload 250
[I 2017-12-14 06:01:00] 127.0.0.1:55036-[user] RETR /path/to/ftp_library_sample/serv/upload/uploaded.png completed=1 bytes=213754 seconds=0.004
[I 2017-12-14 06:01:00] 127.0.0.1:55036-[user] FTP session closed (disconnect).

 

ソースコード

GitHubに上げました。ftp_library_sample ディレクトリの中が今回のファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

*1:pip installできますが、参照して実行すると「ailed: SyntaxError: Missing parentheses in call to 'print'. Did you mean print(int "Starting Robot Framework Ftp Library as a remote server ...")? (FtpLibrary.py, line 451) 」が発生します

Robot Framework向けのIDE、RED(Robot Editor)について

この記事は「Robot Framework Advent Calendar 2017 - Qiita」の13日目の記事です。

前回、エディタやIDEのRobot Frameworkプラグインを試してみました。
エディタやIDEのRobot Frameworkプラグインについて - メモ的な思考的な

 
Robot Framework専用のエディタがないかを調べたところ、

がありました。

ただ、RIDEはPython3.x系では動作しないとのことでした*1

Notice that similarly as Robot Framework, RIDE does not yet support Python 3. Notice also that on OS X RIDE requires 32-bit Python version.

https://github.com/robotframework/RIDE/wiki/Installation-Instructions

 
手元の環境がPython3.x系であるため、今回は RED (Robot Editor) を試してみました。

 
目次

 

環境

 
今回はMacにインストールしましたが、WindowsLinuxでも動作するようです。

 

REDのインストール

インストール方法は以下に記載がありました。

 
今回はREDを試すだけなので、一番お手軽なGitHubのzipを利用します。

REDのreleaseページより、環境に合わせて最新版をダウンロードします。
Releases · nokia/RED

Macであれば、 RED_0.8.1.20171123105248-macosx.cocoa.x86_64.zip をダウンロードします。

このzipには、Eclipse + RDE が含まれています。そのため、別途Eclipseをインストールする必要はありません。

もしEclipseが手元にある場合は、RDEだけをインストールすれば良いようです。

 
ダウンロードしたzipを展開した後に開くと、 開発元が未確認のため開けません。 というエラーメッセージが表示されました。

そのため、以下を参考にしてREDを実行しました。
macOS Sierra: 開発元が未確認のアプリケーションを開く

 

プロジェクトの作成

File > New > Robot Project を選択します。project nameは任意の名前をつけます。

 

Robot Frameworkのインストール

REDのワークスペースの中でRobot Frameworkをインストールすると、自動的に認識されます。

デフォルトのワークスペースは以下の通りです。

/path/to/rf_red/Eclipse.app/Contents/MacOS/workspace

 
ターミナルを使って、デフォルトのワークスペースの中で、Robot Frameworkとそのライブラリのインストールを行います。

# Python3の仮想環境を作成
$ python -m venv rfenv363

# 仮想環境を有効化
$ source rfenv363/bin/activate

# とりあえずRobot FrameworkとSeleniumLibraryを入れる
(rfenv363) $ pip install robotframework-selenium
...
Successfully installed robotframework-3.0.2 robotframework-seleniumlibrary-3.0.1 selenium-3.8.0

 
仮想環境のRobot Frameworkが認識されているかを、 環境設定 > Robot Framework > Install Framework にて確認します。ワークスペースの中であれば、自動的に認識されているはずです。

もし存在しなければ、先ほど作成した仮想環境の bin ディレクトリを指定します。

 

補完設定

設定内容

以下を参考に、デフォルトから補完設定を修正します。
Eclipseの補完設定をカスタマイズして爆速コーディング - ser1zw's blog

REDのRobot Framework設定はこんな感じでした。

f:id:thinkAmi:20171211214716p:plain

ただ、Robot Frameworkでは日本語のテストケース名も書けるのですが、自動有効化トリガーとして設定するのが難しいです*2

そのため、 Ctrl + Space というショートカットも併用します。

また、専用IDEだけあり、SeleniumLibrary 3系のキーワードも問題なく補完してくれます。

 

コンテンツ・アシストの様子

日本語のみの場合は、 Ctrl + Space にてこんな感じで補完されます。

f:id:thinkAmi:20171211214416p:plain

 
プレフィックスとして英語を付けていると、自動補完されます。見た目は日本語のみと同じですね。

f:id:thinkAmi:20171211214503p:plain

 
SeleniumLibrary3.x系も自動補完されます。

f:id:thinkAmi:20171211214546p:plain

 

Robot Frameworkのテストケースを作成

IDEの準備ができたため、Robot Frameworkのテストケースを作成します。

上記で作成したmyprojectを右クリック > New > Robot Test Suite にてテストケースファイルを作成します。

f:id:thinkAmi:20171211214750p:plain

 
今回は、こんな感じのテストケースファイルを作成しました。

*** Settings ***
Library    SeleniumLibrary

*** Keywords ***
はろー
    Log To Console    ワールド
        
myはろー
    Log To Console    こんにちわ

*** Test Cases ***
テスト
    はろー
    myはろー
    Log To Console    hello

 

テストの実行

REDのコンソールよりテストを実行してみます。

=============
Myproject
=============
Myproject.Mytest
=============
???    ????
?????
hello
| PASS |
------------------
Myproject.Mytest    | PASS |

テストはパスしたものの、コンソールに出力した日本語が文字化けしてしまいました。

 
Eclipseの事例では、eclipse.ini に追記すると文字化けが解消されるとの記載がありました。
Eclipseのコンソール文字化け解決法。 - Qiita

同じようなファイルを探してみたところ、それっぽいのが以下にありました。

/rf_red/Eclipse.app/Contents/Eclipse/RED.ini

 
ただ、11行目のように記載してREDを再起動しましたが、結果は同じく文字化けしたままでした。

1 -startup
2 ../Eclipse/plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar
3 --launcher.library
4 ../Eclipse/plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.1.550.v20170928-1359
5 --launcher.appendVmargs
6 -vmargs
7 -Xms512m
8 -Xmx2g
9 -XstartOnFirstThread
10 -Dorg.eclipse.swt.internal.carbon.smallFonts
11 -Dfile.encoding=UTF-8

 
最後のコンソール部分は文字化けしたままですが、Macのターミナルから実行すると問題ないので気にしないことにしました。

 
以上、Robot Framework専用のIDEのREDを使ってみました。Nokiaで使われていることもあり、十分実用的なものでした。

 
ちなみに、メーリングリストには

To create RED we needed a valid business reason to do so. There is a big group of Eclipse users in Nokia who would take advantage in having integrated dev&testing environment. IntelliJ is also considered but we still have a lot to do in RED.

という投稿もありました。IntelliJにも来るといいなー。
https://groups.google.com/d/msg/robotframework-users/c8tNdBVL5WE/kT-uQDROEQAJ

*1:なお、fork先ではexperimental support としてPython3.xで動くようです https://github.com/robotframework/RIDE/issues/1719

*2:プレフィックスとして「test_」などを付けておけば、自動補完は動作しますが...