2014年7月22日 星期二

[CPP] Incremental Test Case for C++ Usage via Google Test @ Ubuntu 14.04

既然做了 Unit Test 了,就希望 Test Case 可以不斷地累積下來。昨晚跟總監級的高手閒聊工作瑣事,以 Python 跟 MongoDB 的互動為例,其實 PyMono 很多操作都還是撰寫 Javascript ,只是從 Python 發動罷了,這時候最佳的設計是讓 Javascript 獨立出來,如此一來可以切割工作出來,讓把玩 Javascript 就專心把玩,而不要每次要改 Javascript 時,還得去動 Python code。

這樣的情境跟 Test Case 有點類似,有沒有辦法讓 Test case 增加時,不必動到測試的邏輯程式?解法就是把 Test Case 用目錄管理,每次做 Unit Test 時,是去掃目錄的檔案出來即可。

片段程式碼:

#include <dirent.h>
bool getFiles(std::string dir, std::vector<std::string> &files) {
        DIR *dp;
        struct dirent *dirp;
        if((dp  = opendir(dir.c_str())) == NULL)
                return false;
        while ((dirp = readdir(dp)) != NULL)
                if(dirp->d_name && dirp->d_name[0] != '.')
                        files.push_back(dir+"/"+std::string(dirp->d_name));
        closedir(dp);
        return true;
}

const std::string testcase_dir_Func1 = "testcase_dir";
TEST(MyJob, testFunc1) {
        std::vector<std::string> testcase;
        std::string dirTarget = testcase_dir_Func1;
        ASSERT_EQ(getFiles(dirTarget,testcase), true);
        ASSERT_EQ(testcase.size() > 0, true);
        for(int i=0 ; i<testcase.size() ; ++i) {
                std::fstream input(testcase[i], std::fstream::binary|std::fstream::in);
                std::stringstream buffer;
                buffer << input.rdbuf();
                ASSERT_EQ(buffer.str().length() > 10, true);

// ...
        }
}

2014年7月21日 星期一

[CPP] Unit Test for C++ Usage via Google Test @ Ubuntu 14.04

跟友人閒聊把玩 CPP 的心得,就被推坑到 Google test 啦:Google C++ Testing Framework

看一下簡介就...準備跳槽了 XD

Who Is Using Google Test?

In addition to many internal projects at Google, Google Test is also used by the following notable projects:
  • The Chromium projects (behind the Chrome browser and Chrome OS)
  • The LLVM compiler
  • Protocol Buffers (Google's data interchange format)
安裝:

$ sudo apt-get install cmake libgtest-dev
$ mkdir gtest && cd gtest
$ cmake /usr/src/gtest/
-- The CXX compiler identification is GNU 4.8.2
-- The C compiler identification is GNU 4.8.2
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Found PythonInterp: /usr/bin/python (found version "2.7.6")
-- Looking for include file pthread.h
-- Looking for include file pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Configuring done
-- Generating done
-- Build files have been written to: /path/gtest

$ make
Scanning dependencies of target gtest
[ 50%] Building CXX object CMakeFiles/gtest.dir/src/gtest-all.cc.o
Linking CXX static library libgtest.a
[ 50%] Built target gtest
Scanning dependencies of target gtest_main
[100%] Building CXX object CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
Linking CXX static library libgtest_main.a
[100%] Built target gtest_main

$ sudo cp *.a /usr/lib


試用:

$ vim gtest.cpp
#include <gtest/gtest.h>

int main(int argc, char** argv) {
        testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
}
$ g++ gtest.cpp -lgtest -lpthread
$ ./a.out
[==========] Running 0 tests from 0 test cases.
[==========] 0 tests from 0 test cases ran. (0 ms total)
[  PASSED  ] 0 tests.


測試失敗範例:

$ vim gtest.cpp
#include <gtest/gtest.h>

TEST(MyJob, Action1) {
    ASSERT_EQ(0, 1);
}

int main(int argc, char** argv) {
        testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
}


$ g++ gtest.cpp -lgtest -lpthread
$ ./a.out

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyJob
[ RUN      ] MyJob.Action1
gtest.cpp:4: Failure
Value of: 1
Expected: 0
[  FAILED  ] MyJob.Action1 (1 ms)
[----------] 1 test from MyJob (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] MyJob.Action1

 1 FAILED TEST


測試成功的範例:

$ vim gtest.cpp
#include <gtest/gtest.h>

TEST(MyJob, Action1) {
    ASSERT_EQ(0, 0);
}

int main(int argc, char** argv) {
        testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
}


$ g++ gtest.cpp -lgtest -lpthread
$ ./a.out

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyJob
[ RUN      ] MyJob.Action1
[       OK ] MyJob.Action1 (0 ms)
[----------] 1 test from MyJob (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 1 test.

2014年7月19日 星期六

[CPP] Unit Test for C++ Usage via CppUnit @ Ubuntu 14.04

隨著時間越來越少後,不知不覺更在意"成果"是否能夠年年累積。想了一會兒後,最佳的方式就是引進軟工管理,除了最基本的版本控制外,幫自己把玩的小玩意 open source 及增加 reuse 的機會也很重要,維護方面則是從簡易人眼的測試改成更制式的 unit test 架構,最好能夠定時批次測試等。只不過採用制式的 unit test 不見得適合 startup 環境,畢竟時間寶貴,有些 prototype 生命週期很短。

在此以 CppUnit 為例,筆記一下怎樣快速使用,更多簡介請參考 CppUnit 文件

安裝:

$ sudo apt-get install libcppunit-dev

$ vim t.cpp
#include <cppunit/ui/text/TestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestResult.h>
#include <cppunit/CompilerOutputter.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/BriefTestProgressListener.h>

class SampleTest : public CppUnit::TestFixture {
private:
int a;
public:
void setUp() {
a = 0;
}
void tearDown() {
a = 0;
}
static CppUnit::Test *suite() {
CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "SampleTest" );
suiteOfTests->addTest( new CppUnit::TestCaller<SampleTest>(
"testSample",
&SampleTest::testSample
));
return suiteOfTests;
}
void testSample() {
CPPUNIT_ASSERT( a == 10 );
}
};

int main() {
std::cout << "\n\n=== Style 1 ===\n\n";
{
CppUnit::TestCaller<SampleTest> test ( "testSample", &SampleTest::testSample );

CppUnit::BriefTestProgressListener progress;
CppUnit::TestResultCollector collectedresults;
CppUnit::TestResult result;
result.addListener(&progress);
test.run( &result );

CppUnit::CompilerOutputter outputter( &collectedresults, std::cerr );
}

std::cout << "\n\n=== Style 2 ===\n\n";
{
CppUnit::TestSuite suite;
suite.addTest( new CppUnit::TestCaller<SampleTest>(
"testSample",
&SampleTest::testSample
));

CppUnit::BriefTestProgressListener progress;
CppUnit::TestResultCollector collectedresults;
CppUnit::TestResult result;
result.addListener(&progress);
suite.run( &result );

CppUnit::CompilerOutputter outputter( &collectedresults, std::cerr );
}

std::cout << "\n\n=== Style 3 ===\n\n";
{
CppUnit::TextUi::TestRunner runner;
runner.addTest( SampleTest::suite() );
runner.run();
}
return 0;
}


測試成功:

=== Style 1 ===

testSample : OK


=== Style 2 ===

testSample : OK


=== Style 3 ===

.


OK (1 tests)


測試失敗:

=== Style 1 ===

testSample : assertion


=== Style 2 ===

testSample : assertion


=== Style 3 ===

.F


!!!FAILURES!!!
Test Results:
Run:  1   Failures: 1   Errors: 0


1) test: testSample (F) line: 27 t.cpp
assertion failed
- Expression: a == 10


目前大概打算對自定的 class 編寫一些偏邏輯層面的 unit test,例如 bool testFunc1(); 的方式,成功就回傳 true,但不套用任何 unit test framework,如此一來純邏輯的驗證更可以適用在不同平台,而要再加上特定的 unit test framework (如 CppUnit) 時,只需簡易套用,可以直接 reusable 而不必重頭寫。

2014年7月17日 星期四

[SQL] 透過 INNER JOIN 更新 Table 新增的欄位數值 @ MySQL 5.6

對於一些當作收集 log 用途的 table tb_log ,隨著時間增加後,通常會再整理另一個 tb_status 的 table,快速查詢各個狀態,設計上就會定期批次從 tb_log 取出資料,存進 tb_status 中。

假設 tb_log 有 5 個欄位,一開始只覺得需要 2 個欄位的資訊,就把 tb_status 設定為 2 個欄位,然而過一陣子後,想多記錄一個欄位時,只好變動 tb_status ,但新增的欄位沒有舊資料,就變成要從 tb_log 取出來再存進 tb_status 了

碰到這種問題,有一個解法就是使用 INNER JOIN 來處理:
  1. 先從 tb_status 找出欄位未有值的資料
  2. 從 tb_log 組出 tb_status 所需的資料
  3. 透過  Update 指令更新
情況敘述:

mysql> describe tb_log;
+--------+--------------+------+-----+---------+----------------+
| Field  | Type         | Null | Key | Default | Extra          |
+--------+--------------+------+-----+---------+----------------+
| f0     | int(11)      | NO   | PRI | NULL    | auto_increment |
| f1     | varchar(8)   | YES  |     | NULL    |                |
| f2     | varchar(8)   | YES  |     | NULL    |                |
| f3     | varchar(8)   | YES  |     | NULL    |                |
| f4     | varchar(8)   | YES  |     | NULL    |                |
+--------+--------------+------+-----+---------+----------------+

mysql> describe tb_status;
+--------+--------------+------+-----+---------+----------------+
| Field  | Type         | Null | Key | Default | Extra          |
+--------+--------------+------+-----+---------+----------------+
| f1     | varchar(8)   | YES  | PRI | NULL    |                |
| f2     | varchar(8)   | YES  |     | NULL    |                |
| f3     | varchar(8)   | YES  |     | NULL    |                |
+--------+--------------+------+-----+---------+----------------+


其中 tb_status.f3 則是新建出來,未有資料的。

第一步:先找出 tb_status.f3 是空的(新進資料會有 f3 數值,只有舊資料沒有)

mysql> SELECT f1 WHERE f3 IS NULL;

第二步:從 tb_log 組出 f3 資料,由於 tb_log 是流水帳,且 tb_status 本身也可以從 tb_log 查詢出來的,只需組出 tb_status 需要的欄位即可:

mysql> SELECT f1, f3 FROM tb_log GROUP BY f1;

第三步,把上述兩個資料 JOIN 起來:

SELECT tb1.f1, tb2.f3 FROM
( SELECT f1 WHERE f3 IS NULL ) AS tb1, (SELECT f1, f3 FROM tb_log GROUP BY f1) AS tb2
WHERE tb1.f1 = tb2.f2;


最後,追加更新 tb_status 的用法:

UPDATE tb_status AS tb4
INNER JOIN

(
SELECT tb1.f1, tb2.f3 FROM
( SELECT f1 WHERE f3 IS NULL ) AS tb1,
(SELECT f1, f3 FROM tb_log GROUP BY f1) AS tb2
WHERE tb1.f1 = tb2.f2
) AS tb3

ON tb4.f1 = tb3.f1

SET

tb4.f3 = tb3.f3;

2014年7月15日 星期二

iOS 開發筆記 - 使用 CocoaPods 管理第三方函式庫

記得前幾年都用 git 跟 git submodule 管理,一切都是手動處理,包含 Xcode 添加 include path 等,現在則是想要把一些把玩的東西 open 出來,就想到用 CocoaPods 來管理,畢竟不是每個人都那麼勤勞的 XD

在此以 CocoaAsyncSocket 和 facebook-ios-sdk 為例。

首先,先安裝 CocoaPods:

$ sudo gem install cocoapods
$ pod update


接著搜尋想要的套件:

$ pod search CocoaAsyncSocket

-> CocoaAsyncSocket (7.3.5)
   Asynchronous socket networking library for Mac and iOS.
   pod 'CocoaAsyncSocket', '~> 7.3.5'
   - Homepage: https://github.com/robbiehanson/CocoaAsyncSocket
   - Source:   https://github.com/robbiehanson/CocoaAsyncSocket.git
   - Versions: 7.3.5, 7.3.4, 7.3.3, 7.3.2, 7.3.1, 7.2.2, 7.0.3, 0.0.1 [master repo]

$ pod search facebook-ios-sdk

-> Facebook-iOS-SDK (3.15.1)
   The iOS SDK provides Facebook Platform support for iOS apps.
   pod 'Facebook-iOS-SDK', '~> 3.15.1'
   - Homepage: https://developers.facebook.com/docs/ios/
   - Source:   https://github.com/facebook/facebook-ios-sdk.git
   - Versions: 3.15.1, 3.15.0, 3.14.1, 3.14.0, 3.13.1, 3.13.0, 3.12.0, 3.11.1, 3.11.0, 3.10.0, 3.9.0, 3.8.0, 3.7.1, 3.7.0, 3.6.0, 3.5.3, 3.5.2, 3.5.1,
   3.5.0, 3.2.1, 3.2.0, 3.1.1, 3.1.0, 3.0.8, 3.0.7, 3.0.6.b, 3.0.5.b, 1.2, 1.last, 0.0.1 [master repo]




接著,開始透過 Xcode 新增一個 Project (HelloPods) 後,接著改用 Terminal 在 Project 目錄下新增 Podfile

$ cd ~/path/ios-project
$ vim Podfile
pod 'CocoaAsyncSocket'
pod 'Facebook-iOS-SDK'
$ pod install
Analyzing dependencies
Downloading dependencies
Installing Bolts (1.1.0)
Installing CocoaAsyncSocket (7.3.5)
Installing Facebook-iOS-SDK (3.15.1)
Generating Pods project
Integrating client project

[!] From now on use `HelloPods.xcworkspace`.


接著,就改用 HelloPods.xcworkspace 吧!

$ open HelloPods.xcworkspace

現況雖然可以輕鬆添加新增的函式庫 header files 了,但在 import 時沒有 autocomplete 的功能,若很在意的話,可在 Project (HelloPods) -> Target (HelloPods) -> Build Settings -> User Header Search Paths -> 增加 ${SRCROOT} 以及 recursive 屬性。(添加完出錯時,在把它刪掉吧)

如此一來,工作應該就跟平常沒什麼兩樣啦。未來,可以一樣可以用 pod update / pod outdated 更新套件最新資訊。

另外,目前 Project (HelloPods) 目錄結構:

$ ls ~/path/ios-project
HelloPods HelloPods.xcworkspace Podfile Pods
HelloPods.xcodeproj HelloPodsTests Podfile.lock


對於哪些目錄需要用 git 管理,可以參考一下官網介紹:Should I ignore the Pods directory in source control?

以上則是利用 CocoaPods 及其管理的第三方函式庫的用法,至於不再 CocoaPods 管理的套件呢?除了在 Podfile 中描述來源外,其套件也要撰寫 *.podspec 描述,請參考 CocoaAsyncSocket / CocoaAsyncSocket.podspec;對於 Podfile 的部分,則是直接指定 git 位置即可:

$ vim Podfile
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git'