kkAyatakaのメモ帳。

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

sphinxcontrib-xlsxtableのモジュール実行

  • sphinxcontrib-xlsxtableをCLIで呼び出し可能にした
  • グリッドテーブル文字列を一旦ファイルに書き出してから、ドキュメントに組み込むことができる
  • グリッドテーブルがファイルに残るので、Git運用との相性がよさそう

モジュール実行

sphinxcontrib-xlsxtableをモジュール実行できるようにした。 CLIから呼び出すことでreStructuredTextのグリッドテーブル文字列を生成することができる。

$ python -m sphinxcontrib.xlsxtable --header-rows=1 sample.xlsx
+----+-------+-----------+--------+
| A1 | B1    | C1        | D1     |
+====+=======+===========+========+
| A2 | B2:B3 | C2        | D2     |
+----+       +-----------+--------+
| A3 |       | C3:D3              |
+----+-------+-----------+--------+
| A4 | B4    | C4        | - D4-1 |
|    |       |           | - D4-2 |
+----+-------+-----------+--------+

rsSTファイルに書き出してからドキュメントに組み込む

これを使うとExcelファイルから直接reSTに埋め込むのではなく、グリッドテーブルを出力したファイル経由でreSTに展開できる。

table.rstに書き出したとして...

$ python -m sphinxcontrib.xlsxtable --header-rows=1 sample.xlsx > table.rst

.. includeで取り込む。

.. table:: Table Caption

   .. include:: table.rst

グリッドテーブルテキストがファイルに残る

テキストとして残るため、Git管理との相性がよく、PR運用での差分確認が楽になる。 Excelファイルと二重管理にはなるものの、Excelファイルの差分確認はかなり手間であるため、メリットのほうが大きい。

スクリプトを用意すれば出力そのものはそれほど手間にはならないため、Excelファイルを直接reSTに埋め込むよりは、より現実的な運用のように思う。

sphinxcontrib-xlsxtable

SphinxExcelファイルから表 (テーブル) を埋め込むSphinx拡張を作りました。

機能的に不十分だったり、Pythonモジュールよくわかってなかったり、PyPIの理解も不十分だったりしますが、まあ、動いとる。

以下が特徴で、これはそのまま自分が必要とした機能であり、既存の拡張機能が肌に合わなかった理由になります。

  • Excelファイルの制御はOpenPyXLを使用しており、xlsxファイルのサポート範囲が広い
  • Sphinx標準表記の表 (テーブル) として出力される
  • 結合セル対応

使い方

pipで入れて、

$ pip install sphinxcontrib-xlsxtable

conf.pyに設定します。

# conf.py
extensions = [
    'sphinxcontrib.xlsxtable',
]

reStructuredTextのディレクティブは次の通りです。現状はほとんどオプションはありませんが、いくらか追加予定です。

ドキュメント化していませんが、一応シート指定にも対応してます。

.. xlsx-table:: Table Caption
   :file: path/to/xlsx/file.xlsx
   :header-rows: 1
   :sheet: Sheet

これによって、以下のExcelファイルから、

f:id:kkAyataka:20200314113454p:plain
Excel

以下の結果が得られます。

f:id:kkAyataka:20200314113537p:plain
結果

セル内の文字列はそのままreSTとして処理されるので、上記の通り「- テキスト」形式で書かれた部分は、箇条書きとして処理されています。

内部でGrid Table文字列を生成

仕組みとしては、OpenPyXLExcelファイルを解析して、内部でreSTのGrid Tableの文字列を作っています。

上記のExcelファイルだと、以下のような感じ。

+----+-------+-----------+--------+
| A1 | B1    | C1        | D1     |
+====+=======+===========+========+
| A2 | B2:B3 | C2        | D2     |
+----+       +-----------+--------+
| A3 |       | 日本語              |
+----+-------+-----------+--------+
| A4 | B4    | C4        | - D4-1 |
|    |       |           | - D4-2 |
+----+-------+-----------+--------+

作った文字列はそのままdocutils標準の解析器 / 生成器に渡しているだけ (のつもり) なので、結合セル、セル内のreST表記に対応し、標準の表のスタイルで表示することを実現しています。

Sphinx / reStructuredTextの表について

元々はSphinxで結合セルの表記をするならExcelから読み込みたい...というので作ったのだけど、使ってみたところ思いの外便利でした。

  • 大規模な表はCSV Table使おうと思ってたけど、CSVの編集が思ったより大変
    • Excelで開いても表示のレイアウトが維持されないので、割と辛い
  • 既存のExcelファイルをそのまま使えるのが強い
    • 既存ドキュメントのまとめ直しで、既存ファイルをそのまま使える
  • Excelで表作るのやっぱ楽。

opensslコマンドを使ってAPIと同じ結果を得る

% openssl aes-128-cbc -e -iv 00000000000000000000000000000000 -K 30313233343536373839414243444546 -nosalt -in data.txt -out out.txt
  • -ivはCBCモードの初期化ベクターで、16進数値で指定する
  • -Kは暗号化キーで、16進数値で指定する
  • -nosoltは暗号化データにsoltが設定されないようにする

opensslコマンドで簡単に暗号化できるけど、データにsoltを足したり、 デフォルトの初期化ベクターが入ったりする。 狙った暗号化 (のデータを得る) には、それらの値も合わせて設定 / 抑制する必要がある。

環境

  • macOS 10.15.3
  • openssl (LibreSSL 2.8.3)

ファイルのバイナリ表示

ファイルを1 byte単位でバイナリ表示するには以下。暗号化したデータの確認に使用する。

% od -t x1 out.txt
0000000    57  b1  cc  91  87  f6  d1  21  4b  cc  71  ab  56  40  3c  c4
0000020

OpenSSLを使った暗号化 (v1.1.1d)

2015-10-24にOpenSSLでの暗号化の記事を書いたけど、 最近になって試したところコンパイルできなくなっていたので、その修正版。

昔の記事、バージョン書いてなかったから差がわかんね。

環境

  • OpenSSL v1.1.1d (ソースからビルド)
  • macOS 10.15.3
  • Xcode 11.3.1

EVP_CIPHER_CTXの作成方法がEVP_CIPHER_CTX_newを使用するようになった

違いと言ってもEVP_CIPHER_CTXの作成方法が異なるだけ。 通常の構造体として使えば良かったのがオペークポインタ形式になっているので、 専用関数で作成する。で、専用関数でリリースする。

// 昔の
EVP_CIPHER_CTX ctx = {}; // 関数定義が非公開になったので、作れなくなった

// 新しいの
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); // newで作って
...
EVP_CIPHER_CTX_free(ctx); // freeで開放

暗号化処理に関してざっくり書くと、以下。EVP_CIPHER_CTXの作成方法が変わっただけで、 それ以外の部分は古いバージョンとの違いは無い。

#include <openssl/evp.h>

EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_CIPHER_CTX_init(ctx);

// 暗号化の設定で、EVP_aes_128_ecb等いろいろ
const unsigned char iv[16] = {0};
EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);

// ブロックサイズで割り切れる部分の処理
int outl = 0;
EVP_EncryptUpdate(ctx, encrypted, &outl, data, data_size);

// 最後のブロックの処理で、PKCSパディングされる。
int pad = 0;
EVP_EncryptFinal_ex(ctx, encrypted + outl, &pad);

EVP_CIPHER_CTX_cleanup(ctx);
EVP_CIPHER_CTX_free(ctx);

ドキュメント

OpenSSLの公式ページの構成が変わっているので、古いURLはリンク切れになった。 以下は完全ソースコードとv1.1.1のドキュメントへのリンク。

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のコマンドラインツールが内包してしまうのも、プログラムそのものであれば重複を気にすることもないので、理にはかなった動作だとは思うけど。