Last active
October 1, 2017 16:47
-
-
Save TeresaP/3fe3abdba01d47d02847 to your computer and use it in GitHub Desktop.
Code Coverage with Calabash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
AfterConfiguration do |config| | |
CodeCoverage.clean_up_code_coverage_archive_folder | |
CodeCoverage.generate_lcov_baseline_info_file | |
end | |
Before do |scenario| | |
$calabash_launcher = Calabash::Cucumber::Launcher.launcher | |
CodeCoverage.clean_up_last_run_files | |
#... snip ... | |
end | |
After do |scenario| | |
begin | |
CodeCoverage.flush | |
rescue Errno::ECONNREFUSED | |
CodeCoverage.generate_failed_coverage_file(scenario) | |
raise | |
end | |
unless $calabash_launcher.calabash_no_stop? | |
calabash_exit | |
if $calabash_launcher.active? | |
$calabash_launcher.stop | |
end | |
end | |
if scenario.passed? | |
CodeCoverage.generate_lcov_info_file(scenario) | |
else | |
CodeCoverage.generate_failed_coverage_file(scenario) | |
end | |
end | |
at_exit do | |
$calabash_launcher = Calabash::Cucumber::Launcher.launcher | |
if $calabash_launcher.simulator_target? | |
$calabash_launcher.simulator_launcher.stop unless $calabash_launcher.calabash_no_stop? | |
end | |
CodeCoverage.combine_lcov_info_files | |
CodeCoverage.generate_lcov_reports_from_info_file | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Application | |
extend Calabash::Cucumber::Operations | |
# Flush the code coverage | |
def self.flush_code_coverage | |
backdoor('flushGCov:', '') | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CodeCoverage | |
@app_name = 'Foo' | |
@build_path = CODE_COVERAGE_BUILD_PARENT_PATH ||= '' | |
@code_coverage_folder_path = File.join(@build_path, 'CodeCoverage/') | |
@baseline_info_filename = "#{@app_name}_base.info" | |
@combined_info_filename = "#{@app_name}_combined.info" | |
@project_intermediates = CODE_COVERAGE_INTERMEDIATES_DIR ||= nil | |
# Check to see if we are set up to collect code coverage | |
# @return [Boolean] True if code coverage is on (CODE_COVERAGE_BUILD_PARENT_PATH was set to something), false if not | |
def self.on? | |
return !@build_path.empty? | |
end | |
# If this is a code coverage build, we need to remove the old code coverage files between each scenario | |
def self.clean_up_last_run_files | |
if self.on? | |
calabash_info("[CODE COVERAGE] Deleting .gcda files from #{@project_intermediates}") | |
`cd #{@project_intermediates} && find . -name "*.gcda" -print0 | xargs -0 rm` | |
end | |
end | |
# Clean up the folder that contains old .info files and reports | |
def self.clean_up_code_coverage_archive_folder | |
if self.on? | |
Dir.chdir(@build_path) | |
calabash_info("[CODE COVERAGE] Deleting the #{@code_coverage_folder_path} directory.") | |
FileUtils.rmtree(@code_coverage_folder_path) if Dir.exists?(@code_coverage_folder_path) | |
end | |
end | |
# Tell the app to output coverage files | |
def self.flush | |
# Flush the code coverage | |
if self.on? | |
calabash_info('[CODE COVERAGE] Flushing code coverage.') | |
Application.flush_code_coverage | |
end | |
end | |
# Generate the baseline file (before any of the tests run) | |
# @param [String] path_to_info_files The path to where the .info files will be stored | |
def self.generate_lcov_baseline_info_file(path_to_info_files=@code_coverage_folder_path) | |
baseline_info_file_path = File.join(path_to_info_files, @baseline_info_filename) | |
if self.on? | |
FileUtils.mkpath(path_to_info_files) | |
calabash_info("[CODE COVERAGE] Generating lcov baseline at #{baseline_info_file_path}.") | |
`lcov --capture --initial --directory #{@project_intermediates} --output-file #{baseline_info_file_path}` | |
end | |
end | |
# Get a sanitized filename without weird characters | |
# @param [String] file_name File name you want to sanitize | |
# @return [String] Sanitized file name | |
def self.sanitize_filename(file_name) | |
file_name = file_name.gsub(/[^\w\.\-]/, '_') | |
file_name = file_name.gsub(/\.{2,}/, '.') | |
return file_name | |
end | |
# Generate an info file for the specified scenario | |
# @param [Object] scenario See https://github.com/cucumber/cucumber/wiki/Hooks | |
def self.generate_lcov_info_file(scenario) | |
if self.on? | |
calabash_info('[CODE COVERAGE] Deleting the ObjectiveC.gcda files because the files cause errors.') | |
`cd #{@project_intermediates} && find . -name "ObjectiveC.gcda" -print0 | xargs -0 rm` | |
# Craft the filename string. Use the scenario feature and scenario name, | |
# but convert spaces to underscores and double-periods into single periods. | |
info_filename_string = "#{@app_name}_#{scenario.feature.name}__#{scenario.name}.info" | |
info_filename_string=self.sanitize_filename(info_filename_string) | |
info_file_path = File.join(@code_coverage_folder_path, info_filename_string) | |
calabash_info("[CODE COVERAGE] Generating lcov info file at #{info_file_path}.") | |
`lcov --capture --directory #{@project_intermediates} --output-file #{info_file_path}` | |
end | |
end | |
# Combine the lcov info files into a single file for reporting | |
# @param [String] path_to_info_files The path to where the .info files are stored | |
def self.combine_lcov_info_files(path_to_info_files=@code_coverage_folder_path) | |
combined_info_file_path = File.join(path_to_info_files, @combined_info_filename) | |
if self.on? | |
info_files = Dir["#{path_to_info_files}/*.info"] | |
index_of_existing_combined_info_file = info_files.index{|s| s.include?(@combined_info_filename)} | |
unless index_of_existing_combined_info_file.nil? | |
log_warning("[CODE COVERAGE] combine_lcov_info_files found an existing #{@combined_info_filename} in #{path_to_info_files}. We will ignore it from the set to combine and override it.") | |
info_files.delete_at(index_of_existing_combined_info_file) | |
end | |
if info_files.empty? | |
raise '[CODE COVERAGE] combine_lcov_info_files could not find any coverage files to combine.' | |
else | |
calabash_info("[CODE COVERAGE] Combining #{info_files.count} info files into a single file at #{path_to_info_files}.") | |
info_files_str = '--add-tracefile ' | |
info_files_str+= info_files.join(' --add-tracefile ') | |
`lcov #{info_files_str} --output-file #{combined_info_file_path}` | |
end | |
end | |
end | |
# Generate a nice html report from the specified info file | |
# @param [String] info_file (Optional) The specified info file is used to generate reports | |
# @param [String] dest_dir (Optional) The destination directory to save the files to | |
def self.generate_lcov_reports_from_info_file(info_file=nil, dest_dir="#{@code_coverage_folder_path}") | |
report_folder_path = File.join(dest_dir, 'reports') | |
info_file = File.join(@code_coverage_folder_path, @combined_info_filename) if info_file.nil? | |
if self.on? | |
unless File.exists?(info_file) | |
raise "[CODE COVERAGE] generate_lcov_reports expected to find #{info_file} at #{dest_dir} but it was not there." | |
end | |
if Dir.exists?(report_folder_path) | |
calabash_info("[CODE COVERAGE] Deleting the #{report_folder_path} directory which was there from a previous run.") | |
FileUtils.rmtree(report_folder_path) | |
end | |
calabash_info("[CODE COVERAGE] Generating code coverage report at #{report_folder_path}.") | |
`cd #{dest_dir} && genhtml --ignore-errors source #{info_file} --legend --title "#{@app_name} Code Coverage Report" --output-directory=#{report_folder_path}` | |
end | |
end | |
# Generate a .fail file so we know which test cases to rerun | |
# @param [Object] scenario See https://github.com/cucumber/cucumber/wiki/Hooks | |
def self.generate_failed_coverage_file(scenario) | |
if self.on? | |
# Craft the filename string. Use the scenario feature and scenario name, | |
# but convert spaces to underscores and double-periods into single periods. | |
info_filename_string = "#{@app_name}_#{scenario.feature.name}__#{scenario.name}.failed" | |
info_filename_string=self.sanitize_filename(info_filename_string) | |
info_file_path = File.join(@code_coverage_folder_path, info_filename_string) | |
`touch "#{info_file_path}"` | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This should be set to the parent directory containing the build folders | |
CODE_COVERAGE_BUILD_PARENT_PATH = ENV['CODE_COVERAGE_BUILD_PARENT_PATH'] | |
if CODE_COVERAGE_BUILD_PARENT_PATH | |
# NOTE: The following lines will get you the path to your intermediates dir as of XCode 7.2 if you are using a workspace. | |
# If you are using projects instead, you will want to modify this section. | |
xc_workspace_path=Dir["#{CODE_COVERAGE_BUILD_PARENT_PATH}/*.xcworkspace"].first | |
schema='YourSchemaName' | |
intermediates_dir=`xcodebuild -showBuildSettings -workspace #{xc_workspace_path} -scheme #{schema} | grep -m 1 'PROJECT_TEMP_ROOT' | sed -n -e 's/^.*PROJECT_TEMP_ROOT = //p'` | |
CODE_COVERAGE_INTERMEDIATES_DIR=intermediates_dir.strip | |
end |
@TeresaP, thanks for the reply.
After making few more changes, it works now.
This is how my env.rb looks:
require "calabash-cucumber/cucumber"
# This should be set to the parent directory containing the build folders
CODE_COVERAGE_BUILD_PARENT_PATH = ENV['CODE_COVERAGE_BUILD_PARENT_PATH']
if CODE_COVERAGE_BUILD_PARENT_PATH
# NOTE: The following lines will get you the path to your intermediates dir as of XCode 7.2 if you are using a workspace.
# If you are using projects instead, you will want to modify this section.
schema='Minion-cal'
# NOTE: If you are using a workspace
#xc_workspace_path=Dir["#{CODE_COVERAGE_BUILD_PARENT_PATH}/*.xcworkspace"].first
#intermediates_dir=`xcodebuild -showBuildSettings -workspace #{xc_workspace_path} -scheme #{schema} | grep -m 1 'PROJECT_TEMP_ROOT' | sed -n -e 's/^.*PROJECT_TEMP_ROOT = //p'`
# NOTE: If you are using a project
xcodeproj_path=Dir["#{CODE_COVERAGE_BUILD_PARENT_PATH}/*.xcodeproj"].first
intermediates_dir=`xcodebuild -showBuildSettings -project #{xcodeproj_path} -scheme #{schema} | grep -m 1 'PROJECT_TEMP_ROOT' | sed -n -e 's/^.*PROJECT_TEMP_ROOT = //p'`
CODE_COVERAGE_INTERMEDIATES_DIR=intermediates_dir.strip
end
There are some more observations, that I want to share with you.
- I have replaced the function log_message with puts.
- Commented unmount_device
I also see that, when the number of steps are small (around 13) everything works fine. But when it is around 18, the report generation fails.
Here is the error that I am getting:
1 scenario (1 passed)
18 steps (18 passed)
1m17.809s
[CODE COVERAGE] Combining 2 info files into a single file at /Users/x08m/Desktop/Minion/CodeCoverage/.
lcov: ERROR: no valid records found in tracefile /Users/me/Desktop/Minion/CodeCoverage//Minion_App_launch__Successful_Applaunch.info
/Users/me/Desktop/Minion/features/support/code_coverage.rb:119:in `generate_lcov_reports_from_info_file': [CODE COVERAGE] generate_lcov_reports expected to find /Users/x08m/Desktop/Minion/CodeCoverage/Minion_combined.info at /Users/me/Desktop/Minion/CodeCoverage/ but it was not there. (RuntimeError)
from /Users/me/Desktop/Minion/features/support/01_launch.rb:71:in `block in <top (required)>'
There is an extra / that I see in the path, which may be the issue.
Here is my sample application https://github.com/nishabe/Minion/tree/master in case you want to reproduce the issue.
@nishabe thanks for your feedback! unmount_device and log_message were a couple of internal functions I forgot to take out, so thank you :).
Teresa, where is calabash_info is defined? I get 'undefined method `calabash_info' for CodeCoverage:Class (NoMethodError)'.
Do you have any suggestions on the error scenario that I mentioned earlier?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@nishabe because this is meant to supplement the code you already have in there :)
With regard to the intermediates_dir line, I suspect you are not using a workspace with projects inside it, but rather you are using only a project. You will need to change that line to one that successfully matches your intermediates directory. I've updated the comments inline and below to indicate this.
Can you try this?
intermediates_dir=
xcodebuild -showBuildSettings -project #{xcodeproj_path} -scheme #{schema} | grep -m 1 'PROJECT_TEMP_ROOT' | sed -n -e 's/^.*PROJECT_TEMP_ROOT = //p'