xcov 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ require "commander"
2
+ require "fastlane_core"
3
+
4
+ HighLine.track_eof = false
5
+
6
+ module Xcov
7
+ class CommandsGenerator
8
+
9
+ include Commander::Methods
10
+
11
+ FastlaneCore::CommanderGenerator.new.generate(Xcov::Options.available_options)
12
+
13
+ def self.start
14
+ FastlaneCore::UpdateChecker.start_looking_for_update("xcov")
15
+ new.run
16
+ ensure
17
+ FastlaneCore::UpdateChecker.show_update_status("xcov", Xcov::VERSION)
18
+ end
19
+
20
+ def convert_options(options)
21
+ o = options.__hash__.dup
22
+ o.delete(:verbose)
23
+ o
24
+ end
25
+
26
+ def run
27
+ program :version, Xcov::VERSION
28
+ program :description, Xcov::DESCRIPTION
29
+ program :help, "Author", "Carlos Vidal <nakioparkour@gmail.com>"
30
+ program :help, "Website", "http://www.nakiostudio.com"
31
+ program :help, "GitHub", "https://github.com/nakiostudio/xcov"
32
+ program :help_formatter, :compact
33
+
34
+ global_option("--verbose") { $verbose = true }
35
+
36
+ command :report do |c|
37
+ c.syntax = "Xcov"
38
+ c.description = Xcov::DESCRIPTION
39
+ c.action do |_args, options|
40
+ config = FastlaneCore::Configuration.create(Xcov::Options.available_options, convert_options(options))
41
+ Xcov::Manager.new.work(config)
42
+ end
43
+ end
44
+
45
+ default_command :report
46
+
47
+ run!
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+
2
+ module Xcov
3
+ class ErrorHandler
4
+
5
+ class << self
6
+ # @param [String] The output of the errored build
7
+ # This method should raise an exception in any case, as the return code indicated a failed build
8
+ def handle_error(output)
9
+ # The order of the handling below is import
10
+ case output
11
+ when /US\-ASCII/
12
+ print "Your shell environment is not correctly configured"
13
+ print "Instead of UTF-8 your shell uses US-ASCII"
14
+ print "Please add the following to your '~/.bashrc':"
15
+ print ""
16
+ print " export LANG=en_US.UTF-8"
17
+ print " export LANGUAGE=en_US.UTF-8"
18
+ print " export LC_ALL=en_US.UTF-8"
19
+ print ""
20
+ print "You'll have to restart your shell session after updating the file."
21
+ print "If you are using zshell or another shell, make sure to edit the correct bash file."
22
+ print "For more information visit this stackoverflow answer:"
23
+ print "https://stackoverflow.com/a/17031697/445598"
24
+ when /CoverageNotFound/
25
+ print "Unable to find any .xccoverage file."
26
+ print "Make sure you have enabled 'Gather code coverage' setting on your scheme settings."
27
+ print "Alternatively you can provide the full path to your .xccoverage file."
28
+ when /Executed/
29
+ # this is *really* important:
30
+ # we don't want to raise an exception here
31
+ # as we handle this in runner.rb at a later point
32
+ # after parsing the actual test results
33
+ return
34
+ end
35
+ raise "Error processing coverage file - see the log above".red
36
+ end
37
+
38
+ private
39
+
40
+ def print(text)
41
+ Helper.log.error text.red
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ require "fastlane_core"
2
+
3
+ module Xcov
4
+ class Manager
5
+
6
+ def work(options)
7
+ Xcov.config = options
8
+ FastlaneCore::PrintTable.print_values(config: options, hide_keys: [:slack_url], title: "Summary for xCov #{Xcov::VERSION}")
9
+ Runner.new.run
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ require 'erb'
2
+
3
+ module Xcov
4
+ class Base
5
+
6
+ attr_accessor :name
7
+ attr_accessor :coverage
8
+ attr_accessor :displayable_coverage
9
+ attr_accessor :coverage_color
10
+
11
+ attr_accessor :id
12
+
13
+ def create_displayable_coverage
14
+ "%.0f%%" % [(@coverage*100)]
15
+ end
16
+
17
+ def create_coverage_color
18
+ if @coverage > 0.8
19
+ return "#1FCB32"
20
+ elsif @coverage > 0.65
21
+ return "#FCFF00"
22
+ elsif @coverage > 0.5
23
+ return "#FF9C00"
24
+ else
25
+ return "#FF0000"
26
+ end
27
+ end
28
+
29
+ def create_summary
30
+ if @coverage > 0.8
31
+ return "Overall coverage is good"
32
+ elsif @coverage > 0.65
33
+ return "There is room for improvement"
34
+ elsif @coverage > 0.5
35
+ return "Almost unmaintainable"
36
+ else
37
+ return "Keep calm and leave the boat"
38
+ end
39
+ end
40
+
41
+ # Class methods
42
+ def self.template(name)
43
+ ERB.new(File.read(File.join(File.dirname(__FILE__), "../../../views/", "#{name}.erb")))
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ require 'cgi'
2
+
3
+ module Xcov
4
+ class Function < Xcov::Base
5
+
6
+ def initialize (name, coverage)
7
+ @name = CGI::escapeHTML(name)
8
+ @coverage = coverage
9
+ @displayable_coverage = self.create_displayable_coverage
10
+ @coverage_color = self.create_coverage_color
11
+ end
12
+
13
+ def print_description
14
+ puts "\t\t\t#{@name} (#{@displayable_coverage})"
15
+ end
16
+
17
+ def html_value
18
+ Function.template("function").result(binding)
19
+ end
20
+
21
+ # Class methods
22
+
23
+ def self.map (dictionary)
24
+ Function.new(dictionary["name"], dictionary["coverage"])
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+
2
+ module Xcov
3
+ class Report < Xcov::Base
4
+
5
+ attr_accessor :coverage
6
+ attr_accessor :targets
7
+ attr_accessor :summary
8
+ attr_accessor :target_templates
9
+
10
+ def initialize (targets)
11
+ @targets = targets
12
+ @coverage = average_coverage(targets)
13
+ @displayable_coverage = self.create_displayable_coverage
14
+ @coverage_color = self.create_coverage_color
15
+ @summary = self.create_summary
16
+ end
17
+
18
+ def average_coverage targets
19
+ coverage = 0
20
+ targets.each do |target|
21
+ coverage = coverage + target.coverage
22
+ end
23
+ coverage / targets.count
24
+ end
25
+
26
+ def print_description
27
+ puts "Total coverage: (#{@coverage})"
28
+ @targets.each do |target|
29
+ target.print_description
30
+ end
31
+ end
32
+
33
+ def html_value
34
+ @target_templates = ""
35
+ @targets.each do |target|
36
+ @target_templates << target.html_value
37
+ end
38
+
39
+ Function.template("report").result(binding)
40
+ end
41
+
42
+ # Class methods
43
+
44
+ def self.map dictionary
45
+ targets = dictionary["targets"]
46
+ .select { |target| !target["name"].include?(".xctest") }
47
+ .map { |target| Target.map(target)}
48
+
49
+ Report.new(targets)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ require 'cgi'
2
+
3
+ module Xcov
4
+ class Source < Xcov::Base
5
+
6
+ attr_accessor :name
7
+ attr_accessor :type
8
+ attr_accessor :coverage
9
+ attr_accessor :functions
10
+ attr_accessor :function_templates
11
+
12
+ def initialize (name, coverage, functions)
13
+ @name = CGI::escapeHTML(name)
14
+ @coverage = coverage
15
+ @functions = functions
16
+ @displayable_coverage = self.create_displayable_coverage
17
+ @coverage_color = self.create_coverage_color
18
+ @id = Digest::SHA1.hexdigest(name)
19
+ @type = Source.type(name)
20
+ end
21
+
22
+ def print_description
23
+ puts "\t\t#{@name} (#{@coverage})"
24
+ @functions.each do |function|
25
+ function.print_description
26
+ end
27
+ end
28
+
29
+ def html_value
30
+ @function_templates = ""
31
+ @functions.each do |function|
32
+ @function_templates << function.html_value
33
+ end
34
+
35
+ Function.template("file").result(binding)
36
+ end
37
+
38
+ # Class methods
39
+
40
+ def self.map (dictionary)
41
+ name = dictionary["name"]
42
+ coverage = dictionary["coverage"]
43
+ functions = dictionary["functions"].map { |function| Function.map(function)}
44
+
45
+ Source.new(name, coverage, functions)
46
+ end
47
+
48
+ def self.type (name)
49
+ types_map = {
50
+ ".swift" => "swift",
51
+ ".m" => "objc",
52
+ ".cpp" => "cpp",
53
+ ".mm" => "cpp"
54
+ }
55
+
56
+ extension = File.extname(name)
57
+ type = types_map[extension]
58
+ type = "objc" if type.nil?
59
+
60
+ type
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ require 'cgi'
2
+
3
+ module Xcov
4
+ class Target < Xcov::Base
5
+
6
+ attr_accessor :name
7
+ attr_accessor :coverage
8
+ attr_accessor :files
9
+ attr_accessor :file_templates
10
+
11
+ def initialize (name, coverage, files)
12
+ @name = CGI::escapeHTML(name)
13
+ @coverage = coverage
14
+ @files = files
15
+ @displayable_coverage = self.create_displayable_coverage
16
+ @coverage_color = self.create_coverage_color
17
+ @id = Digest::SHA1.hexdigest(name)
18
+ end
19
+
20
+ def print_description
21
+ puts "\t#{@name} (#{@coverage})"
22
+ @files.each do |file|
23
+ file.print_description
24
+ end
25
+ end
26
+
27
+ def html_value
28
+ @file_templates = ""
29
+ @files.each do |file|
30
+ @file_templates << file.html_value
31
+ end
32
+
33
+ Function.template("target").result(binding)
34
+ end
35
+
36
+ # Class methods
37
+
38
+ def self.map (dictionary)
39
+ name = dictionary["name"]
40
+ coverage = dictionary["coverage"]
41
+ files = dictionary["files"].map { |file| Source.map(file)}
42
+ files = files.sort! { |lhs, rhs| lhs.coverage <=> rhs.coverage }
43
+
44
+ Target.new(name, coverage, files)
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,69 @@
1
+ require "fastlane_core"
2
+ require "credentials_manager"
3
+
4
+ module Xcov
5
+ class Options
6
+
7
+ def self.available_options
8
+ containing = FastlaneCore::Helper.fastlane_enabled? ? './fastlane' : '.'
9
+
10
+ [
11
+ FastlaneCore::ConfigItem.new(key: :workspace,
12
+ short_option: "-w",
13
+ env_name: "XCOV_WORKSPACE",
14
+ optional: true,
15
+ description: "Path the workspace file",
16
+ verify_block: proc do |value|
17
+ v = File.expand_path(value.to_s)
18
+ raise "Workspace file not found at path '#{v}'".red unless File.exist?(v)
19
+ raise "Workspace file invalid".red unless File.directory?(v)
20
+ raise "Workspace file is not a workspace, must end with .xcworkspace".red unless v.include?(".xcworkspace")
21
+ end),
22
+ FastlaneCore::ConfigItem.new(key: :project,
23
+ short_option: "-p",
24
+ optional: true,
25
+ env_name: "XCOV_PROJECT",
26
+ description: "Path the project file",
27
+ verify_block: proc do |value|
28
+ v = File.expand_path(value.to_s)
29
+ raise "Project file not found at path '#{v}'".red unless File.exist?(v)
30
+ raise "Project file invalid".red unless File.directory?(v)
31
+ raise "Project file is not a project file, must end with .xcodeproj".red unless v.include?(".xcodeproj")
32
+ end),
33
+ FastlaneCore::ConfigItem.new(key: :scheme,
34
+ short_option: "-s",
35
+ optional: true,
36
+ env_name: "XCOV_SCHEME",
37
+ description: "The project's scheme. Make sure it's marked as `Shared`"),
38
+ FastlaneCore::ConfigItem.new(key: :derived_data_path,
39
+ short_option: "-j",
40
+ env_name: "XCOV_DERIVED_DATA_PATH",
41
+ description: "The directory where build products and other derived data will go",
42
+ optional: true),
43
+ FastlaneCore::ConfigItem.new(key: :output_directory,
44
+ short_option: "-o",
45
+ env_name: "XCOV_OUTPUT_DIRECTORY",
46
+ description: "The directory in which all reports will be stored",
47
+ default_value: File.join(containing, "xcov_report")),
48
+ FastlaneCore::ConfigItem.new(key: :slack_url,
49
+ short_option: "-i",
50
+ env_name: "SLACK_URL",
51
+ description: "Create an Incoming WebHook for your Slack group to post results there",
52
+ optional: true,
53
+ verify_block: proc do |value|
54
+ raise "Invalid URL, must start with https://" unless value.start_with? "https://"
55
+ end),
56
+ FastlaneCore::ConfigItem.new(key: :slack_channel,
57
+ short_option: "-e",
58
+ env_name: "XCOV_SLACK_CHANNEL",
59
+ description: "#channel or @username",
60
+ optional: true),
61
+ FastlaneCore::ConfigItem.new(key: :skip_slack,
62
+ description: "Don't publish to slack, even when an URL is given",
63
+ is_string: false,
64
+ default_value: false)
65
+ ]
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,79 @@
1
+ require 'pty'
2
+ require 'open3'
3
+ require 'fileutils'
4
+ require 'terminal-table'
5
+ require 'xcov-core'
6
+ require 'pathname'
7
+ require 'json'
8
+
9
+ module Xcov
10
+ class Runner
11
+
12
+ def run
13
+ report_json = parse_xccoverage
14
+ generate_xcov_report(report_json)
15
+ end
16
+
17
+ def parse_xccoverage
18
+ # Find .xccoverage file
19
+ product_builds_path = Pathname.new(Xcov.project.default_build_settings(key: "SYMROOT"))
20
+ test_logs_path = product_builds_path.parent.parent + "Logs/Test/"
21
+ xccoverage_files = Dir["#{test_logs_path}*.xccoverage"].sort_by { |filename| File.mtime(filename) }
22
+
23
+ unless test_logs_path.directory? && !xccoverage_files.empty?
24
+ ErrorHandler.handle_error("CoverageNotFound")
25
+ end
26
+
27
+ Xcov::Core::Parser.parse(xccoverage_files.first)
28
+ end
29
+
30
+ def generate_xcov_report report_json
31
+ # Create output path
32
+ output_path = Xcov.config[:output_directory]
33
+ FileUtils.mkdir_p(output_path)
34
+ resources_path = File.join(output_path, "resources")
35
+ FileUtils.mkdir_p(resources_path)
36
+
37
+ # Copy images to output resources folder
38
+ Dir[File.join(File.dirname(__FILE__), "../../assets/images/*")].each do |path|
39
+ FileUtils.cp_r(path, resources_path)
40
+ end
41
+
42
+ # Copy stylesheets to output resources folder
43
+ Dir[File.join(File.dirname(__FILE__), "../../assets/stylesheets/*")].each do |path|
44
+ FileUtils.cp_r(path, resources_path)
45
+ end
46
+
47
+ # Copy javascripts to output resources folder
48
+ Dir[File.join(File.dirname(__FILE__), "../../assets/javascripts/*")].each do |path|
49
+ FileUtils.cp_r(path, resources_path)
50
+ end
51
+
52
+ # Convert report to xCov model objects
53
+ report = Report.map(report_json)
54
+
55
+ # Create HTML report
56
+ File.open(File.join(output_path, "index.html"), "wb") do |file|
57
+ file.puts report.html_value
58
+ end
59
+
60
+ # Post result
61
+ SlackPoster.new.run(report)
62
+
63
+ # Print output
64
+ table_rows = []
65
+ report.targets.each do |target|
66
+ table_rows << [target.name, target.displayable_coverage]
67
+ end
68
+ puts Terminal::Table.new({
69
+ title: "xCov Coverage Report".green,
70
+ rows: table_rows
71
+ })
72
+ puts ""
73
+
74
+ # Raise exception in case of failure
75
+ raise "Unable to create coverage report" if report.nil?
76
+ end
77
+
78
+ end
79
+ end