pycryptoのインストールがCentOS7.3 minimalで失敗する問題の対処
環境
[root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.3.1611 (Core)
結論
調査
デフォルトで入れようとすると以下ログでこける。Cコンパイラがないとのことなのでgccを入れる
[root@localhost ~]# pip install pycrypto ... configure: error: in `/tmp/pip-build-EJivFC/pycrypto': configure: error: no acceptable C compiler found in $PATH See `config.log' for more details ... Command "/usr/bin/python2 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-EJivFC/pycrypto/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().re place('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-WsN8Es-record/install-record.txt --single-version-externally-managed --compil e" failed with error code 1 in /tmp/pip-build-EJivFC/pycrypto/
[root@localhost ~]# yum install gcc
もう1回実行するとPython.hがないと言われる。調べた結果これはpython-develがないためとのことなので入れて解決。
... gcc -pthread -fno-strict-aliasing -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -std=c99 -O3 -fomit-frame-pointer -Isrc/ -I/usr/include/python2.7 -c src/MD2.c -o build/temp.linux-x86_64-2.7/src/MD2.o src/MD2.c:31:20: fatal error: Python.h: No such file or directory #include "Python.h" ^ compilation terminated. error: command 'gcc' failed with exit status 1 ---------------------------------------- Command "/usr/bin/python2 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-2l6kpU/pycrypto/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-7LFD3X-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-2l6kpU/pycrypto/
[root@localhost ~]# yum install python-devel
Android通知のバージョンごとのUIの違い
自分用にメモ。以下のコードを実行したときのAndroid OSバージョンごとのUIを調べた。
Intent intent = new Intent(context, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); Notification notification = new NotificationCompat.Builder(context) .setAutoCancel(true) // Delete notification when user taps .setContentTitle("Test") .setTicker("ticker") // Message when notification shows for ~4.4 .setContentInfo("content info") .setContentText("content text") .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher)) .setContentIntent(pi) .build(); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(ID, notification);
- Android 4.4までは通知が表示されるときにTickerの文字が表示される。5.0以上では設定しても無視される。
- Android 5.0からはSmall IconがBig iconの右下に表示される。
- Android 7.0からはContent Infoが表示されない。
Notification | Ticker | |
---|---|---|
4.1 | ||
4.2 | ||
4.3 | ||
4.4 | ||
5.0 | ||
5.1 | ||
6.0 | ||
7.0 |
CircleCIでAndroid SDKとGradleをキャッシュする
CircleCIで普通にビルドしていると毎回Android SDKとGradleのダウンロードが行われてビルド時間が長くなってくる。CircleCIにはデフォルトのキャッシュ以外にも自分でキャッシュの設定ができるので、Android SDKとGradleをキャッシュすることでビルドを短くできる。~/.gradleはデフォルトでキャッシュされるようになっているようなので設定は不要。
dependencies: override: ... cache_directories: - /usr/local/android-sdk-linux/tools - /usr/local/android-sdk-linux/platforms/android-25 - /usr/local/android-sdk-linux/platforms/android-23 - /usr/local/android-sdk-linux/platforms/android-16 - /usr/local/android-sdk-linux/build-tools/25.0.2
キャッシュを使うかダウンロードするかの判定
tools, platforms/android-xxにはsource.propertiesというファイルがある。これにバージョンがPkg.Revision=25.2.5のように書かれているので、この値を見ることでキャッシュを使うかダウンロードするかを決める。build-toolsとGradleはフォルダがあるかどうかで判定する。
dependencies: override: - if ! $(grep -q "Pkg.Revision=25.2.5" $ANDROID_HOME/tools/source.properties); then echo y | android update sdk --no-ui --all --filter "tools"; fi - if [ ! -e $ANDROID_HOME/build-tools/25.0.2 ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"; fi - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-25/source.properties); then echo y | android update sdk --no-ui --all --filter "android-25"; fi - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-23/source.properties); then echo y | android update sdk --no-ui --all --filter "android-23"; fi - if ! $(grep -q "Pkg.Revision=5" $ANDROID_HOME/platforms/android-16/source.properties); then echo y | android update sdk --no-ui --all --filter "android-16"; fi - if [ ! -e ~/.gradle/wrapper/dists/gradle-3.5-all ]; then ./gradlew init; fi cache_directories: - /usr/local/android-sdk-linux/tools - /usr/local/android-sdk-linux/platforms/android-25 - /usr/local/android-sdk-linux/platforms/android-23 - /usr/local/android-sdk-linux/platforms/android-16 - /usr/local/android-sdk-linux/build-tools/25.0.2
注意点として先にAndroid SDKのアップデートをしないとGradleの初期化中にAndroid SDKの使用許諾が取れていなくて失敗する。
circle.yml
machine: java: version: openjdk8 environment: ANDROID_HOME: /usr/local/android-sdk-linux dependencies: override: - if ! $(grep -q "Pkg.Revision=25.2.5" $ANDROID_HOME/tools/source.properties); then echo y | android update sdk --no-ui --all --filter "tools"; fi - if [ ! -e $ANDROID_HOME/build-tools/25.0.2 ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"; fi - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-25/source.properties); then echo y | android update sdk --no-ui --all --filter "android-25"; fi - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-23/source.properties); then echo y | android update sdk --no-ui --all --filter "android-23"; fi - if ! $(grep -q "Pkg.Revision=5" $ANDROID_HOME/platforms/android-16/source.properties); then echo y | android update sdk --no-ui --all --filter "android-16"; fi - if [ ! -e ~/.gradle/wrapper/dists/gradle-3.5-all ]; then ./gradlew init; fi cache_directories: - /usr/local/android-sdk-linux/tools - /usr/local/android-sdk-linux/platforms/android-25 - /usr/local/android-sdk-linux/platforms/android-23 - /usr/local/android-sdk-linux/platforms/android-16 - /usr/local/android-sdk-linux/build-tools/25.0.2 test: override: - ./gradlew assembleDebug - cp -r ~/$CIRCLE_PROJECT_REPONAME/app/build/outputs/apk/* $CIRCLE_ARTIFACTS # unit test - ./gradlew testDebugUnitTest - cp -r ~/$CIRCLE_PROJECT_REPONAME/app/build/test-results/testDebugUnitTest/* $CIRCLE_TEST_REPORTS deployment: master: branch: master commands: - ./gradlew assembleRelease
参考
https://circleci.com/docs/1.0/how-cache-works/https://discuss.circleci.com/t/installing-android-build-tools-23-0-2/924/6
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; } }