Pull to refresh

Анализ Code Coverage для iOS и OS X проектов (xCode 4.4)

Development for iOS *Objective C *

Предисловие


Этот топик не ставит своей целью рассказать о code coverage, и о том, нужно это средство или нет. Также, не будет подниматься вопрос о целесообразности тестов в iOS проектах (положим, что они все-таки кому-то нужы, а значит есть).

Мотивация


Очень удобно, когда средства для профилирования/анализа встроены в IDE. История С code coverage в xCode не совсем безоблачная: во времена xCode 3.x и GCC все было просто и нужные ссылки и флаги компилятора гуглились на раз. C приходом xCode 4.1 все стало немного сложнее ввиду использования LLVM-GCC, приходилось идти на некоторые ухищрения (вплоть до сборки LLVM своими руками). А в 4.3 библиотеку libprofile_rt переместили в другую директорию, что тоже вызвало немало проблем.

Опытным путем было установлено, что для xCode 4.4 настройка code coverage занимает несколько минут, а раз это дешево, то почему бы не использовать? Альтернативный практический вариант использования, проверенный на практике — тестирование непосредственно кода проекта и поиск 'мертвого' кода, с последующим его анализом и чисткой.

Настройка проекта в xCode 4.4


Создадим новый проект (iOS / OS X) c галочкой Include Unit Tests. Можно использовать мой тестовый проект с готовыми юнит-тестами.
Настройка проекта включает два шага:
1. Открываем таргет %project-name%, и выставляем флаги в секции Code generation:
Generate Test Coverage Files = YES
Instrument Program Flow = YES


image

2. Только для iOS. Во избежание крешей, описаных здесь, необходимо добавить в проект *.c файл со следующим содержимым:

#include <stdio.h>

FILE* fopen$UNIX2003(const char* filename, const char* mode);
size_t fwrite$UNIX2003(const void* ptr, size_t size, size_t nitems, FILE* stream);

FILE* fopen$UNIX2003(const char* filename, const char* mode) {
    return fopen(filename, mode);
}

size_t fwrite$UNIX2003(const void* ptr, size_t size, size_t nitems, FILE* stream) {
    return fwrite(ptr, size, nitems, stream);
}

Вот и вся настройка проекта. Теперь, после запуска %projectname%Tests, необходимо открыть директорию (для iOS) "/Users/%user%/Library/Developer/Xcode/DerivedData/%project-nameBLABLABLABLA%/Build/Intermediates/%project-name%.build/Debug-iphonesimulator/%project-name%.build/Objects-normal/i386". В этой директории нас интересуют файлы *.gcda и *.gcno, которые содержат данные о покрытии. Важно: если вы собираетесь тестировать покрытие кода приложения, а не тестов, необходимо в *.plist указать UIApplicationExitsOnSuspend = YES, так как файлы *.gcda создаются только после 'усрешного' завершения работы программы.

Для наглядности привожу код тестируемого класса и несколько тестов:

#import "MyCalc.h"

@implementation MyCalc

- (CGFloat)performOperation:(MyMathOperation)operation withA:(CGFloat)a B:(CGFloat)b {
    CGFloat result = 0.f;
    
    switch (operation) {
        case MyMathOperationAdd:
            result = a + b;
            break;
        case MyMathOperationSubtract:
            result = a - b;
            
            break;
        case MyMathOperationDivide:
            result = a / b;
            
            break;
        case MyMathOperationMultiply:
            result = a * b;
            break;
        default:
            NSLog(@"Unsupported operation");
            break;
    }
    return result;
}
- (CGFloat)negate:(CGFloat)number {
    //this method works incorrectly
    return number;
}
@end


- (void)testNegation {
    CGFloat input = 3;
    CGFloat expected = -3;
    
    CGFloat result = [self.calculator negate:input];
    STAssertEquals(result, expected, @"Negation failed. Expected: %f, Actual: %f", expected, result);
}

- (void)testAddition {
    CGFloat a = 3;
    CGFloat b = 4;
    CGFloat expected = a + b;
    
    CGFloat result = [self.calculator performOperation:MyMathOperationAdd withA:a B:b];
    STAssertEquals(result, expected, @"Addition failed. Expected: %f, Actual: %f", expected, result);
}

- (void)testMultiplication {
    CGFloat a = 14;
    CGFloat b = 3;
    CGFloat expected = a * b;
    
    CGFloat result = [self.calculator performOperation:MyMathOperationMultiply withA:a B:b];
    STAssertEquals(result, expected, @"Addition failed. Expected: %f, Actual: %f", expected, result);
}


Анализ результатов


Рассмотрим несколько средств для представления статистики в удобном для человека формате.

gcov

gcov — утилита, которая генерирует статистику покрытия на основании файлов *.gcda и *.gcno. До недавнего времени работала только с GCC, в настоящий момент отлично работает и с LLVM. На выходе получаем plain-text отчет.
Для примера, вот результат запуска на файлах MyCalc.gcda из тестового проекта:



На выходе имеем статистику в процентах о покрытии, а также файл MyCalc.m.gcov:

        -:    0:Source:/Users/dlebedev/src/sandbox/Coverage/iOS/iOSCoverage/../../Common/MyCalc.m
        -:    0:Graph:MyCalc.gcno
        -:    0:Data:MyCalc.gcda
        -:    0:Runs:0
        -:    0:Programs:0
        -:    1://
        -:    2://  MyCalc.m
        -:    3://  iOSCoverage
        -:    4://
        -:    5://  Created by Denis Lebedev on 23.08.12.
        -:    6://  Copyright (c) 2012 Denis Lebedev. All rights reserved.
        -:    7://
        -:    8:
        -:    9:#import "MyCalc.h"
        -:   10:
        -:   11:@implementation MyCalc
        -:   12:
        2:   13:- (CGFloat)performOperation:(MyMathOperation)operation withA:(CGFloat)a B:(CGFloat)b {
        2:   14:    CGFloat result = 0.f;
        -:   15:    
        2:   16:    switch (operation) {
        -:   17:        case MyMathOperationAdd:
        1:   18:            result = a + b;
        1:   19:            break;
        -:   20:        case MyMathOperationSubtract:
    #####:   21:            result = a - b;
        -:   22:            
    #####:   23:            break;
        -:   24:        case MyMathOperationDivide:
    #####:   25:            result = a / b;
        -:   26:            
    #####:   27:            break;
        -:   28:        case MyMathOperationMultiply:
        1:   29:            result = a * b;
        1:   30:            break;
        -:   31:        default:
    #####:   32:            NSLog(@"Unsupported operation");
    #####:   33:            break;
        -:   34:    }
        2:   35:    return result;
        -:   36:}
        1:   37:- (CGFloat)negate:(CGFloat)number {
        -:   38:    //this method works incorrectly
        1:   39:    return number;
        -:   40:}
        -:   41:
        -:   42:@end


#####:- строка не выполнилась.
n: — строка исполнилась n раз.

Более подробно можно почитать здесь.

CoverStory

CoverStory — GUI надстройка над gcov, дополнительно позволяет генерировать html-отчеты с помощью Apple Script.



lcov

lcov — еще один графический фронт-енд для gcov. Очень удобен при наличии большого количества файлов, так как группирует html по директориям, а также при автоматизации процесса — утилита работает из терминала.

Установка lcov:

# sudo mkdir -p /usr/local/src; cd /usr/local/src
# sudo wget http://downloads.sourceforge.net/ltp/lcov-1.6.tar.gz
# sudo tar -xzvf lcov-1.6.tar.gz
# cd lcov-1.6

# sudo vim /usr/local/src/lcov-1.6/bin/install.sh</code>

В строке 34 (install -D $SOURCE $TARGET) необходимо удалить флаг -D.

# sudo make install

Для получения отчета выполним следующие команды в папке с *.gcda-файлами:

lcov -t 'Code coverage report' -o report.info -c -d .
genhtml -o html-report report.info

Результат в папке html-report:


Автоматизация


Как уже упоминалось выше, lcov удобен в continuos integration. Демонстрируемый пример будет исключительно академическим и с досадными недостатками (так и не удалось применить скрипт к проекту, использующему CocoaPods).

Код скрипта (его также можно найти в папке с тестовым iOS проектом):
#!/bin/sh
TARGET_NAME="iOSCoverage"
TEST_TARGET_NAME="iOSCoverageTests"
BUILD_CONFIG="Debug"
SDK_VERSION="iphonesimulator5.1"
rm -rf build
rm -rf html-report
echo Building and running tests
xcodebuild -target $TEST_TARGET_NAME OBJECT_FILE_DIR_normal=/build/ TEST_AFTER_BUILD=YES -sdk iphonesimulator5.1 -configuration $BUILD_CONFIG -xcconfig settings.xcconfig
echo Copying files
mkdir build/gcda
cp build/$TARGET_NAME.build/$BUILD_CONFIG-iphonesimulator/$TARGET_NAME.build/Objects-normal/i386/*.gcda build/gcda/
cp build/$TARGET_NAME.build/$BUILD_CONFIG-iphonesimulator/$TARGET_NAME.build/Objects-normal/i386/*.gcno build/gcda/
echo Generating report
cd build/gcda
lcov -t 'Code coverage report' -o report.info -c -d .
cd .. 
cd ..
genhtml -o html-report build/gcda/report.info

Содержимое settings.xcconfig (в проекте можно не прописывать флаги):
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
GCC_GENERATE_TEST_COVERAGE_FILES = YES

Кладем скрипт и файл settings.xcconfig рядом с проектом (предварительно заменяем переменные названий таргетов и SDK на нужные), запускаем… и получаем ошибку. Так как изначально iPhone Simulator не умеет запускать тесты из командной строки. Как починить это досадное недоразумение, описано здесь. После этого, запускаем скрипт еще раз и получаем папку html-report со статистикой.

UPD: Для более тесной интеграции с Jenkins можно использовать gcovr + Cobertura Plugin . Спасибо moborb за наводку.
Tags:
Hubs:
Total votes 16: ↑15 and ↓1 +14
Views 6.8K
Comments Comments 7