Tuesday, August 23, 2011

Xcode4 - Regular Expression Find / Replace

Quick one....

In Xcode4 if you want to perform a 'regular expression' Find (and Replace) but have no idea how, this is what you should do:

  • Just hit ⌘F (within file) or ⇧⌘F (all files)
  • Click on the magnifying glass and Show Find Options
  • Change the Style to Regular Expression
  • Go nuts with your Regular Expressions :-)

Friday, August 12, 2011

iOS Continuous Integration - Part IV

My previous posts described a way for you to have automated testing and code coverage for your iOS projects using Jenkins.
Now that we have that in place, we can also rely on Jenkins to generate Ad Hoc & Distribution builds. Even more interesting, we can have Jenkins deploy these new binaries and notify your alpha/beta-testers there is a new version for them to play with.

References

There is actually a bunch of information out there on how to do this.

General Information:
Distribute via Xcode's Archive Gotchas - when working with static libraries (i.e. SampleApp) TestFlightApp Manually generating .ipa

Preparing SampleApp for IPA

Our SampleApp project, although very simple, tries to mimic a real project, where you have 3rd party and/or your own libraries all inside a Xcode4's workspace.

Because of that, if you just go ahead and try to "Product -> Archive" it'll most likely fail. That's why there a few links under the "Gotchas" section above :)

Let's prepare SampleApp so that is able to generate an IPA from within Xcode. Even if you don't actually build the IPA from Xcode, is good to have it all prepared just in case you want to do it.

SampleLib Project
  • Deployment = Skip Install = Yes - set in project level
  • Packaging = Public Headers Folder Path = Coverage & Debug = ../include/$(PROJECT_NAME)
  • Packaging = Public Headers Folder Path = Release = ../../include/$(PROJECT_NAME)
SampleApp Project
  • Search Paths = User Header Search Paths = $(BUILT_PRODUCTS_DIR)/../include $(BUILT_PRODUCTS_DIR)/../../include $(BUILT_PRODUCTS_DIR)/** - set in project level
  • RootViewController.m, change the import statment to #import <SampleLib/NSDate+Humanize.h>
  • would also be nice to add an icon for your app...
With these settings, you should be able to Run (⌘R - which builds Debug), Profile (⌘I - which builds Release), and Archive.
Note: if Archive is grayed out, that's because you don't have SampleApp -> iOS Device scheme selected.

Quickest Way

If you don't want to use Jenkins and just want to get you App out there for people to give it a try, I'd say the best is to use TestFlightApp.com.

Automated Way - Basic

If your project is not contained on a Xcode4 Workspace, then my suggestion is just to follow the instructions from Shine Tech's blog. It relies on the XCode plugin for Jenkins and describes how to automatically deploy your IPA to TestFlightApp.com.

Automated Way - Advanced

As our SampleApp is currently contained on XCode4 Workspace, we cannot rely on XCode plugin for Jenkins as it does not currently supports workspaces.

That's not a problem, we can manually build the IPA by running a simple script (thanks to Mike Nachbaur's post). We'll also use Jenkins to copy the build IPA to your Dropbox account and send a message to your testers, so then can play with the new version (and log a bunch of new bugs for you :).

We'll be also updating the Build version (CFBundleVersion) using the current date & build version. It's good for tracking purposes, but feel free to include it or not in your project.

SampleApp
  • add the following configuration file under a newly created scripts folder:
    rolima:~ $ cd ~/Work/SampleApp
    rolima:~/Work/SampleApp (master)$ mkdir scripts
    rolima:~/Work/SampleApp (master)$ cd scripts
    rolima:~/Work/SampleApp (master)$ vi build.config
    
    # For AdHoc, I'd rather send a DEBUG build.
    CONFIGURATIONS="Debug Release"
    
    # AdHoc Debug settings
    Debug_ProvisionFile=AdHoc.mobileprovision
    Debug_Codesign="iPhone Developer"
    
    # Release settings
    Release_ProvisionFile=AppStore.mobileprovision
    Release_Codesign="iPhone Distribution"
    
    # OTA settings -- used to build the link
    OTAURL="http://dl.dropbox.com/u/YOUR_USER_ID/MyApps"
    OTASmallIcon=SampleApp/icon.png
    OTALargeIcon=SampleApp/icon-512.png
    OTADeployPath=~/Dropbox/Public/MyApps
    OTATitle=SampleApp
    OTASubtitle=SampleApp for blog post
        
  • Also, let's add a Run Script build phase to SampleApp, so that Jenkins is able to find the SampleApp.app
    echo "Make sure Output folder exists..."
    mkdir -p $SRCROOT/../output
    
    echo "Save path to SampleApp.app..."
    echo $CODESIGNING_FOLDER_PATH > $SRCROOT/../output/$PROJECT_NAME.app.filepath.txt      
        
Jenkins
  • Let's start by creating a new job on Jenkins, so that we can schedule when it will build, and not mess with testing/code coverage automation jobs already there.
  • Job Name = SampleApp-AdHoc - better if it does not have spaces, as it will be part of the IPA file name
  • Build a free-style software project
  • Source Code Management = Git
  • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleApp.git or your GitHub repository URL
  • Build Triggers = Build periodically = @weekly - for example, if you want a new AdHoc every week. Feel free to play with these settings
  • Build - add an Execute Shell magic step that: bumps CFBundleVersion, compiles the app, signs it, generates the IPA, creates the plist for the app, and copy all that is needed to a Dropbox public folder. You should modify this accordingly to whatever your needs/requirements for deployment.
    #!/bin/sh
    function failed()
    {
      echo "Failed $*: $@" >&2
      exit 1
    }
    
    # properties
    export OUTPUT=$WORKSPACE/output
    rm -rf $OUTPUT
    mkdir -p $OUTPUT
    PROFILE_HOME=~/Library/MobileDevice/Provisioning\ Profiles/
    
    # read build configuration 
    . "$WORKSPACE/scripts/build.config"
    
    # bump version
    BUNDLE_VERSION=`date +'%y%m%d'`.$BUILD_NUMBER
    cd SampleApp
    agvtool new-version -all $BUNDLE_VERSION
    cd ..
    
    # process builds 
    for config in $CONFIGURATIONS; do
      echo "\n==================================================================\n"
      echo "COMPILE $JOB_NAME-$BUILD_NUMBER-$config ..."  
    
      provfile=$(eval echo \$`echo ${config}_ProvisionFile`)
      codesign=$(eval echo \$`echo ${config}_Codesign`)
      cert="$PROFILE_HOME/$provfile"
      fileprefix="$JOB_NAME-$BUILD_NUMBER-$config"
      ipaname="$fileprefix.ipa"
      otaname="$fileprefix.plist"
    
      #invoke build on workspace
      xcodebuild -workspace SampleApp.xcworkspace -scheme SampleApp -configuration $config -sdk iphoneos4.3 build || failed build;
    
      #generate the IPA
      app_path=$(cat output/SampleApp.app.filepath.txt)
      xcrun -verbose -sdk iphoneos PackageApplication \
          "$app_path" -o "$OUTPUT/$ipaname" \
          --sign "$codesign" --embed "$cert"
    
      echo "\n==================================================================\n"
      echo "DEPLOY $OTADeployPath/$fileprefix...."
    
      #make sure folder exists
      mkdir -p $OTADeployPath/$fileprefix
      #copy icons
      cp $WORKSPACE/$OTASmallIcon $OTADeployPath/$fileprefix/Icon-57.png
      cp $WORKSPACE/$OTALargeIcon $OTADeployPath/$fileprefix/Icon-512.png
      #copy IPA
      cp $OUTPUT/$ipaname $OTADeployPath/$fileprefix
    
      #read properties
      info_plist=$(ls SampleApp/SampleApp/*Info.plist | sed -e 's/\.plist//')
      bundle_version=$(defaults read $WORKSPACE/$info_plist CFBundleVersion)
    
      #generate PLIST
      cat << EOF > $OTADeployPath/$fileprefix/$otaname
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
       <key>items</key>
       <array>
           <dict>
               <key>assets</key>
               <array>
                   <dict>
                       <key>kind</key>
                       <string>software-package</string>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/$ipaname</string>
                   </dict>
                   <dict>
                       <key>kind</key>
                       <string>display-image</string>
                       <key>needs-shine</key>
                       <true/>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/Icon-57.png</string>
                   </dict>
                   <dict>
                       <key>kind</key>
                       <string>full-size-image</string>
                       <key>needs-shine</key>
                       <true/>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/Icon-512.png</string>
                   </dict>
               </array>
               <key>metadata</key>
               <dict>
                   <key>bundle-identifier</key>
                   <string>com.rolima.SampleApp</string>
                   <key>bundle-version</key>
                   <string>$bundle_version</string>
                   <key>kind</key>
                   <string>software</string>
                   <key>title</key>
                   <string>$OTATitle</string>
                   <key>subtitle</key>
                   <string>$OTASubtitle</string>
               </dict>
           </dict>
       </array>
    </dict>
    </plist>
    EOF
    
    done
        
  • We now just need to email people with the link to the new IPA, so they can proceed with their testing.
  • Just check Editable Email Notification and add something like this for the email message text:
    <h3>$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS</h3>
    <p>
    Check console output at $BUILD_URL to view the results.
    </p>
    <p>
    To install this version on your iPhone, just click 
    <a href="itms-services://?action=download-manifest&
    url=http://dl.dropbox.com/u/YOUR_USER_ID/MyApps/$PROJECT_NAME-$BUILD_NUMBER-Debug/
    $PROJECT_NAME-$BUILD_NUMBER-Debug.plist">here</a>.
    
    If you have any problems, please contact 
    <a href="mailto:administrator@yoursite.com">me</a>
    </p>
    <hr/>
    <b>Changes</b>
    <code>
    ${CHANGES}
    </code>
        
  • Note that I used the Debug version of the app, and that is currently using the AdHoc.mobileprovision (see build.config). That requires that the UUID of the tester's device to be registered so he/she is able to install and run the app.
  • Under the Advanced section for Editable Email Notification, you need to add a trigger for Success builds otherwise you'll not receive emails. Note that you can also add different recipients for each build case -- failure, success, etc -- so you can email your testers only when the build is successful.
  • You should be able to save the Jenkins job, run it and start receiving the email notifications.
Have fun!

Monday, August 8, 2011

iOS Continuous Integration - Part III

In the previous posts -- Part I & Part II -- we described how to setup your local GIT repository for working with submodules, then cloning the SampleLib repository and integrating with in Jenkins with automated testing and code coverage reports.

Now, we want to create a SimpleApp using the SampleLib and also applying Jenkins automation so to have a complete environment for your applications.

Again, feel free to get a copy of the project from my github account, or just follow along the instructions.

SampleApp

This assumes you already cloned the SampleApp repository under ~/Work/SampleApp (or whatever folder of your preference).
  • Launch Xcode4 and choose to Create a new Workspace (⌃⌘N)
  • Name is SampleApp and save it under ~/Work/SampleApp
  • Make sure you're viewing the "Project Navigator" (⌘1) and drag-n-drop the SampleLib project onto the Workspace
  • Now, create a New Project (⇧⌘N)
  • Choose Navigation-based Application (for this sample), call it SampleApp, and choose to "Include Unit Tests"
  • You should end up with something like the screenshot below:
  • You should be able to run SampleApp and see the empty TableView
  • Good time also to git commit everything so far

Using SampleLib

Let's adjust the build settings in order to use SampleLib. The advantage of doing this with Workspaces, rather than fat-libs, is that you let Xcode take care of choosing and building the right version (Debug vs Release and Device vs Simulator).
  • From the Project Navigator, go to SampleApp target -> Build Phases
  • From the left pane, just drag-n-drop SampleLib / Products / libSampleLib.a under "Link Binary with Libraries"
  • Under Build Settings set the following:
    • Linking -> Other Linker Flags -> Project level = -ObjC -all_load
    • Search Paths -> Always Search User Paths -> Project level = YES
    • Search Paths -> User Headers Search Paths -> Project level = $(BUILT_PRODUCTS_DIR)/**
  • We also need to make sure SampleLib is copying the headers
  • From the Project Navigator, go to SampleLib target -> Build Phases
  • Under "Copy Headers" phase, make sure NSDate+Humanize.h is copied to "Public"
  • Back to SampleApp make the following changes to RootViewController.m
//
//  RootViewController.m
//  SampleApp
//
//  Created by Rodrigo Lima on 8/8/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "RootViewController.h"
#import "NSDate+Humanize.h"

@implementation RootViewController

......................

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 5;
}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    switch (indexPath.row) {
        case 0:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:-(2*DAY_IN_SECONDS)];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 1:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:-DAY_IN_SECONDS];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 2:
        {
            NSDate* somedate = [NSDate date];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 3:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:DAY_IN_SECONDS];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 4:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:(2*DAY_IN_SECONDS)];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;

        default:
            break;
    }

    // Configure the cell.
    return cell;
}

......................

@end
  • Build and run, you should see the following:

Saving Changes

As we had to make a small change to SampleLib in order to copy the header files to a public location, now we're in a situation where the git submodule also has changes that need to be committed. The git submodule tutorial describes this situation under the Gotchas subsection. It says: "Always publish the submodule change before publishing the change to the superproject that references it. If you forget to publish the submodule change, others won't be able to clone the repository" Thus, let's do the following:
rolima@mac ~/Work/src/SampleApp (master)$ git st
# On branch master
#
#  modified:   Library/SampleLib (modified content)
#  modified:   SampleApp/SampleApp.xcodeproj/project.pbxproj
#  modified:   SampleApp/SampleApp/RootViewController.m
#
no changes added to commit (use "git add" and/or "git commit -a")

rolima@mac ~/Work/src/SampleApp (master)$ cd Library/SampleLib/
rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git st
# On branch master
#
#  modified:   SampleLib.xcodeproj/project.pbxproj
#
no changes added to commit (use "git add" and/or "git commit -a")

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git commit -a -m "headers should be public"
[master 114b3ae] headers should be public
 1 files changed, 1 insertions(+), 1 deletions(-)

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git push
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 429 bytes, done.
Total 4 (delta 2), reused 0 (delta 0)
To git@github.com:rodrigo-lima/SampleLib.git
   6e62f27..114b3ae  master -> master

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ cd ../..
rolima@mac ~/Work/src/SampleApp (master)$ git add .
rolima@mac ~/Work/src/SampleApp (master)$ git st
# On branch master
# Changes to be committed:
#
#  modified:   Library/SampleLib
#  modified:   SampleApp/SampleApp.xcodeproj/project.pbxproj
#  modified:   SampleApp/SampleApp/RootViewController.m
#

rolima@mac ~/Work/src/SampleApp (master)$ git commit -a -m "using SampleLib"
[master f51e25d] using SampleLib
 3 files changed, 54 insertions(+), 2 deletions(-)

rolima@mac ~/Work/src/SampleApp (master)$ git push
Counting objects: 15, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (8/8), 1.18 KiB, done.
Total 8 (delta 4), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.
To /Users/rolima/Work/CODE/blog/GIT-REPO/public/SampleApp.git
   285951e..f51e25d  master -> master
rolima@mac ~/Work/src/SampleApp (master)$ 

Automation, Automation, Automation

As we're building a Xcode workspace in Jenkins, we need a quick trick so that Jenkins is able to run successfully. I looked for ways of automating this, but still could not find it. If you know, please leave a comment :-)

The problem is that we're not saving some user-specific metadata to GIT. Thus, when Jenkins checks out the project the workspace is not 'fully prepared'. You need to open it in Xcode in order to be able to run it successfully.

So, let's start by going to your Jenkins' Dashboard and choosing to create a New Job
  • Job Name = SampleApp
  • To simplify, Copy existing job = SampleLib
  • Source Code Management = Git
  • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleApp.git or your GitHub repository URL
  • Build Triggers = Build after other projects are built = SampleLib
  • Build Triggers = Poll SCM (or any other) = * * * * * (to poll every minute) -- feel free to adjust to your likings
  • Build - add an Execute Shell step:
    xcodebuild -workspace SampleApp.xcworkspace -scheme SampleApp -sdk iphonesimulator4.3
  • Post-build Actions - do not check anything right now
Now, run the build once so Jenkins checks out the project. Don't worry if it fails, that's what we're fixing now...

From a terminal window:
rolima@mac ~ $ cd /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace/
rolima@mac /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace ((no branch))$ xcodebuild -workspace SampleApp.xcworkspace -list
2011-08-08 13:51:54.828 xcodebuild[17224:a0b] WARNING: Timed out waiting for /"runContextManager.runContexts" (10.001445 seconds elapsed)
There are no schemes in workspace "SampleApp".
See the problem? So, just fire Xcode and open the Workspace (⌘O) from /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace/SampleApp.xcworkspace. That's it, now close the Workspace and run the command again from Terminal:
rolima@mac /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace ((no branch))$ xcodebuild -workspace SampleApp.xcworkspace -listInformation about workspace "SampleApp":
    Schemes:
        SampleLib
        SampleApp
  • Now we're good to go... Just go back to Jenkins and run another build
  • All should go well and you now have a Jenkins jobs for the SampleApp.

What's left to do?

Now that you have the environment ready, you just need to implement the Test cases for SampleAppTests and repeat the configuration steps on both Xcode and Jenkins in order to be able to run unit tests reports and code coverage.

If you don't remember how to do it, just take a look at Part II.

Have fun!

Friday, August 5, 2011

iOS Continuous Integration - Part II


In the previous post, we prepared out local GIT repositories for both the SampleLib and the SampleApp.

This post will describe how to prepare the library for unit testing, code coverage, and automated builds with Jenkins.
It will also show how to consume this library on your SampleApp.

Feel free to get a copy of the project from my github account, although if you follow this post you'll ended up with exactly what I have there, plus you'll be learning how to do it :-)

SampleLib

This assumes you already cloned the SampleLib repository under ~/Work/SampleLib (or whatever folder of your preference).
  • Launch Xcode4 and choose to Create a new Xcode project
  • Choose iOS Templates -> Framework & Library -> Cocoa Touch Static Library
  • Product Name = SampleLib and check the box to Include Unit Tests
  • Choose to save it under ~/Work/SampleLib - where you cloned SampleLib repository
    • Note: after that, there will be a 'SampleLib' folder under 'SampleLib' - root of the repository.
    • Suggestion is to close the Xcode project and move the contents one folder up to avoid confusion.
    • See screenshots for before and after moving the folders
    • Once you're done, re-open the project in Xcode.

Unit Tests

  • As common TDD practice, we should always start writing tests, run so it fails, write/test/rewrite the code until needed.
  • After a few iterations, these are the files I have for Unit Tests & NSDate+Humanize
  • Note: in order for you to run the tests, make sure you have the following flag set for the SampleLibTests Target
    • Other Linked Flags = -all_load -framework SenTestingKit

Internationalization

When looking at NSDate+Humanize.m you've noticed calls to NSLocalizedString, but we still don't have the properties file with those Strings. The reason for that is that we also want to automate the generation of Strings in order to avoid forgetting something.
  • Make sure you're viewing the "Project Navigator" (⌘1)
  • Click on the project SampleLib -> Targets -> SampleLib
  • Click on Build Phases
  • Add a new Run Script phase
  • Move it after "Target Dependencies" and before "Compile Sources"
  • Add the following code:
lpath=${SOURCE_ROOT}/SampleLib/Resources/en.lproj
mkdir -p ${lpath}
rm -f ${lpath}/*
find ${SOURCE_ROOT}/SampleLib -name "*.m" -print0 | xargs -0 genstrings -q -s NSLocalizedString -o ${lpath}
shopt -s nullglob
for strings in ${lpath}/*
 do
   echo "*** ${strings} ***"
   iconv -f UTF-16 -t UTF-8 ${strings} > ${strings}.utf8
   cat ${strings}.utf8 | grep -v "No comment provided by engineer." > ${strings}
   rm ${strings}.utf8
   cat ${strings}
done
  • Build again, and verify the script got called and the strings are generated, by looking at the Log Navigator (⌘7)
  • You also must add the generated en.lproj to SampleLib project
  • Go back to "Project Navigator" (⌘1)
  • Make sure you right click under "SampleLib", choose Add File to "SampleLib"..., and add the Resources folder

Preparing for Code Coverage

Now that we have Unit Testing and our library ready, we should start looking at the code coverage reports and make sure our tests are exercising as much as possible of our code.
  • Make sure you're in "Project Navigator" (⌘1) and click on the SampleLib project
  • create a *Coverage* configuration, by duplicating the "Debug" Configuration
  • In order to validate the code coverage will be generated by Jenkins, let's make sure we can generate it from Xcode as well.
  • To do that, select Product -> Edit Scheme and change Test to use Coverage configuration and click Ok to close the dialog.
  • Select the testing target SampleLibTests -> Build Phases -> Run Script and change it to:
# Run the unit tests in this test bundle.
"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests"

# Run gcov on the framework getting tested
if [ "${CONFIGURATION}" = 'Coverage' ]; then
    # MUST manually set this, as the project runs from SampleLib
    FRAMEWORK_NAME=SampleLib
    FRAMEWORK_OBJ_DIR=${CONFIGURATION_TEMP_DIR}/${FRAMEWORK_NAME}.build/Objects-normal/${NATIVE_ARCH}

    mkdir -p coverage
    pushd coverage
    find ${OBJROOT} -name *.gcda -exec gcov -o ${FRAMEWORK_OBJ_DIR} {} \;
    popd
fi 
  • Now, make sure you configure all the Build Settings as shown below
  • Pay special attention to differences for the Coverage settings
  • I find it makes it easier to select the SampleLib Project and the 2 targets: SampleLib & SampleLibTests, so that Xcode display a matrix with all the settings (see screenshot below)
- Compiler for C/C++/Objective-C -- set in Project level
    - Coverage      = GCC 4.2
    - Debug/Release = LLVM GCC 4.2

- Other Linker Flags -- set in Target Level
    - SampleLib + Coverage      = -lgcov
    - SampleLib + Debug/Release = -ObjC
    - SampleLibTests + All      = -all_load -framework SenTestingKit

- Library Search Paths -- set in Project level
    - Coverage      = /Developer/usr/lib/gcc/i686-apple-darwin11/4.2.1
    - Debug/Release = empty

- Test After Build -- set in Project level
    - Coverage   = Yes
    - all others = No 

- Generate Test Coverage Files -- set in Target Level
    - SampleLib + Coverage = Yes
    - all others           = No 

- Instrument Program Flow -- set in Target Level
    - SampleLib + Coverage = Yes
    - all others           = No 

- Precompile Prefix Header -- set in Target Level
    - SampleLib + Coverage = No
    - all others           = Yes

- User-Defined - "Add User-Defined Setting" -> GCOV_PREFIX
    - set it in all levels = coverage
  • You should now be able to choose Product -> Clean (⇧⌘K), Product -> Test (⌘U) and see the code coverage stats being displayed on the console output
  • If you don't, go through the settings list above and make sure you have all the settings correctly defined in your project
  • If still having problems, you can take a look at Complication no.5
  • After verifying all works fine, you might want to revert the Test scheme to run out of Debug configuration. Just go to Product -> Edit Scheme and change Test to use Debug configuration and click Ok to close the dialog.
    • Reason for that is that you'll want to see the cool report from Jenkins, and not the text-based output from Xcode.

Let's automate this -- Jenkins Configuration

If you have not yet committed the code to git up to this point, dude you're doing it wrong.... :)
Make sure all is added, committed and pushed to your master repository -- remember Part I with all that cool GIT setup?

If you already have a Jenkins machine running on your network, you might be able to re-use it. The only requirement is that it need to run on a MacOS based machine, so we can call xcodebuild. If you're feeling adventurous, you might also want to take a look at this post from Shine Technologies on Continuous Deployment of iOS Apps with Jenkins and TestFlight. It describes how to configure a Jenkins node on a MacOS machine, when the master Jenkins server runs on some other OS.

To simplify things, I have Jenkins running on the same MacOS machine. Also, differently from Shine Technologies' approach, I'll not be using Ray Yamamoto's Hudson plugin. Maybe in a future post, I'll give it a try and also integrate with TestFlight to have the complete solution.

Now, let's configure Jenkins:
  • As I said, I have Jenkins running on the same MacOS development machine. Not very recommended, but I have only 1 Mac now.
  • Get the following external plugins/packages for Jenkins
    • ocunit2junit.rb - from Jayway's blog Continuos Integration for XCode projects
    • Git and/or GitHub plugins - from Jenkin Dashboard -> Manage -> Manage Plugins -> Available
    • Jenkins Cobertura plugin - also from Dashboard -> Manage -> Manage Plugins -> Available
  • Once all plugins are installed, we're ready to create a New Job
    • Job Name = SampleLib
    • Build a free-style software project
    • Source Code Management = Git
    • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleLib.git or your GitHub repository URL
    • Build Triggers = Poll SCM (or any other) = * * * * * (to poll every minute) -- feel free to adjust to your likings
    • Build - add an Execute Shell step:
      xcodebuild -target "SampleLibTests" -configuration "Coverage" -sdk iphonesimulator4.3 | /usr/local/bin/ocunit2junit.rb
    • Build - add another Execute Shell step:
      gcovr -r SampleLib -e /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.3.sdk -x -o coverage/coverage.xml
    • Post-build Actions
      • check Publish Coberura Coverage Report, Cobertura xml report pattern = **/coverage.xml
      • check Publish JUnit test result report, Test report XMLs = test-reports/*.xml
  • You might want to configure other settings for your Jenkins jobs, following whatever standards on your company. The above are the ones needed for the code coverage automation.
  • Save the job and Build Now
  • If all goes correctly, you should see the code coverage report. To see the Unit test report, just run a 2nd build.
  • Below are some screenshots of the Jenkins configuration, and generated reports

Building the SampleApp

On the next post, we'll show how to use the lib on a SampleApp, and have it all building and testing with Jenkins.

Thursday, August 4, 2011

iOS Continuous Integration

Background

Recently we've been re-organizing our code looking for common/re-usable components and extracting them as libraries. The idea is to organize the code in such a way that we can have people dedicated on the core functionality (REST/JSON parsing), object layer, and UI components.

This separation also helps us with testing and code coverage, as the person responsible for a library is also responsible for its unit testing. I've been researching ways of organizing code in XCode and also Continuous Integration best practices.

There's a bunch of information out there and here are some very helpful links:
Libraries
Continuous Integration & Testing

Based on the research, we ended up with something like:

  • split the code in libraries -- core, model, UI, etc.
  • use XCode4's Workspaces, rather than compile fat-libraries. Let XCode4 take care of compiling and linking the right version of library for us.
  • distribute and use the libraries in other projects as GIT submodules.
  • automate CI with Jenkins + Unit Test + Code Coverage.


Sample Project

I'll break the post in three parts. Part I will present the sample project and setup the GIT repositories for it. Part II will talk about CI + Unit Testing + Code Coverage for the Sample Lib, and finally Part III talks about the SampleApp and how everything comes together so you can have a great development environment for your iOS projects.

The Sample Project is really simple and consists of:
  • SampleLib - simple library with a Category on NSDate to print humanized dates
  • SampleApp - displays a TableView with a bunch of dates
Note: The main goal is not actually the project, but the steps to configure it to achieve automated CI.

Part I - Setting up Repositories

This is to configure the *GIT server* repository. It should be done only *once* when setting up a new GIT server to host the project files. The way I did for this post was to setup and work on local repository, and later push it to github. Steps:
  • choose a location for the server repository, i.e. ~/Work/GIT-REPO
  • run the script below to setup the following repositories:
    • *SampleLib* - hosts the Library sources
    • If you have more libraries, you can easily modify the script below to setup it up all at once.
  • this script creates folders private and public
    • private - contains initial setup files - readme & .gitignore
    • public - the .git file used for cloning this repository
Note: The script assumes both files below are present under ~/Work/GIT-REPO:
  • gitignore_template - template for .gitignore
  • setupLibs.sh - shell script to create the repositories
#### `.gitignore`
# xcode noise
build/*
dist/*
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
*.xcuserdatad/
profile

#automated testing & code coverage
test-reports/*
coverage/*

# osx noise
.LSOverride
*.swp
.DS_Store
#!/bin/sh

ROOT_REPO=~/Work/GIT-REPO

mkdir -p ${ROOT_REPO}/public
mkdir -p ${ROOT_REPO}/private
cd ${ROOT_REPO}/private

for mod in SampleLib; do
 echo "Initializing GIT structure for $mod..."   
 mkdir $mod
 cd $mod
 git init
 cp ${ROOT_REPO}/../gitignore_template ./.gitignore
 echo "$mod Readme" > Readme.md
 git add .
 git commit -m "Initial commit, setting up $mod"
 git clone --bare . ${ROOT_REPO}/public/$mod.git
 git remote add origin ${ROOT_REPO}/public/$mod.git
 git config branch.master.remote origin
 git config branch.master.merge refs/heads/master
 cd ..
done

echo "DONE!"

Repository Setup -- SampleApp

  • Client application will reference SampleLib libraries by using Xcode4 Workspaces
  • SampleLib will be added as submodules of SampleApp. This is useful also if you want to add other 3rd party libraries, like three20
  • The script below automates this process when setting up your GIT repository
#!/bin/sh

ROOT_REPO=~/Work/GIT-REPO

cd ${ROOT_REPO}/private

echo "Initializing GIT structure for SampleApp..."
for mod in SampleApp; do
 mkdir $mod
 cd $mod
 git init
 cp ${ROOT_REPO}/../gitignore_template ./.gitignore
 echo "$mod Readme" > Readme.md
 mkdir Library
 echo "Libraries folder" > Library/Readme.md
 git add .
 git commit -m "Initial commit, setting up $mod"
 git clone --bare . ${ROOT_REPO}/public/$mod.git
 git remote add origin ${ROOT_REPO}/public/$mod.git
 git config branch.master.remote origin
 git config branch.master.merge refs/heads/master
 cd ..
done

echo "Setting up GIT Submodules...."
cd SampleApp
#SampleLib -- assuming local
git submodule add ${ROOT_REPO}/public/SampleLib.git Library/SampleLib

#SampleLib -- assuming github
#git submodule add git://github.com/rodrigo-lima/SampleLib.git Library/SampleLib

#External libs -- for example three20
#git submodule add git://github.com/facebook/three20.git Library/three20

#initial commit
git add .
git commit -m "Adding submodules."
git push

echo "DONE!"

Using the Development Repository -- SampleLib

The beauty of having separate repositories for your libraries is that you can have individuals responsible for the whole coding process for each library, including writing the test cases. Another advantage with the proposed setup, is that Jenkins will be building, testing and generating code coverage reports automatically for each commit. A person who wishes to work on the SampleLib just needs to:
  • go to your development folder and clone the SampleLib repo
  • open SampleLib.xcodeproj with XCode
  • code away :)
rolima:~ $ cd ~/Work/
 rolima:~/Work $ git clone ~/Work/GIT-REPO/public/SampleLib.git
 rolima:~/Work $ cd SampleLib
 rolima:~/Work/SampleLib $ 

Using the Development Repository -- SampleApp

For the person responsible for working on the actual App, he just needs the following in order to clone it from GIT and start working:
  • go to your development folder and clone the ClientApp repo
  • must also init & update submodules
  • open SampleApp.xcworkspace with XCode - NOTE - it's the Workspace
rolima:~ $ cd ~/Work/
rolima:~/Work $ git clone ~/Work/GIT-REPO/public/SampleApp.git
rolima:~/Work $ cd SampleApp
rolima:~/Work/SampleApp (master)$ cd SampleApp
rolima:~/Work/SampleApp (master)$ git submodule init
....

rolima:~/Work/SampleApp (master)$ git submodule update
.....

rolima:~/Work/SampleApp (master)$
Note that once you run git submodule init/update, the submodules will be on a detached branch, so you need to check it out to some branch before making any work.
rolima:~/Work/SampleApp (master)$ cd Library/SampleLib
rolima:~/Work/SampleApp/Library/SampleLib ((no branch))$ git checkout master
rolima:~/Work/SampleApp/Library/SampleLib (master)$ cd ../../
rolima:~/Work/SampleApp (master)$ 
We now have the environment ready for Part II .
In future, if you want to send this repository to your github account, you just need to follow the instructions from github -- Manual clone and push. First, create a new repo on the target account. Next, create a mirror that includes all branches and tags. To do this you need to do a bare-clone followed by a mirror-push:
rolima:~/Work/tmp $ git clone --bare ~/Work/GIT-REPO/public/SampleLib.git
rolima:~/Work/tmp $ cd SampleLib
rolima:~/Work/tmp/SampleLib $ git push --mirror git@github.com:mycompany/SampleLib.git
rolima:~/Work/tmp/SampleLib $ cd ..
rolima:~/Work/tmp $ rm -rf SampleLib