Python3でSlackのステータスを変える
最近Slackにステータスの機能が実装された。ミーティング中や帰宅済みなど名前の横を見ればステータスがわかるようになって非常に便利になった。 https://get.slack.help/hc/ja/articles/201864558-Slack-%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B APIが公開されていてトークンがあればSlackの外から変更できる。 https://api.slack.com/methods/users.profile.set トークンの取得はこちらから https://api.slack.com/custom-integrations/legacy-tokens 以下はPython3でのコードになります。
環境
- python 3.6.0
- requests 2.13.0
コード
import requests import json import sys arguments = sys.argv text = arguments[1] emoji = arguments[2] url = "https://slack.com/api/users.profile.set" params = { "token": "your_token", "profile": json.dumps( { "status_text": text, "status_emoji": emoji } ) } headers = {"Content-Type": "application/json"} r = requests.get(url, params=params, headers=headers) print(r.url) print(r.text)
実行
python3 change_slack_status.py "In meeting" ":spiral_calendar_pad:"
【Android】jacocoでコードカバレッジを取る
追記
Kotlin対応
最近UTを書くようになってきたのでカバレッジを取ってみました。Gradleにjacocoのプラグインがあるのでそれを使います。
build.gradleはDroidkaigi2017のbuild.gradleを参考にしました。
apply 'jacoco' apply plugin: 'com.android.application' android { // Settings for Android... } jacoco { toolVersion = "0.7.7.201606060606" } // A list of directories which should be included in coverage report def coverageSourceDirs = ['src/main/java'] // A list of files which should be excluded from coverage report since they are generated and/or framework code def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*', '**/BuildConfig.class', '**/*Activity*.class', '**/*Fragment*.class', '**/*Receiver.class', '**/*Manifest*.class', '**/*Application*.class'] task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled true html.enabled true csv.enabled false xml.destination "${buildDir}/reports/jacoco/jacocoTestReport.xml" html.destination "${buildDir}/reports/jacoco/html" classDirectories = files( fileTree( dir: "${buildDir}/intermediates/classes/uiTest/debug", exclude: coverageExcludeFiles)) } sourceDirectories = files(coverageSourceDirs) executionData = files "${buildDir}/jacoco/testUiTestDebugUnitTest.exec" doLast { println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/jacocoTestReport.xml" println "jacoco html report has been generated to file://${reports.html.destination}/index.html" } } dependencies { // For dependencies... }
まずapply 'jacoco'でプラグインを適用します。そしてjacocoプラグインのバージョンを指定します。
jacoco {
toolVersion = "0.7.7.201606060606"
}
バージョンの一覧はここにあります。
次にコードカバレッジのレポートを作るタスクを作ります。 jacocoプラグインにはJacocoReportというレポートを作成するタスクがあります。jacocoの解析対象はバイトコードのため、UTのタスク(test(ProductFlavor)DebugUnitTest)と同時に実行させる必要があるので依存させます。私の場合はuiTestというProductFlavorでUTを実行しているので依存させるタスク名はtestUiTestDebugUnitTestになります。タスク名がわからなかったら./gradlew tasksで調べます。
task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) {
}
あとはJacocoReportのページを参考に設定します。私の場合はActivityやFragment等のクラスはカバレッジに含めないようにしました。先ほど書いたようにjacocoの解析対象はバイトコードのため、reports.classDirectoriesにはビルド後のパスを設定します。executionDataのパスは"${buildDir}/jacoco/test(ProductFlavor)DebugUnitTest.exec"となるようです。
// A list of directories which should be included in coverage report def coverageSourceDirs = ['src/main/java'] // A list of files which should be excluded from coverage report since they are generated and/or framework code def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*', '**/BuildConfig.class', '**/*Activity*.class', '**/*Fragment*.class', '**/*Receiver.class', '**/*Manifest*.class', '**/*Application*.class'] task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled true html.enabled true csv.enabled false xml.destination "${buildDir}/reports/jacoco/jacocoTestReport.xml" html.destination "${buildDir}/reports/jacoco/html" classDirectories = files( fileTree( dir: "${buildDir}/intermediates/classes/uiTest/debug", exclude: coverageExcludeFiles)) } sourceDirectories = files(coverageSourceDirs) executionData = files "${buildDir}/jacoco/testUiTestDebugUnitTest.exec" }
結果は↓のような感じ
これでjacocoによるコードカバレッジが取れるようになりました。 次はCircleCIとCodecovを連携させてコードカバレッジをGitHubのREADMEに表示する方法について書きたいと思います。
【UI Automator 2.0】UiObject2#longClick()が効かない問題の対策
UI Automator 2.0を使ってListViewのContextMenuのテストを書きたかったのですが、UiObject2#longClick()ではどうもまくいきませんでした。
動きを見る感じロングクリックの時間が短いですね・・・ 調べるとUiDevice#swipe()でロングクリックしたい場所の座標をスワイプすることで解決できるようです。
参考:http://stackoverflow.com/questions/21432561/how-to-achieve-long-click-in-uiautomator
UiDevice#swipe()の説明を見ると
Performs a swipe from one coordinate to another using the number of steps to determine smoothness and speed. Each step execution is throttled to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
とあるので最後のstepの値でロングクリックの時間を制御できそうです。 以下テストコードです。
@Test public void deleteFilter() { UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); // アプリ起動等 // ... // ロングクリック UiObject2 filterList = device.wait(Until.findObject( By.clazz(ListView.class)), 5000); if (filterList == null) fail("Filter list was not found"); List<UiObject2> filters = filterList.findObjects( By.clazz(LinearLayout.class).depth(2)); if (filters == null) fail("Filter item was not found"); assertThat(filters.size(), is(1)); // 動かない // filters.get(0).longClick(); Rect filterRect = filters.get(0).getVisibleBounds(); device.swipe(filterRect.centerX(), filterRect.centerY(), filterRect.centerX(), filterRect.centerY(), 100); // 削除をクリック UiObject2 dialogContentList = device.wait(Until.findObject( By.res("android", "select_dialog_listview")), 5000); if (dialogContentList == null) fail("Dialog was not found"); List<UiObject2> contents = dialogContentList.findObjects( By.clazz(RelativeLayout.class).depth(2)); for (UiObject2 content : contents) { UiObject2 contentText = content.findObject( By.clazz(TextView.class)); if (contentText != null && contentText.getText().equals("フィルター削除")) { content.click(); break; } } // 結果確認 UiObject2 emptyView = device.wait(Until.findObject( By.res(BuildConfig.APPLICATION_ID, "filter_emptyView")), 5000); assertNotNull(emptyView); }
うまくいきました!
【Android】SQLiteのテーブルからカラムを削除する
ALTER TABLE mytable DROP COLOMN mycolomnを実行すれば完了・・・と思いきや、SQLiteはDROP COLOMNをサポートしていないらしい。
http://www.sqlite.org/faq.html#q11
そのため、以下の手順でカラムを削除した
下のコードはfilterテーブルからfeedIdカラムを削除したコードになる。
public class DatabaseHelper extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "rss_manage"; private static final int DATABASE_VERSION = 3; private static final int DATABASE_VERSION_ADD_FILTER_FEED_REGISTRATION = 3; private String createFiltersTableSQL = "create table " + Filter.TABLE_NAME + "(" + Filter.ID + " integer primary key autoincrement,"+ Filter.KEYWORD + " text,"+ Filter.URL + " text," + Filter.TITLE + " text,"+ Filter.ENABLED + " integer)"; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } //onCreate() is called when database is created @Override public void onCreate(SQLiteDatabase db) { db.execSQL(createFiltersTableSQL); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (newVersion < oldVersion) return; if ((oldVersion < DATABASE_VERSION_ADD_FILTER_FEED_REGISTRATION)) { // Drop feed ID column in filter table, but Androd does not support drop column. // Copy and drop table and insert. ArrayList<Filter> filters = getAllFilters(db); String sql = "DROP TABLE " + Filter.TABLE_NAME; db.execSQL(sql); db.execSQL(createFiltersTableSQL); // Insert all of the filters insertFilters(db, filters); } } private void insertFilters(@NonNull SQLiteDatabase db, @NonNull ArrayList<Filter> filters) { try { db.beginTransaction(); boolean result = true; for (Filter filter : filters) { ContentValues filterVal = new ContentValues(); filterVal.put(Filter.TITLE, filter.getTitle()); filterVal.put(Filter.KEYWORD, filter.getKeyword()); filterVal.put(Filter.URL, filter.getUrl()); filterVal.put(Filter.ENABLED, filter.isEnabled()); long newFilterId = db.insert(Filter.TABLE_NAME, null, filterVal); if (newFilterId == -1) { result = false; break; } } if (result) db.setTransactionSuccessful(); } catch (SQLiteException e) { e.printStackTrace(); } finally { db.endTransaction(); } } private ArrayList<Filter> getAllFilters(SQLiteDatabase db) { Cursor cursor = null; ArrayList<Filter> filters = new ArrayList<>(); try { db.beginTransaction(); String[] columns = { Filter.ID, Filter.TITLE, Filter.KEYWORD, Filter.URL, Filter.FEED_ID, Filter.ENABLED }; cursor = db.query(Filter.TABLE_NAME, columns, "", null, null, null, null); if (cursor != null && cursor.getCount() > 0) { while (cursor.moveToNext()) { int filterId = cursor.getInt(0); String title = cursor.getString(1); String keyword = cursor.getString(2); String url = cursor.getString(3); int feedId = cursor.getInt(4); int enabled = cursor.getInt(5); Filter filter = new Filter(filterId, title, keyword, url, feedId, enabled); filters.add(filter); } } db.setTransactionSuccessful(); } catch (SQLException e) { e.printStackTrace(); } finally { if (cursor != null) { cursor.close(); } db.endTransaction(); } return filters; } }
JenkinsでSlaveを作ってビルドする
久々にデスクトップを買ったのでUbuntuを入れてビルドマシンとして設定してみる。
Slave(Ubuntu14.0.4)
まずJenkinsをインストールする。
[bash] wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add - sudo sh -c ‘echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list’ sudo apt-get update sudo apt-get install jenkins [/bash]
次にMaster-slave間でssh通信するために、公開鍵を受け取る必要があるので、一旦パスワード認証を有効にする。
[bash]
PasswordAuthentication yesに変更
sudo vim /etc/ssh/sshd_config sudo /etc/init.d/ssh restart [/bash]
Host(Mac)
- MacBook Air (11-inch, Mid 2012)
- OS X El Capitan 10.11.6
- brew 1.1.0
Jenkinsのインストール
[bash] brew install jenkins brew services start jenkins [/bash]
brewで楽々インストール・・・と思ったらサービス開始のところで躓いた。
[bash] /Users/phicdy/Library/LaunchAgents/homebrew.mxcl.jenkins.plist: Operation not permitted [/bash]
どうやらtmuxから実行したのが原因らしい。 brew servicesは中でlaunchctlを使っていて、tmux上ではnamespaceが変わってしまうため、launchctlをうまく実行できないとのこと。
参考: launchctl load で Operation not permitted(解決?)
[bash] brew install reattach-to-user-namespace [/bash]
~/.tmux.confに以下を追加
[bash] set-option -g default-command “reattach-to-user-namespace -l bash” [/bash]
鍵設定
scpで鍵を送って登録する。鍵送信後はパスワード認証を無効に戻しておく。
[bash]
ssh-keygen
scp .ssh/id_rsa.pub slave-user@
ここからSlave
cat id_rsa.pub >> ~/.ssh/authorized_keys rm id_rsa.pub exit [/bash]
Slaveの追加
- Jenkinsの管理→ノードの管理→新規ノード作成
- リモートFSルートにリモートのワークスペースのパスを入力
- 起動方法を「SSH経由でUnixマシンのスレーブエージェントを起動」に変更
- ホストにIPを入力
- 認証情報を追加
- 種類をSSHユーザ名と秘密鍵
- ユーザ名をリモートのログインユーザに変更
- 秘密鍵を「Jenkinsマスター上の~/.sshから」に変更
- 保存
おわりに
Android開発のビルドはかなりCPUとメモリを使うので、スペックが低いとビルド中に何もできないことがあった。デバッグ中はAndroid Studioを使うでビルドする必要があるが、それ以外のビルドはSlaveを使うことでかなりリソースを節約できると思う。3万くらいで買えたマシンだったので有効活用していきたい。
ErgoDox EZを買いました
最近流行ってることもあり買いました。 https://www.indiegogo.com/projects/ergodox-ez-an-incredible-mechanical-keyboard-computers-health#/
以前使っていたキーボードはテンキー付きで横に広いものでした。そのためキーボードの横にあるマウスを使うたびに肩と腕に負担がかかっていました。通常のキーボードは中央にキーが集まっているため、肩が内側に寄り身体への負担が大きいです。ErgoDoxはセパレートキーボードで、肩を開いて打てるというのが魅力でした。 ErgoDoxはキーボード配列を自由に変更できるのも大きな魅力で、色んな人が自分の考えた最強のキーマップを公開していて面白そうだなと思ったのもあります。
キーマップ
現在のキーマップです。
方針としては、まずは通常のキー配列にして作業をしました。その中で自分が押す頻度の高いキーを親指に集め、よく使う記号を押しやすい位置に変更していきました。
親指周辺
私は普段MacでAndroid StudioにIdea Vimを入れて作業しています。そのためVimでよく使うEscキーを押しやすい親指の位置に持ってきました。 親指シフトというのは色んなところでいいと見かけていたので、左手親指に設定しました。確かにかなり楽に大文字が打てます。
英数⇔かな変換
英数⇔かな変換はKarabinerを使い、左右commandキーで行うようにしました。最初はGoogle日本語入力のショートカットで行っていましたが、Vim操作時に余計なキーが押されてうまく変換が行えないということがありました。
括弧
コードを書くときにでよく使う括弧が押しやすい位置にあったらいいなと思い、左キーボードに左括弧、右キーボードに右括弧を置く配置にしました。Shiftを押しながらここだっけな?と押して逆向きの括弧が入力されることがなくなってかなり快適です。
ショートカットキー
どの記事か忘れましたが、コピー&ペーストをキーに割り当てている人がいて便利そうだったので真似しました。AlfredユーザなのでAlfredをすぐ呼べるようにAlt+Spaceをキーに割り当てました
レイヤー
ErgoDoxはレイヤー機能があり、キーを押すだけで切り替えができます。私は基本1レイヤーで済まそうとしていますが、他に2レイヤー追加しています。
L1: 足りなかったキー用レイヤー
普段使いで足りなかったキーだけ配置してます。ファンクションキーとハット(^)だけです。もっとうまく使えるような気もしてます。
L2: Windows, Ubuntu用
リモートデスクトップでWindows, Ubuntuに繋ぐとき用です。キーの割り当て対応とCmdをCtrlに置き換えています。なぜか_と|が割り当てできず困っています...ひとまずは辞書に「あ」の変換で登録しておきました。もしくはどこかからかコピーしてきます。基本こっちではコードを書かないようにしています。
おわりに
使い始めて2ヶ月程度ですが、明らかに腕や肩が楽になりました。よりよいタイピングができるようにキーマップを改善していきたいと思います。 私のキーマップはこちらです。
【Android】UI Automator 2.0でUIテストをする
UI AutomatorはGoogleが開発しているAndroid向け自動UIテストフレームワークです。 2015年3月にバージョンが2.0になりGradleに対応しました。 有名なテストフレームワークのAppiumも中ではUI Automatorを実行しています。
Espressoとの違い
GoogleはテストフレームワークとしてEspressoも開発しています。EspressoとUI Automatorの違いとして最も大きいのは、UI Automatorは複数のアプリをテストできるということです。Espressoはアプリのソースコードに紐付いており、アプリのプロジェクトの中に入れる必要があります。それに対してUI Automatorは自分のアプリはもちろんのこと、設定アプリや自分で作ったツールアプリや他社のアプリ、通知バーなど自由に操作することができます。ソースコードと紐付いていないので、別プロジェクトで管理することもできますし、アプリと同じプロジェクトに含めることもできます。またUI Automatorは2.0からEspressoと併用できるようになったため、自分のアプリはEspresso、他のアプリを操作するときはUI Automatorといった書き方が可能です。
UI AutomatorはAndroid 4.3以上のみをサポートしています。まだまだAndroidでは最新のOSのみを対象とはできない状況なので、通常はAndroid 4.3以上のみを対象とていないことがほとんどかと思います。ここはProductFlavorを分けるかプロジェクトごと分けることで対応します。なおEspressoはAndroid 2.2からサポートしており、設定アプリなどを使わないテストのみであれば、Espressoのほうが幅広くテストを行えます。
設定
UI Automatorは2.0からAndroid Support Libraryの一部となり、build.gradleで簡単に使うことができるようになりました。 まずAndroid SDK ManagerからAndroid Support Repositoryをインストールします。
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion "26.0.0" defaultConfig { applicationId "com.phicdy.uiautomator2sample" minSdkVersion 14 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } productFlavors { production { minSdkVersion 14 } uiTest { minSdkVersion 18 } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:design:25.3.1' uiTestCompile 'com.android.support.test:runner:0.5' uiTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' }
testInstrumentationRunnerをandroid.support.test.runner.AndroidJUnitRunnerに指定します。これでJUnit4形式でテストが書けるようになります。productFlavorをアプリ用とUI Automator用で分け、UI Automator用のminSdkVersionを18(Android 4.3)に設定します。今回はuiTestとしました。
最後に実行に必要なライブラリであるcom.android.support.test:runner:0.5とcom.android.support.test.uiautomator:uiautomator-v18:2.1.2をuiTestCompileで読み込みます。
テストを書く時や実行時は、Android Studio上のBuild VariantsをuiTestDebugに変更します。
これでUI Automatorの設定は終わりです。
テストを書く
テストはsrc/androidTest以下にJUnit4の書き方で追加していきます。 デフォルトでApplicationTestが入っていますが必要ないので消し、新たにテストを追加します。
package com.phicdy.uiautomator2sample; import android.content.Context; import android.content.Intent; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SdkSuppress; import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.By; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; import org.junit.Test; import org.junit.runner.RunWith; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; @RunWith(AndroidJUnit4.class) @SdkSuppress(minSdkVersion = 18) public class MainUiTest { @Test public void floatingButtonTest() { UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); // Launch MainActivity Context context = InstrumentationRegistry.getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); // Click floating button UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000); if (btn == null) fail("Floating button was not found"); btn.click(); UiObject2 snakeBar = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000); assertNotNull(snakeBar); assertThat(snakeBar.getText(), is("Replace with your own action")); } }
今回はプロジェクト作成するときにTabbed Activityで作成したデフォルトのアプリのテストを作ります。 テストステップは以下の通りです。
- MainActiivtyを起動
- FloatingButtonを押す
- SnakeBarが出るので文言が"Replace with your own action"であることを確認
テストの初めにUiDeviceのインスタンスを取得します。 UiDeviceは端末の操作をしたり、UiObject2を取得したり、様々な場面で使います。 UiDeviceを管理するクラスを用意してもいいと思います。
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
MainActivity起動部分です。UI Automator 2.0ではContextが使えるようになったのでstartActivity()で起動します。 MainActivityはアプリ起動時に起動されるActivityなので、context.getPackageManager().getLaunchIntentForPackage()で対象のパッケージ名を指定して起動しています。
// Launch MainActivity Context context = InstrumentationRegistry.getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);
FloatingButtonをクリックしてSnackBarのUiObject2を取得する部分です。
UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000);
UI Automator 2.0では各ViewをUiObject2として取得します。主に取得には、UiDevice#findObject()かUiDevice#wait()を使います。 UiDevice#wait()を使うとViewが描画されるまで待ってくれるのでテストの成功率が上がります。
UiDevice#wait()にはSearchConditionとtimeoutを指定します。 SearchConditionはどの条件で待つかを指定します。Untilという便利なクラスがあるので、これを使ってSearchConditionを作ります。今回は特定のUiObject2が出るまで待つので、Unitl.findObject()を使います。Unitl.findObject()にはBySelectorを指定します。BySelectorはByクラスから生成してUiObject2を特定する条件を指定します。条件はresoruse IDやテキスト、クラス名などで指定します。resource IDが確実なので、アプリ側できるだけresource IDを設定します。Android SDKのtoolsにuiautomatorviewerというツールがあるのでこれで簡単にIDやViewの階層構造を確認できます。Android Studio 2.2からはこういった機能がAndroid Studio自体に追加されるようです。
UiObject2を取得したらnullチェックをしてエラーハンドリングをした後、クリックします。
if (btn == null) fail("Floating button was not found"); btn.click();
クリックするとSnackBarが出るので、テキスト部分を同様に取得します。 最後にテキスト部分のnullチェックと文言を確認します。
UiObject2 snakeBarText = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000); assertNotNull(snakeBarText); assertThat(snakeBarText.getText(), is("Replace with your own action"));
最後に
UI Automator 2.0を使うことで、UIテストが書けました。 今回は1つのアプリだけを対象としたテストでしたが、例えば設定アプリを起動して機内モードをオンにし、エラーダイアログが出るか確認するといったテストもUI Autoamtor 2.0では可能です。 UIテストを書くことでCIでのリグレッションテストや手動テストの削減ができるので、少しずつ書いていきたいです。
今回のサンプルプロジェクトはこちらです。 以上です。