(Source Code: https://github.com/testfoxx/TwoFlashLight.git)
完成 FP 類別的重構後,只剩下 MainActivity,開始修改它吧
Remove useless comment
首先把註解的程式碼移除,移除前 MainActivity 的總行數為 558 行,移除後MainActivity的總行數只剩 282 行。
為了確保沒有移除到不應該移除的部份,先跑 UnitTest 看看,結果如下。
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 2.222 [exec] [exec] OK (2 tests) ... ... BUILD SUCCESSFUL
成功通過測試,把本次的修改 merge 到遠端 Server 吧。
也許會覺得這次的修改部份不多,可以一併重構其他部份再merge,但使用版本控制就是盡量保持小部份但穩定的merge。
好處是萬一日後真的出現 bug,尋找 commit 可以切割的比較乾淨。
接著來看看變數的命名和存取權限部份。
Rename variable
重構前
... public class MainActivity extends Activity { static Camera c; static Parameters p; public LayoutParams lp; public PowerManager powerm; public PowerManager.WakeLock WL; public static int width; public static int height; public int moveX; public int backLightIndex; public int moveY; public int frontLightIndex; AdView adview; int testnum; boolean isCameraUse; ...
從 static Camera c 開始,單一字母的變數命名方式很難看出該變數代表的意義,把 c 改為 mCamera。
變數名稱前綴加上m,代表該變數為類別成員。
看看 c 的存取範圍,c 只有被 MainActivity 內部存取,因此可以縮減存取範圍為 private。
相同的方式來處理 static Parameters p , 將 p 改為 private Parameters mParameters;
其中 AdView adview , int testnum ,boolean isCameraUse 這3個變數並沒有被引用,因此把他們刪除掉。
重構完,如下
... public class MainActivity extends Activity { private Camera mCamera; private Parameters mParameters; private LayoutParams mLayoutParams; private PowerManager mPowerManager; private PowerManager.WakeLock mWakeLock; private int mScreenWidth; private int mScreenHeight; private int mXCoordinatesOfScreen; private int mYCorrdinatesOfScreen; private int mBackLightIndex; private int mFrontLightIndex; ...
執行 UnitTest 看看測試結果。
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.INSTRUMENTATION_RESULT: shortMsg=java.lang.RuntimeException [exec] INSTRUMENTATION_RESULT: longMsg=java.lang.RuntimeException: Fail to connect to camera service [exec] INSTRUMENTATION_CODE: 0 BUILD FAILED
發生 build failed , 觀察訊息看起來問題出在 RuntimeException : Fail to connect to camera service。
google 錯誤訊息 ,參考 http://stackoverflow.com/questions/23904459/android-java-lang-runtimeexception-fail-to-connect-to-camera-service
原因為當 Camera 使用後必須在 onPause()中 呼叫 release() 把資源釋放掉,最好是在 onResume()中 呼叫 open ()。
參考 http://developer.android.com/intl/zh-tw/reference/android/hardware/Camera.html#getNumberOfCameras()
了解原因之後就來修改 bug 吧,接下來先考慮甚麼時候要開啟 Camera , 根據 activity 的生命週期,
當 user 啟動 activity 或是暫停 activty 或是開啟另一個activity後,
除了 destroy 該 activity 之外,onResume() 一定都會被呼叫到,所以我們可以統一將初始化的動作放在 onResume()並針對不同狀態作處理。
加入 2 個方法如下,首先 initCamera 會初始化 mCamera 物件並設定其閃燈模式,第 12 行的 try catch 為了避免有些裝置沒有後置鏡頭其回傳值為 null 的情況。
@Override protected void onResume() { super.onResume(); initCamera(); detBackLight(); detFrontLight(); } private void initCamera() { try { mCamera = Camera.open(); } catch (RuntimeException e) { e.printStackTrace(); } if (mCamera != null) { mParameters = mCamera.getParameters(); mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH); mCamera.setParameters(mParameters); } }
接著考慮甚麼時候需要把 camera release,雖然 android developer 建議在 onPause() 中 release , 但因為我們希望 camera 被移動到背景時也能正常工作。
因此 release 的時間點就在於 onDestroy() 方法中。
加入 onDestroy()
@Override protected void onDestroy() { super.onPause(); mCamera.release(); mCamera = null; }
完成後,執行 UnitTest,結果如下
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 2.095 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
修正bug之後,針對方法來重構吧,首先從 showAdmob() 開始,
重構 showAdmob() 前,先來考慮 SRP 原則,只考慮 showAdmob 和 MainActivity 2者即可,
showAdmob() 主要有4個步驟
1.建立adView 物件
2.建立廣告layout
3.廣告layout加入adView物件
4.adview物件讀取廣告
如下
private void showAdmob() { AdView adView = new AdView(this, AdSize.BANNER, "a150c81d1f5acca"); LinearLayout layout = (LinearLayout) findViewById(R.id.AdLayout); layout.addView(adView); adView.loadAd(new AdRequest()); }
照目前的設計,當 showAdmob()發生變化時,一定需要改動MainActivity,比如說 “a150c81d1f5acca” 需要改為別的號碼,或者想把 layout 改為別的顯示畫面。
這種高耦合的設計是違反 SRP 的。確實,顯示廣告是 MainActivity 的工作,但這不一定需要由 MainActivity 來完成。
我們可以建立另一個 AdmobManager,把顯示廣告的工作內容交給它,讓 MainActivity 簡單的呼叫 AdmobManager 的方法即可。
除了減低耦合以外,若日後有其他Activity 需要顯示廣告,只要呼叫 AdmobManager 即可,不必複製貼上同樣的內容,也可減少重複的程式碼。
Extract Class
建立 AdmobManager 並把 showAdmob() 搬移進來,AdmobManager 如下
package com.twoflashlight.utility; import android.app.Activity; import android.widget.LinearLayout; import com.google.ads.AdRequest; import com.google.ads.AdSize; import com.google.ads.AdView; import com.tylerfoxx.twoflash.R; public class AdmobManager { private static final String AD_UNIT_ID = "a150c81d1f5acca"; public static void showAdmob(Activity activity) { AdView adView = new AdView(activity, AdSize.BANNER, AD_UNIT_ID); LinearLayout layout = (LinearLayout) activity.findViewById(R.id.AdLayout); layout.addView(adView); adView.loadAd(new AdRequest()); } }
在 MainActivity 的 init() 改為呼叫 AdmobManager.showAdmob(),如下
void init() { ... AdmobManager.showAdmob(this); ... }
完成後,執行UnitTest,結果如下
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 2.563 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
成功通過測試。
接著重構 init() ,首先修改存取權限為 private ,接著把方法重構為以下內容,其中 ScreenWakeLockManager 為延續 AdmobManager 的作法,
把 WakeLock 的實作內容提取到 ScreenWakeLockManager 。
private void init() { countBackLightIndex(); countFrontLightIndex(); AdmobManager.showAdmob(this); ScreenWakeLockManager.makeScreenWakeLock(this); setScreenBrightness(0.1f); } private void countFrontLightIndex() { mScreenHeight = (getWindowManager().getDefaultDisplay().getHeight()); mYCorrdinatesOfScreen = mScreenHeight / 2; mFrontLightIndex = 5; } private void countBackLightIndex() { mScreenWidth = (getWindowManager().getDefaultDisplay().getWidth()); mXCoordinatesOfScreen = mScreenWidth / 2; mBackLightIndex = 1; } private void setScreenBrightness(float brightness) { mLayoutParams = getWindow().getAttributes(); mLayoutParams.screenBrightness = brightness; getWindow().setAttributes(mLayoutParams); }
ScreenWakeLockManager 如下
package com.twoflashlight.utility; import android.app.Activity; import android.content.Context; import android.os.PowerManager; public class ScreenWakeLockManager { private static final String DEBUG_TRACE_IDENTIFY = "ScreenWakeLockManager"; public static void makeScreenWakeLock(Activity activity){ PowerManager powerManager = (PowerManager) activity.getSystemService(Context.POWER_SERVICE); powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, DEBUG_TRACE_IDENTIFY).acquire(); } }
完成重構後,跑 UnitTest ,測試結果如下
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 3.595 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
成功通過。
接著是public void finishApp(), 重構後如下
private void showExitDialog() { Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("Warning"); dialog.setMessage("Exit Application??"); dialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { nmShow(); MainActivity.this.finish(); } }); dialog.setNegativeButton("No", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); dialog.show(); }
Run UnitTest!! e.g.
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 1.727 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
Success!!
接著是 void nmShow() , 重構後如下
private void showNotificationToTitleBar() { Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, 0); Notification notification = new Notification(); notification.tickerText = "TwoFlashLight!!!"; notification.defaults = Notification.DEFAULT_ALL; notification.setLatestEventInfo(MainActivity.this, "TwoFlashLight", "Welcome to use!!!", pendingIntent); NotificationManager notificatoinManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificatoinManager.notify(0, notification); }
run UnitTest
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 3.88 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
Success!!
接著是 public boolean onTouchEvent()
重構後,改變為3個方法,改變為3個方法的原因為增加可讀性以及保持方法的抽象層次。
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: adjustLight(event); break; } return true; } private void adjustLight(MotionEvent event) { getCoordinatesOnScreen(event); detBackLight(); detFrontLight(); } private void getCoordinatesOnScreen(MotionEvent event) { mXCoordinatesOfScreen = (int) event.getX(); mYCorrdinatesOfScreen = (int) event.getY(); }
run UnitTest
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:.. [exec] Test results for InstrumentationTestRunner=.. [exec] Time: 2.689 [exec] [exec] OK (2 tests) ... BUILD SUCCESSFUL
重構public boolean onKeyDone()
重構變動的行數不多,主要是把 magic number 改為可讀性高的 KeyEvent.KEYCODE_BACK
@Override public boolean onKeyDown(int keycode, KeyEvent event) { super.onKeyDown(keycode, event); TraceLogger.print("keycode:" + keycode); switch (keycode) { case KeyEvent.KEYCODE_BACK: showExitDialog(); return true; } return false; }
由於變動到按鍵功能,必須增加按下 back key 的測項到 UnitTest 中。
但這時候出現另一個問題,要如何測試 dialog?
按下 back key 會呼叫 showExitDialog() 進而顯示 dialog , 但我們必須按下 dialog 上的按鈕(Yes / No),才有辦法確認功能是否正常。
但因為 dialog 是私有方法的區域變數,除非修改 MainActivity 讓 dialog 可以存取,否則無法在外部取得該 dialog。
這裡推薦一個好用的開源測試工具,Robotium。它把複雜測試指令包裝起來方便讓人呼叫,以本次的測試為例。
當按下 back key 之後,出現 dialog,可以輸入有關按鈕的文字,讓Robotium自動去尋找符合的元件,如下
第3行送出 back key 指令以顯示dialog
第4行尋找含有 “Warning” 的元件(dialog)
第5行點擊含有 “No” 的元件(No button)
即可測試按下 no 的功能是否正常。
private void testExitDialogFunction() { mMainActivity.sendKey(KeyEvent.KEYCODE_BACK); mMainActivity.searchText("Warning"); mMainActivity.clickLongOnText("No"); ... }
最後修改 UnitTest 如下,也加入模擬改變亮度測項(testChangeLightFunction)。
package com.twoflashlight.test; import android.test.ActivityInstrumentationTestCase2; import android.view.KeyEvent; import com.robotium.solo.Solo; import com.twoflashlight.main.MainActivity; public class UnitTest extends ActivityInstrumentationTestCase2<MainActivity> { private static final int MOVE_STEP_ON_SCREEN = 20; private Solo mMainActivity; public UnitTest() { super(MainActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); mMainActivity = new Solo(getInstrumentation(), getActivity()); } private void testChangeLightFunction() { testChangeBackLightToMin(); testChangeFrontLightToMax(); testChangeBackLightToMax(); testChangeFrontLightToMin(); } private void testChangeBackLightToMax() { float startX = 0; float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth(); float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight() / 2; float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight() / 2; mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN); } private void testChangeBackLightToMin() { float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth(); float endX = 0; float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight() / 2; float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight() / 2; mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN); } private void testChangeFrontLightToMax() { float startY = 0; float endY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight(); float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth() / 2; float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth() / 2; mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN); } private void testChangeFrontLightToMin() { float endY = 0; float startY = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getHeight(); float startX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth() / 2; float endX = mMainActivity.getCurrentActivity().getWindowManager().getDefaultDisplay() .getWidth() / 2; mMainActivity.drag(startX, endX, startY, endY, MOVE_STEP_ON_SCREEN); } private void testExitDialogFunction() { mMainActivity.sendKey(KeyEvent.KEYCODE_BACK); mMainActivity.searchText("Warning"); mMainActivity.clickLongOnText("No"); mMainActivity.sendKey(KeyEvent.KEYCODE_BACK); mMainActivity.searchText("Warning"); mMainActivity.clickLongOnText("Yes"); } public void testMainActivityFunction() { testChangeLightFunction(); testExitDialogFunction(); assertNotNull(mMainActivity); } }
完成後執行 UnitTest,
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:. [exec] Test results for InstrumentationTestRunner=. [exec] Time: 16.019 [exec] [exec] OK (1 test) ... BUILD SUCCESSFUL
接著看看 UnitTest 的覆蓋率。
[EMMA v2.0.5312 report, generated Thu Oct 22 13:31:18 CST 2015] ------------------------------------------------------------------------------- OVERALL COVERAGE SUMMARY: [class, %] [method, %] [block, %] [line, %] [name] 100% (6/6) 86% (24/28) 95% (476/500) 91% (112/123) all classes OVERALL STATS SUMMARY: total packages: 2 total classes: 6 total methods: 28 total executable files: 4 total executable lines: 123 COVERAGE BREAKDOWN BY PACKAGE: [class, %] [method, %] [block, %] [line, %] [name] 100% (3/3) 50% (3/6)! 80% (37/46) 77% (10/13)! com.twoflashlight.utility 100% (3/3) 95% (21/22) 97% (439/454) 93% (102/110) com.twoflashlight.main -------------------------------------------------------------------------------
class 的覆蓋率 100% ,方法覆蓋率 86%,方法的覆蓋率包含類別建構式,在 com.twoflashlight.utility 內的 class 都是僅只有 static method , 不必產生其類別實體即可呼叫。
接著只剩下 detBackLight 和 detFrontLight 這2個方法等待重構。
準備重構 detBackLight 和 detFrontLight,首先更改2個方法的名稱為 changeBackLight 和 changeFrontLight 比較適合方法的意圖。
接著根據方法內容提取出不同功能的部份以 changeFrontLight 為例,
在方法內部其實可以再分為2個部份為
1.計算 front light 索引
2.設定螢幕亮度
於是我們再提取出這兩個方法如下。
private void changeFrontLight() { calculateFrontLightIndex(); setScreenBrightness(); }
2個方法內容為
private void calculateFrontLightIndex() { ArrayList<Integer> screenHeightQuarterIndex = new ArrayList<Integer>(); int screenHeightQuarterMax = 10; for (int i = 0; i < screenHeightQuarterMax; i++) { screenHeightQuarterIndex.add(mScreenHeight * i / screenHeightQuarterMax); } for (int i = 0; i < screenHeightQuarterIndex.size() - 1; i++) { if (mYCorrdinatesOfScreen >= screenHeightQuarterIndex.get(i) && mYCorrdinatesOfScreen <= screenHeightQuarterIndex.get(i + 1)) { mFrontLightIndex = i; } } mLayoutParams.screenBrightness = (float) mFrontLightIndex / screenHeightQuarterMax; } private void setScreenBrightness() { float screenBrightnessMin = 0.1f; if (mLayoutParams.screenBrightness <= screenBrightnessMin) { mLayoutParams.screenBrightness = screenBrightnessMin; } getWindow().setAttributes(mLayoutParams); }
接著使用同樣的方式重構 changeBackLight()
重構 changeBackLight 完成如下
private void changeBackLight() { calculateBackLightIndex(); setBackLightFlashMode(); } private void setBackLightFlashMode() { if (mParameters != null) { if (mBackLightIndex == BackLightIndex.OFF.getIndex()) { mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); } else if (mBackLightIndex == BackLightIndex.MACRO.getIndex()) { mParameters.setFocusMode("macro"); mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH); } else if (mBackLightIndex == BackLightIndex.AUTO.getIndex()) { mParameters.setFocusMode("auto"); mParameters.setFlashMode(Parameters.FLASH_MODE_TORCH); } try { mCamera.setParameters(mParameters); } catch (Exception e) { e.printStackTrace(); } } } private void calculateBackLightIndex() { ArrayList<Integer> screenWidthQuarterIndex = new ArrayList<Integer>(); int screenWidthQuarterMax = 4; for (int i = 0; i < screenWidthQuarterMax; ++i) { screenWidthQuarterIndex.add(mScreenWidth * i / screenWidthQuarterMax - 1); } for (int i = 0; i < screenWidthQuarterIndex.size() - 1; ++i) { if (mXCoordinatesOfScreen >= screenWidthQuarterIndex.get(i) && mXCoordinatesOfScreen <= screenWidthQuarterIndex.get(i + 1)) { mBackLightIndex = i; } } }
完成後一樣需要 run UnitTest
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:. [exec] Test results for InstrumentationTestRunner=. [exec] Time: 15.171 [exec] [exec] OK (1 test) ... BUILD SUCCESSFUL
接著進行本地化資源的動作,如果將 UI 上的顯示文字寫死在 java code 中,就無法針對不同的國家顯示不同的文字。
因此我們必須對資源進行本地化。android 系統預設會到 res/values/strings.xml 中尋找字串資源,預設的strings.xml如下
<resources> <string name="app_name">TwoFlashLight</string> <string name="menu_settings">Settings</string> <string name="title_activity_main">Two Flash Light</string> <string name="operate_hint">Drag around the screen , adjust the brightness of the back\n\n Drag up or down the screen , adjust the front luminance\n\nPress the Home key for background use\n\n please press the back key to close app</string> <string name="exit_dialog_title">Warning!!</string> <string name="exit_dialog_message">Exit Application??</string> <string name="exit_dialog_yes">Yes</string> <string name="exit_dialog_no">No</string> <string name="notification_text">TwoFlashLight!!!</string> <string name="notification_welcomeUse">Welcome to Use!!!</string> </resources>
若需要針對不同國家進行資源的設定必須在 res/ 資料夾下建立相對應的資料夾,比如我們想針對繁體字串資源的設定就建立
res/values-zh-rTW 資料夾,並在資料夾中新建 strings.xml 如下,
<resources> <string name="app_name">雙面手電筒</string> <string name="menu_settings">設定</string> <string name="title_activity_main">雙面手電筒</string> <string name="operate_hint">左右拖曳畫面,調整背面亮度\n\n上下拖曳畫面,調整正面亮度\n\n背景使用請按Home鍵\n\n關閉程式請按返回鍵</string> <string name="exit_dialog_title">警告!!</string> <string name="exit_dialog_message">離開應用程式??</string> <string name="exit_dialog_yes">是</string> <string name="exit_dialog_no">否</string> <string name="notification_text">雙面手電筒!!!</string> <string name="notification_welcomeUse">歡迎使用!!!</string> </resources>
完成後也必須修改 UnitTest,只要修改 testExitDialogFunction()即可
private void testExitDialogFunction() { mMainActivity.sendKey(KeyEvent.KEYCODE_BACK); mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title)); mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_no)); mMainActivity.sendKey(KeyEvent.KEYCODE_BACK); mMainActivity.searchText(mMainActivity.getString(R.string.exit_dialog_title)); mMainActivity.clickLongOnText(mMainActivity.getString(R.string.exit_dialog_yes)); }
因為已經針對不同的語系設定不同的資源,所以測項也不可寫死在 java code。
Run UnitTest!!
test: [getlibpath] Library dependencies: [getlibpath] No Libraries [echo] Running tests... [echo] Running tests ... [exec] [exec] com.twoflashlight.test.UnitTest:. [exec] Test results for InstrumentationTestRunner=. [exec] Time: 15.668 [exec] [exec] OK (1 test) ... BUILD SUCCESSFUL