├── checksum ├── .gitkeep ├── metric_fu-4.2.0.gem.sha512 ├── metric_fu-4.2.1.gem.sha512 ├── metric_fu-4.3.0.gem.sha512 ├── metric_fu-4.3.1.gem.sha512 ├── metric_fu-4.4.0.gem.sha512 ├── metric_fu-4.4.1.gem.sha512 ├── metric_fu-4.4.2.gem.sha512 ├── metric_fu-4.4.3.gem.sha512 ├── metric_fu-4.4.4.gem.sha512 ├── metric_fu-4.5.0.gem.sha512 ├── metric_fu-4.5.1.gem.sha512 ├── metric_fu-4.5.2.gem.sha512 ├── metric_fu-4.6.0.gem.sha512 ├── metric_fu-4.7.0.gem.sha512 ├── metric_fu-4.7.1.gem.sha512 ├── metric_fu-4.7.2.gem.sha512 ├── metric_fu-4.7.3.gem.sha512 ├── metric_fu-4.7.4.gem.sha512 ├── metric_fu-4.8.0.gem.sha512 ├── metric_fu-4.9.0.gem.sha512 ├── metric_fu-4.10.0.gem.sha512 ├── metric_fu-4.11.0.gem.sha512 ├── metric_fu-4.11.1.gem.sha512 ├── metric_fu-4.11.2.gem.sha512 ├── metric_fu-4.11.3.gem.sha512 ├── metric_fu-4.11.4.gem.sha512 ├── metric_fu-4.12.0.gem.sha512 └── metric_fu-4.13.0.gem.sha512 ├── spec ├── dummy │ ├── .gitkeep │ ├── lib │ │ ├── .gitkeep │ │ └── bad_encoding.rb │ ├── spec │ │ └── .gitkeep │ └── .gitignore ├── fixtures │ ├── metric_missing.yml │ ├── exit0.sh │ ├── exit1.sh │ ├── hotspots │ │ ├── stats.yml │ │ ├── reek.yml │ │ ├── roodi.yml │ │ ├── saikuro.yml │ │ ├── three_metrics_on_same_file.yml │ │ ├── several_metrics.yml │ │ ├── generator.yml │ │ └── generator_analysis.yml │ ├── line_numbers │ │ ├── two_classes.rb │ │ ├── module.rb │ │ ├── module_surrounds_class.rb │ │ └── foo.rb │ ├── coverage.rb │ ├── coverage-153.rb │ ├── saikuro_sfiles │ │ └── thing.rb_cyclo.html │ └── saikuro │ │ └── app │ │ └── controllers │ │ ├── sessions_controller.rb_cyclo.html │ │ └── users_controller.rb_cyclo.html ├── support │ ├── samples │ │ └── reek │ │ │ └── alfa.rb │ ├── .metrics │ ├── timeout.rb │ ├── suite.rb │ ├── deferred_garbaged_collection.rb │ ├── test_fixtures.rb │ ├── matcher_create_file.rb │ ├── helper_methods.rb │ └── matcher_create_files.rb ├── metric_fu │ ├── metrics │ │ ├── hotspots │ │ │ ├── analysis │ │ │ │ ├── table_spec.rb │ │ │ │ └── ranking_spec.rb │ │ │ ├── hotspot_analyzer_spec.rb │ │ │ ├── hotspot_spec.rb │ │ │ └── generator_spec.rb │ │ ├── flay │ │ │ ├── configuration_spec.rb │ │ │ └── grapher_spec.rb │ │ ├── reek │ │ │ ├── configuration_spec.rb │ │ │ └── grapher_spec.rb │ │ ├── churn │ │ │ └── configuration_spec.rb │ │ ├── roodi │ │ │ ├── configuration_spec.rb │ │ │ └── grapher_spec.rb │ │ ├── flog │ │ │ └── configuration_spec.rb │ │ ├── rcov │ │ │ ├── hotspot_spec.rb │ │ │ ├── configuration_spec.rb │ │ │ ├── generator_spec.rb │ │ │ ├── grapher_spec.rb │ │ │ └── rcov_line_spec.rb │ │ ├── cane │ │ │ └── configuration_spec.rb │ │ ├── saikuro │ │ │ ├── configuration_spec.rb │ │ │ └── generator_spec.rb │ │ ├── rails_best_practices │ │ │ ├── generator_spec.rb │ │ │ ├── configuration_spec.rb │ │ │ └── grapher_spec.rb │ │ └── stats │ │ │ └── grapher_spec.rb │ ├── loader_spec.rb │ ├── templates │ │ ├── metrics_template_spec.rb │ │ ├── report_spec.rb │ │ └── configuration_spec.rb │ ├── utility_spec.rb │ ├── gem_version_spec.rb │ ├── calculate_spec.rb │ ├── reporting │ │ ├── graphs │ │ │ ├── grapher_spec.rb │ │ │ └── graph_spec.rb │ │ └── result_spec.rb │ ├── reporter_spec.rb │ ├── metric_spec.rb │ ├── formatter │ │ ├── configuration_spec.rb │ │ └── yaml_spec.rb │ └── formatter_spec.rb ├── shared │ └── configured.rb ├── spec_helper.rb ├── capture_warnings.rb └── metric_fu_spec.rb ├── .rspec ├── .metrics ├── etc ├── erd.png └── README.md ├── .yardopts ├── lib └── metric_fu │ ├── templates │ ├── _report_footer.html.erb │ ├── _graph.html.erb │ ├── index.html.erb │ ├── css │ │ ├── bluff.css │ │ ├── rcov.css │ │ ├── syntax.css │ │ ├── default.css │ │ ├── reset.css │ │ └── buttons.css │ ├── javascripts │ │ ├── utils.js │ │ ├── bluff_graph.js │ │ └── highcharts_graph.js │ ├── configuration.rb │ ├── report.html.erb │ ├── report.rb │ └── layout.html.erb │ ├── errors │ └── analysis_error.rb │ ├── version.rb │ ├── calculate.rb │ ├── metrics │ ├── hotspots │ │ ├── analysis │ │ │ ├── groupings.rb │ │ │ ├── problems.rb │ │ │ ├── scoring_strategies.rb │ │ │ ├── grouping.rb │ │ │ ├── record.rb │ │ │ ├── ranking.rb │ │ │ ├── analyzed_problems.rb │ │ │ ├── table.rb │ │ │ └── ranked_problem_location.rb │ │ ├── metric.rb │ │ ├── generator.rb │ │ ├── hotspot_analyzer.rb │ │ └── report.html.erb │ ├── reek │ │ ├── metric.rb │ │ ├── report.html.erb │ │ └── grapher.rb │ ├── roodi │ │ ├── metric.rb │ │ ├── report.html.erb │ │ ├── hotspot.rb │ │ ├── grapher.rb │ │ └── generator.rb │ ├── rcov │ │ ├── external_client.rb │ │ ├── grapher.rb │ │ ├── rcov_line.rb │ │ ├── hotspot.rb │ │ ├── report.html.erb │ │ ├── metric.rb │ │ └── generator.rb │ ├── churn │ │ ├── metric.rb │ │ ├── hotspot.rb │ │ ├── generator.rb │ │ └── report.html.erb │ ├── flay │ │ ├── metric.rb │ │ ├── grapher.rb │ │ ├── report.html.erb │ │ ├── generator.rb │ │ └── hotspot.rb │ ├── saikuro │ │ ├── metric.rb │ │ ├── parsing_element.rb │ │ ├── hotspot.rb │ │ └── report.html.erb │ ├── flog │ │ ├── metric.rb │ │ ├── hotspot.rb │ │ ├── report.html.erb │ │ └── grapher.rb │ ├── stats │ │ ├── hotspot.rb │ │ ├── metric.rb │ │ ├── grapher.rb │ │ └── report.html.erb │ ├── rails_best_practices │ │ ├── metric.rb │ │ ├── report.html.erb │ │ ├── grapher.rb │ │ └── generator.rb │ └── cane │ │ ├── metric.rb │ │ ├── grapher.rb │ │ └── violations.rb │ ├── formatter │ ├── yaml.rb │ └── syntax.rb │ ├── cli │ ├── client.rb │ └── helper.rb │ ├── logging │ └── mf_debugger.rb │ ├── reporter.rb │ ├── formatter.rb │ ├── logger.rb │ ├── reporting │ ├── graphs │ │ ├── grapher.rb │ │ └── graph.rb │ └── result.rb │ ├── utility.rb │ ├── constantize.rb │ ├── tasks │ └── metric_fu.rake │ └── gem_run.rb ├── AUTHORS ├── bin ├── metric_fu ├── mf-cane ├── mf-flay ├── mf-reek ├── mf-churn ├── mf-roodi └── mf-saikuro ├── .rubocop.yml ├── gem_tasks ├── rubocop.rake ├── yard.rake └── usage_test.rake ├── .gitignore ├── .travis.yml ├── .github └── workflows │ └── ruby.yml ├── config └── roodi_config.yml ├── appveyor.yml ├── Rakefile ├── MIT-LICENSE ├── Guardfile ├── CONTRIBUTORS ├── certs └── bf4.pem ├── Gemfile ├── .rubocop_todo.yml ├── DEV.md └── CONTRIBUTING.md /checksum/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --profile -------------------------------------------------------------------------------- /spec/dummy/spec/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /spec/fixtures/metric_missing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /spec/support/samples/reek/alfa.rb: -------------------------------------------------------------------------------- 1 | def foo(echo); end 2 | -------------------------------------------------------------------------------- /.metrics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Put configuration code here 4 | -------------------------------------------------------------------------------- /spec/fixtures/exit0.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | which ruby 3 | exit 0 4 | -------------------------------------------------------------------------------- /spec/fixtures/exit1.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | which ruby 3 | exit 1 4 | -------------------------------------------------------------------------------- /etc/erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metricfu/metric_fu/HEAD/etc/erd.png -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --readme README.md 3 | lib/metric_fu.rb 4 | lib/metric_fu/**/*.rb 5 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/_report_footer.html.erb: -------------------------------------------------------------------------------- 1 |

Generated on <%= MetricFu.current_time %>

-------------------------------------------------------------------------------- /lib/metric_fu/errors/analysis_error.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class AnalysisError < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/stats.yml: -------------------------------------------------------------------------------- 1 | :stats: 2 | :codeLOC: 4222 3 | :testLOC: 2111 4 | :code_to_test_ratio: 2 5 | -------------------------------------------------------------------------------- /spec/support/.metrics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # For testing that this file gets loaded 4 | $metric_file_loaded = true 5 | -------------------------------------------------------------------------------- /spec/dummy/lib/bad_encoding.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | class SomeClass 3 | def initialize(_a, _b, _c) 4 | "hey, invalid ASCII\xED" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /checksum/metric_fu-4.2.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 9dab16044285faa2d79afc40e701fb35d7e41d658d147bb161f616676f3258e069c1e250c1d854460e67ca1674ae286265f43dcd50255416c0e6d8e9e919b516 -------------------------------------------------------------------------------- /checksum/metric_fu-4.2.1.gem.sha512: -------------------------------------------------------------------------------- 1 | 529b99cf3f5fb314e9a7684ad38198e7509ec13926ded779bfef2a4b968b0538fc72248606a3b0b1bdead31163e04641bd6f565d5db62bb168ee80b854e9c580 -------------------------------------------------------------------------------- /checksum/metric_fu-4.3.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 7a5e74c8b010bd9629e9cbdcf297cc1d75295338bb34a327a9ed57a742c5fc57107608bd148e31e5355263dc113841249be8719877a68fcb42271212edd71f13 -------------------------------------------------------------------------------- /checksum/metric_fu-4.3.1.gem.sha512: -------------------------------------------------------------------------------- 1 | 8ceae4f1bb2719d8ecbe2c7b3e929fe477cbe4ea6582297092a1bc09a6bef9e423f3a33c619b62372b208f956c4d8f9839257cd491475b58c06a2c8341eafc77 -------------------------------------------------------------------------------- /checksum/metric_fu-4.4.0.gem.sha512: -------------------------------------------------------------------------------- 1 | c01694dc4a374806bd6cfaf5091808d223db1ea07a85d90a9749ed8e603c3abee283a36a6aacde81072b7c0ec2eec64f85d977caa6cd69687a532040a2137bd4 -------------------------------------------------------------------------------- /checksum/metric_fu-4.4.1.gem.sha512: -------------------------------------------------------------------------------- 1 | bbcd106dba00bcf0c1bfe8562d012a82f0945d8388bcd768598b872c35655668bfc9d0c1e7b0d67369713902dd9f1dd6fba9b83b21d79fe8ee774a2f8fa2defe -------------------------------------------------------------------------------- /checksum/metric_fu-4.4.2.gem.sha512: -------------------------------------------------------------------------------- 1 | 1beb22d0d940c4d5297efbfa5a2c6ad46a5f64fd68a0695bc1d81a7b3d85fdafc9004ede390272f2602a2a87f306137fccf53e8225b9dd9943e87ef5c7103c78 -------------------------------------------------------------------------------- /checksum/metric_fu-4.4.3.gem.sha512: -------------------------------------------------------------------------------- 1 | 6d7c86a7231183dc5cf3d13b03fa4174873ffa6202d0b3fa09b485acc0e076951d6c906a99816fa9908fed75fa513cfba1ce29fb3911f881a93fb0922a70ff8f -------------------------------------------------------------------------------- /checksum/metric_fu-4.4.4.gem.sha512: -------------------------------------------------------------------------------- 1 | 287c0e2d61b94453f05b6c6b8d4666c49337022af56d4b3066ff6f9856747ef6ce5830b121bed2cfbe747716049c88ac1c9edc19a4cccc9b408454107899c663 -------------------------------------------------------------------------------- /checksum/metric_fu-4.5.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 60fdd77882c1eac658fec94a3207b00d3556e082ee9a01ce4097d8fa297df32d66de4ec2fc780b7566dc331975ad4469143dd8eb0dcd8df985c01405ce57c580 -------------------------------------------------------------------------------- /checksum/metric_fu-4.5.1.gem.sha512: -------------------------------------------------------------------------------- 1 | a1ff91abb134992e958518efddea3faaee87d987d4f7d6190a065eff5647514b9e58d16c78db061f42a53429f1647f47f3efda1bc75f746a342b9ba50bf72b1c -------------------------------------------------------------------------------- /checksum/metric_fu-4.5.2.gem.sha512: -------------------------------------------------------------------------------- 1 | cecb4dabfef6d7f955f394a9af5a8755cae4269196dfe6f4447f0d465426a8c4bb777572d3dc8404de352c0e64dd74562b6b8e65687ca79bdd247d3fbafe6e69 -------------------------------------------------------------------------------- /checksum/metric_fu-4.6.0.gem.sha512: -------------------------------------------------------------------------------- 1 | b9488463cdbac040cc3f4fdff3ec18385b3129876464ae14de2078c1742a1be7951ba40b7f2efba54809cb8e7766fc53ff63c2672ffd25452ddca2f9a9387ac4 -------------------------------------------------------------------------------- /checksum/metric_fu-4.7.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 7abc57b0cf4a342710a2dc53f9113c4797b0260912e68f386888692a72b84631bf81cee47aa9deba81ea3ddb07f8302b08279562c0802a678d78b7cc3c32ad38 -------------------------------------------------------------------------------- /checksum/metric_fu-4.7.1.gem.sha512: -------------------------------------------------------------------------------- 1 | c7769b05094f900a0b48c1e8b9e4df67372223e0540d97c94109eb00af5acf32cb20615c8c5adcf7352a3ee1ad1b3106b5b20b87ebccc3dfc86a21ca5709490b -------------------------------------------------------------------------------- /checksum/metric_fu-4.7.2.gem.sha512: -------------------------------------------------------------------------------- 1 | ad47170c438b84335051bae41480a00dec428e1f755a70813364c6232f43d16c15c55813ffb555e5236189360fe84bfcbe194e6cd54b304d3b2688985b3609ac -------------------------------------------------------------------------------- /checksum/metric_fu-4.7.3.gem.sha512: -------------------------------------------------------------------------------- 1 | 9a3c76074181ca4ec770f1a52f652268e44af10648cf82cb641672d2075d92133751ad275ad1ba417c473ba8095d872df762eb379e2e48fd38ce1db06b4ea1e7 -------------------------------------------------------------------------------- /checksum/metric_fu-4.7.4.gem.sha512: -------------------------------------------------------------------------------- 1 | 26caa046f537a1a81415bf5f238a7cd36a5b1c1f3f2e1008276e0fdbb35d08925fbf3f2bdd95477f75f716088639630cebec9ca5d300f03d6cd6cb3ae1c437dc -------------------------------------------------------------------------------- /checksum/metric_fu-4.8.0.gem.sha512: -------------------------------------------------------------------------------- 1 | d46528ba18e30ce561e3d0dfb4245ee2e4b7ff72a895cff24cb4bb4a86ace4ed3f759eba96b26aa9e512ebd253d78d9697423bf468c09a09fb764597977f26c2 -------------------------------------------------------------------------------- /checksum/metric_fu-4.9.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 6a59eb643743ccb2d34c7de915a0c448c6409a8b31a76b5bc8315fced802e5e186f8d97a7973c95ba306ac17d7de59d015cd93c50bda56180a952c91ac56c216 -------------------------------------------------------------------------------- /checksum/metric_fu-4.10.0.gem.sha512: -------------------------------------------------------------------------------- 1 | e71e2ed626a7b3ee170116c5e72d846ba36039b0f016eb31ca87c9815534f27d5c85cf9bb42308bb2b3f539d123aa3ffea8fb7692c9a1c6f2b616a3511bf11f1 -------------------------------------------------------------------------------- /checksum/metric_fu-4.11.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 5402db0fe4fb45627ce37d071b0d0d0ec4d173320cfffb251e3e0406556e828ac97fc416a4ddd9387b39aa9e374687ebf900f94cb0a578d771b12ae4af8506c6 -------------------------------------------------------------------------------- /checksum/metric_fu-4.11.1.gem.sha512: -------------------------------------------------------------------------------- 1 | 2d38eac5067748ee4417ab7088302a9e6c465e5163712cef9a1998bcb7048bf74c1b5c1facaccd8af042d2362693e4774de5b8c2bc6dbae5613eb91920ec58aa -------------------------------------------------------------------------------- /checksum/metric_fu-4.11.2.gem.sha512: -------------------------------------------------------------------------------- 1 | c31f9343f44972b7b614d97cf33d457ad11518b408fb5e8e009eff8ee3c88353bd1dab14ac143a313433b7920b96f89d34753a0cd910364a70396ead3dc39980 -------------------------------------------------------------------------------- /checksum/metric_fu-4.11.3.gem.sha512: -------------------------------------------------------------------------------- 1 | 2290ac18a8467b1ec187273cd94afdb3c2cb187dcf1c58152b99acf54e4249746512ef55604a8b4dc348ad106b7b25ea4a5f225fd2e9659367d20340407578d5 -------------------------------------------------------------------------------- /checksum/metric_fu-4.11.4.gem.sha512: -------------------------------------------------------------------------------- 1 | 9a74861bf6d89f703786e562cdff96eea727aac874e0d9615d4ac8af57b2c97d15e2121de8d978b7097b56bd72b40744c9a27be9d3d12f9ecf75b66ac565e3ef -------------------------------------------------------------------------------- /checksum/metric_fu-4.12.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 5af713f345549aa574af94d5790ec5360d61aa9ff9dce867da339f5a86e1d2d39c23d025434dd98fc04e0912b6a6da64f271663242a5aa75cbf15cac1c9c73e1 -------------------------------------------------------------------------------- /checksum/metric_fu-4.13.0.gem.sha512: -------------------------------------------------------------------------------- 1 | e57d3944216f0d2f9ff06ddabae68cc240510df5a3b346739d866c9dc9e86d5bc7e341efd13f5bc34f5d06bdb90cf455a4491dfc65d2f197bc0ca616ea911f7c -------------------------------------------------------------------------------- /spec/fixtures/line_numbers/two_classes.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | def stuff 3 | 5 4 | end 5 | end 6 | 7 | class Bar 8 | def stuff 9 | 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/_graph.html.erb: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/hotspots/analysis/table_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/analysis/table" } 3 | 4 | describe MetricFu::Table do 5 | it "needs tests" 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/line_numbers/module.rb: -------------------------------------------------------------------------------- 1 | module KickAss 2 | 3 | def get_beat_up? 4 | [1,2,3].inject {|m,o| m = m + o} 5 | true 6 | end 7 | 8 | def fight 9 | "poorly" 10 | end 11 | end -------------------------------------------------------------------------------- /spec/fixtures/coverage.rb: -------------------------------------------------------------------------------- 1 | class SomeClass 2 | def method_1 3 | 1 + 1 4 | end 5 | 6 | def method_2(value) 7 | value * value 8 | end 9 | 10 | def method_3 11 | "über" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jake Scruggs 2 | Sean Soper 3 | Andre Arko 4 | Petrik de Heus 5 | Grant McInnes 6 | Nick Quaranto 7 | Édouard Brière 8 | Carl Youngblood 9 | Richard Huang 10 | Dan Mayer 11 | Benjamin Fleischer 12 | Robin Curry 13 | -------------------------------------------------------------------------------- /lib/metric_fu/version.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class Version 3 | MAJOR = "4" 4 | MINOR = "13" 5 | PATCH = "0" 6 | PRE = "" 7 | end 8 | VERSION = [Version::MAJOR, Version::MINOR, Version::PATCH].join(".") 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/line_numbers/module_surrounds_class.rb: -------------------------------------------------------------------------------- 1 | module StuffModule 2 | 3 | class ThingClass 4 | 5 | def do_it 6 | "do it" 7 | end 8 | 9 | end 10 | 11 | def blah 12 | "blah blah" 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /bin/metric_fu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'metric_fu' 3 | require 'metric_fu/cli/client' 4 | 5 | cli = MetricFu::Cli::Client.new 6 | # see http://obtiva.com/blog/185-fun-with-ruby-it-s-a-trap 7 | trap("INT") { cli.shutdown } 8 | 9 | cli.run 10 | -------------------------------------------------------------------------------- /lib/metric_fu/calculate.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | module Calculate 3 | module_function 4 | 5 | def integer_percent(num, total) 6 | return 0 if total.zero? 7 | (Float(num) / Float(total) * 100).round 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/mf-cane: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'cane', 7 | metric_name: 'cane', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /bin/mf-flay: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'flay', 7 | metric_name: 'flay', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /bin/mf-reek: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'reek', 7 | metric_name: 'reek', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /bin/mf-churn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'churn', 7 | metric_name: 'churn', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /bin/mf-roodi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'roodi', 7 | metric_name: 'roodi', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /spec/support/timeout.rb: -------------------------------------------------------------------------------- 1 | require "timeout" 2 | # No spec should run longer than this 3 | # (define here to support the closure) 4 | p "setting timeout #{timeout = 15.0} seconds" 5 | RSpec.configuration.around(:each) do |example| 6 | Timeout.timeout(timeout, &example) 7 | end 8 | -------------------------------------------------------------------------------- /bin/mf-saikuro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | 4 | require 'metric_fu/gem_run' 5 | options = { 6 | gem_name: 'metric_fu-Saikuro', 7 | metric_name: 'saikuro', 8 | args: ARGV.dup, 9 | } 10 | STDOUT.puts MetricFu::GemRun.new(options).run.output 11 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/index.html.erb: -------------------------------------------------------------------------------- 1 |

<%= MetricFu.metric_name %> Results

2 | 9 | 10 | <%= render_partial 'report_footer' %> 11 | -------------------------------------------------------------------------------- /spec/fixtures/coverage-153.rb: -------------------------------------------------------------------------------- 1 | # from https://github.com/metricfu/metric_fu/issues/153 2 | class A 3 | def m(arg1) 4 | p "this is my method" # Assume that none of the line covered in this method 5 | if arg1 > 5 6 | p "more than 5" 7 | else 8 | p "not more than 5" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - config/rubocop.yml 3 | - .rubocop_todo.yml 4 | 5 | # Files you want to exclude 6 | AllCops: 7 | Exclude: 8 | - bin/* 9 | - gem_tasks/* 10 | - lib/tasks/* 11 | - Gemfile 12 | - Rakefile 13 | - bundle/**/* 14 | - vendor/bundle/**/* 15 | - spec/fixtures/line_numbers/* 16 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/bluff.css: -------------------------------------------------------------------------------- 1 | .bluff-tooltip { 2 | background: #fff; 3 | border: 1px solid #d1edf5; 4 | padding: 8px 8px 6px; 5 | } 6 | .bluff-tooltip .color { 7 | display: block; 8 | height: 4px; 9 | width: 30px; 10 | margin: 0 0 4px; 11 | overflow: hidden; 12 | } 13 | .bluff-tooltip .data { 14 | font-weight: bold; 15 | } -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/groupings.rb: -------------------------------------------------------------------------------- 1 | MetricFu.metrics_require { "hotspots/analysis/grouping" } 2 | module MetricFu 3 | class HotspotGroupings 4 | def initialize(table, opts) 5 | @table, @opts = table, opts 6 | end 7 | 8 | def get_grouping 9 | MetricFu::Grouping.new(@table, @opts) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /gem_tasks/rubocop.rake: -------------------------------------------------------------------------------- 1 | if ENV["FULL_BUILD"] != "true" # skip on Travis 2 | require "rubocop/rake_task" 3 | RuboCop::RakeTask.new(:rubocop) do |task| 4 | task.patterns = ["lib", "spec"] 5 | task.formatters = ["progress"] 6 | task.options = ["--display-cop-names"] 7 | task.fail_on_error = false 8 | task.verbose = false 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/javascripts/utils.js: -------------------------------------------------------------------------------- 1 | function createGraphElement(elementType) { 2 | var graphContainer = document.getElementById("graph_container"); 3 | 4 | if(graphContainer) { 5 | var graphElement = document.createElement(elementType); 6 | graphElement.setAttribute("id", "graph"); 7 | graphContainer.appendChild(graphElement); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .rvmrc* 3 | *.gem 4 | *.rbc 5 | .bundle 6 | .config 7 | .yardoc 8 | Gemfile.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | *.swp 21 | tags 22 | .idea 23 | 24 | # rvm 25 | .ruby-version 26 | .ruby-gemset 27 | 28 | .overcommit.yml -------------------------------------------------------------------------------- /spec/fixtures/saikuro_sfiles/thing.rb_cyclo.html: -------------------------------------------------------------------------------- 1 | -- START Mod -- 2 | Type:Module Name:Mod Complexity:2 Lines:7 3 | Type:Def Name:self.included Complexity:2 Lines:5 4 | -- START -- 5 | Type:Class Name: Complexity:0 Lines:1 6 | -- END -- 7 | Type:Def Name:update Complexity:1 Lines:1 8 | -- END Mod -- 9 | -- START Thing -- 10 | Type:Class Name:Thing Complexity:0 Lines:2 11 | -- END Thing -- 12 | -------------------------------------------------------------------------------- /spec/metric_fu/loader_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MetricFu do 4 | it "loads the .metrics file" do 5 | # Global only for testing that this file gets loaded 6 | $metric_file_loaded = false 7 | MetricFu.with_run_dir "spec/support" do 8 | MetricFu.loader.load_user_configuration 9 | end 10 | 11 | expect($metric_file_loaded).to be_truthy 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/hotspots/hotspot_analyzer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/hotspot_analyzer" } 3 | 4 | describe MetricFu::HotspotAnalyzer do 5 | it "should have its own tests regarding how it orchestrates the analysis of results, rankings, tables, and analyzed_problems" 6 | 7 | it "#hotspots aka worst_items" 8 | 9 | it "#analyzed_problems" 10 | end 11 | -------------------------------------------------------------------------------- /spec/metric_fu/templates/metrics_template_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MetricFu::Templates::MetricsTemplate do 4 | let(:template) { Templates::MetricsTemplate.new } 5 | 6 | describe "#html_filename" do 7 | it "returns the hashed filename ending with .html" do 8 | expect(template.html_filename("some_file.rb")).to eq("10580a1fcbe74a931db8210462a584.html") 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricHotspots < Metric 3 | def name 4 | :hotspots 5 | end 6 | 7 | # TODO remove explicit Churn dependency 8 | def default_run_options 9 | { start_date: "1 year ago", minimum_churn_count: 10 } 10 | end 11 | 12 | def has_graph? 13 | false 14 | end 15 | 16 | def enable 17 | super 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/line_numbers/foo.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | 3 | def self.awesome 4 | 1 + 1 5 | end 6 | 7 | def what 8 | 12 9 | end 10 | 11 | def hello 12 | puts "Hi There" 13 | end 14 | 15 | 16 | def awesome 17 | 3+4 18 | end 19 | 20 | class << self 21 | def neat 22 | "23 skido" 23 | end 24 | end 25 | 26 | private 27 | 28 | def whoop 29 | "w00t" 30 | end 31 | end 32 | 33 | #comment -------------------------------------------------------------------------------- /lib/metric_fu/formatter/yaml.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | module Formatter 3 | class YAML 4 | include MetricFu::Io 5 | 6 | DEFAULT_PATH = "report.yml" 7 | 8 | def initialize(opts = {}) 9 | @options = opts 10 | @path_or_io = @options[:output] || DEFAULT_PATH 11 | end 12 | 13 | def finish 14 | write_output(MetricFu.result.as_yaml, @path_or_io) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/saikuro/app/controllers/sessions_controller.rb_cyclo.html: -------------------------------------------------------------------------------- 1 | -- START -- 2 | Type:Global Name: Complexity:0 Lines:1 3 | -- END -- 4 | -- START SessionsController -- 5 | Type:Class Name:SessionsController Complexity:6 Lines:40 6 | Type:Def Name:new Complexity:2 Lines:2 7 | Type:Def Name:create Complexity:2 Lines:19 8 | Type:Def Name:destroy Complexity:1 Lines:4 9 | Type:Def Name:note_failed_signin Complexity:1 Lines:3 10 | -- END SessionsController -- 11 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :reek: 3 | :matches: 4 | - :file_path: lib/client/client.rb 5 | :code_smells: 6 | - :type: Large Class 7 | :message: has at least 27 methods 8 | :method: Devver::Client 9 | - :type: Long Method 10 | :message: has approx 6 statements 11 | :method: Devver::Client#client_requested_sync 12 | - :type: Large Class 13 | :message: has at least 20 methods 14 | :method: Devver::Foo 15 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/flay/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for flay" do 5 | it_behaves_like "configured" do 6 | it "should set @flay to {:dirs_to_flay => @code_dirs}" do 7 | load_metric "flay" 8 | expect(MetricFu::Metric.get_metric(:flay).run_options).to eq( 9 | dirs_to_flay: ["lib"], minimum_score: nil 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/reek/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for reek" do 5 | it_behaves_like "configured" do 6 | it "should set @reek to {:dirs_to_reek => @code_dirs}" do 7 | load_metric "reek" 8 | expect(MetricFu::Metric.get_metric(:reek).run_options).to eq( 9 | config_file_pattern: nil, dirs_to_reek: ["lib"] 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --path vendor/bundle --jobs=3 --retry=3 3 | script: bundle exec rspec 4 | cache: bundler 5 | 6 | before_install: 7 | - gem install bundler 8 | - bundle config --local without local_development yard guard 9 | rvm: 10 | # 2.1, not 2.1.0 until fixed https://github.com/travis-ci/travis-ci/issues/2220 11 | - 2.5 12 | - 2.6 13 | - 2.7 14 | - 3.0 15 | - jruby 16 | matrix: 17 | allow_failures: 18 | - rvm: jruby 19 | fast_finish: true 20 | -------------------------------------------------------------------------------- /spec/metric_fu/utility_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "metric_fu/utility" 3 | 4 | describe MetricFu::Utility do 5 | it "strips ANSI escape codes from text" do 6 | text = "\e[31m./app/models/account.rb:64 - Found = in conditional. It should probably be an ==\e[0m" 7 | output = "./app/models/account.rb:64 - Found = in conditional. It should probably be an ==" 8 | 9 | result = MetricFu::Utility.strip_escape_codes(text) 10 | expect(result).to eq(output) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/reek/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricReek < Metric 3 | def name 4 | :reek 5 | end 6 | 7 | def default_run_options 8 | { 9 | dirs_to_reek: MetricFu::Io::FileSystem.directory("code_dirs"), 10 | config_file_pattern: nil, 11 | } 12 | end 13 | 14 | def has_graph? 15 | true 16 | end 17 | 18 | def enable 19 | super 20 | end 21 | 22 | def activate 23 | super 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/javascripts/bluff_graph.js: -------------------------------------------------------------------------------- 1 | createGraphElement("canvas"); 2 | 3 | var chart = new Bluff.Line("graph", "1000x600"); 4 | chart.theme_37signals(); 5 | chart.tooltips = true; 6 | chart.title_font_size = "24px"; 7 | chart.legend_font_size = "12px"; 8 | chart.marker_font_size = "10px"; 9 | chart.title = graph_title; 10 | for(var i = 0; i < graph_series.length; i++) { 11 | var serie = graph_series[i]; 12 | chart.data(serie.name, serie.data); 13 | } 14 | chart.labels = graph_labels; 15 | chart.draw(); 16 | -------------------------------------------------------------------------------- /spec/metric_fu/gem_version_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.lib_require { "gem_version" } 3 | 4 | describe MetricFu::GemVersion do 5 | it "has a list of gem deps" do 6 | gem_version = MetricFu::GemVersion.new 7 | gem_deps = gem_version.gem_runtime_dependencies.map(&:name) 8 | MetricFu::Metric.metrics.reject { |metric| metric.name == :hotspots || metric.name == :stats }.map(&:name).map(&:to_s).each do |metric_name| 9 | expect(gem_deps).to include(metric_name) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/churn/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for churn" do 5 | it_behaves_like "configured" do 6 | it "should set @churn to {}" do 7 | load_metric "churn" 8 | expect(MetricFu::Metric.get_metric(:churn).run_options).to eq( 9 | start_date: '"1 year ago"', minimum_churn_count: 10, ignore_files: [], data_directory: MetricFu::Io::FileSystem.scratch_directory("churn") 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/roodi/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricRoodi < Metric 3 | def name 4 | :roodi 5 | end 6 | 7 | def default_run_options 8 | { dirs_to_roodi: MetricFu::Io::FileSystem.directory("code_dirs"), 9 | roodi_config: "#{MetricFu::Io::FileSystem.directory('root_directory')}/config/roodi_config.yml" } 10 | end 11 | 12 | def has_graph? 13 | true 14 | end 15 | 16 | def enable 17 | super 18 | end 19 | 20 | def activate 21 | super 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/roodi.yml: -------------------------------------------------------------------------------- 1 | :roodi: 2 | :total: 3 | - Found 164 errors. 4 | :problems: 5 | - :line: "158" 6 | :file: lib/client/client.rb 7 | :problem: Method name "process" cyclomatic complexity is 10. It should be 8 or less. 8 | - :line: "232" 9 | :file: lib/client/client.rb 10 | :problem: Method name "process_ready" cyclomatic complexity is 15. It should be 8 or less. 11 | - :line: "288" 12 | :file: lib/client/foobar.rb 13 | :problem: Method name "send_tests" cyclomatic complexity is 10. It should be 8 or less. 14 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/roodi/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for roodi" do 5 | it_behaves_like "configured" do 6 | it "should set @roodi to {:dirs_to_roodi => @code_dirs}" do 7 | load_metric "roodi" 8 | expect(MetricFu::Metric.get_metric(:roodi).run_options).to eq( 9 | dirs_to_roodi: directory("code_dirs"), 10 | roodi_config: "#{directory('root_directory')}/config/roodi_config.yml" 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/hotspots/hotspot_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/hotspot" } 3 | 4 | describe MetricFu::Hotspot do 5 | before do 6 | enable_hotspots 7 | end 8 | 9 | it "returns an array of of the analyzers that subclass it" do 10 | expected_analyzers = [ReekHotspot, RoodiHotspot, 11 | FlogHotspot, ChurnHotspot, SaikuroHotspot, 12 | FlayHotspot, StatsHotspot, RcovHotspot] 13 | 14 | expect(MetricFu::Hotspot.analyzers.size).to eq(expected_analyzers.size) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/flog/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for flog" do 5 | it_behaves_like "configured" do 6 | if MetricFu.configuration.mri? 7 | it "should set @flog to {:dirs_to_flog => @code_dirs}" do 8 | load_metric "flog" 9 | expect(MetricFu::Metric.get_metric(:flog).run_options).to eq( 10 | all: true, 11 | continue: true, 12 | dirs_to_flog: ["lib"], 13 | quiet: true 14 | ) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/rcov.css: -------------------------------------------------------------------------------- 1 | .rcov_code td { 2 | border-bottom: 1px solid #ddd ; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | .rcov_code tr { 7 | border: 0px; 8 | padding:0px; 9 | margin: 0px; 10 | } 11 | .rcov_code pre { 12 | border: 0px; 13 | padding: 0px; 14 | margin: 0px; 15 | } 16 | .rcov_run {} 17 | .rcov_not_run { 18 | background-color: #d88; 19 | } 20 | .rcov_run a, .rcov_not_run a { 21 | text-decoration: none; 22 | } 23 | .rcov_run a { 24 | color: #333; 25 | } 26 | .rcov_not_run a { 27 | color: #000; 28 | } 29 | .rcov_overflow { 30 | overflow: auto; 31 | font-size: 50%; 32 | } 33 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/external_client.rb: -------------------------------------------------------------------------------- 1 | # Reads and writes external coverage files as BINARY 2 | module MetricFu 3 | class RCovTestCoverageClient 4 | def initialize(coverage_file) 5 | @file_path = Pathname(coverage_file) 6 | @file_path.dirname.mkpath 7 | end 8 | 9 | def post_results(payload) 10 | mf_log "Saving coverage payload to #{@file_path}" 11 | dump(payload) 12 | end 13 | 14 | def load 15 | File.binread(@file_path) 16 | end 17 | 18 | def dump(payload) 19 | File.open(@file_path, "wb") { |file| file.write(payload) } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /gem_tasks/yard.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require "yard" 5 | rescue LoadError 6 | else 7 | namespace :yard do 8 | YARD::Rake::YardocTask.new(:doc) do |t| 9 | t.stats_options = ["--list-undoc"] 10 | end 11 | 12 | desc "start a gem server" 13 | task :server do 14 | sh "bundle exec yard server --gems" 15 | end 16 | 17 | desc "use Graphviz to generate dot graph" 18 | task :graph do 19 | output_file = "doc/erd.dot" 20 | sh "bundle exec yard graph --protected --full --dependencies > #{output_file}" 21 | puts "open doc/erd.dot if you have graphviz installed" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/churn/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricChurn < Metric 3 | def name 4 | :churn 5 | end 6 | 7 | def default_run_options 8 | { 9 | start_date: '"1 year ago"', 10 | minimum_churn_count: 10, 11 | ignore_files: [], 12 | data_directory: MetricFu::Io::FileSystem.scratch_directory(name) 13 | } 14 | end 15 | 16 | def has_graph? 17 | false 18 | end 19 | 20 | def enable 21 | super 22 | end 23 | 24 | def activate 25 | activate_library("churn/calculator") 26 | super 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['2.5', '2.6', '2.7', '3.0'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 24 | - name: Run tests 25 | run: bundle exec rake 26 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/problems.rb: -------------------------------------------------------------------------------- 1 | MetricFu.metrics_require { "hotspots/analysis/groupings" } 2 | module MetricFu 3 | class HotspotProblems 4 | def initialize(sub_table) 5 | @grouping = group_by(sub_table, "metric") 6 | end 7 | 8 | def problems 9 | problems = {} 10 | @grouping.each do |metric, table| 11 | problems[metric] = MetricFu::Hotspot.analyzer_for_metric(metric).present_group(table) 12 | end 13 | problems 14 | end 15 | 16 | def group_by(sub_table, by = "metric") 17 | MetricFu::HotspotGroupings.new(sub_table, by: by).get_grouping 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/scoring_strategies.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | module HotspotScoringStrategies 3 | module_function 4 | 5 | # per project score percentile 6 | def percentile(ranking, item) 7 | ranking.percentile(item) 8 | end 9 | 10 | # Use the score you got 11 | # (ex flog score of 20 is not bad even if it is the top one in project) 12 | def identity(ranking, item) 13 | ranking.fetch(item) 14 | end 15 | 16 | def sum(scores) 17 | scores.inject(&:+) 18 | end 19 | 20 | def average(scores) 21 | sum(scores).to_f / scores.size.to_f 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/roodi_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | CaseMissingElseCheck: {} 3 | ClassLineCountCheck: 4 | line_count: 300 5 | ClassNameCheck: 6 | pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ 7 | CyclomaticComplexityBlockCheck: 8 | complexity: 4 9 | CyclomaticComplexityMethodCheck: 10 | complexity: 8 11 | EmptyRescueBodyCheck: {} 12 | ForLoopCheck: {} 13 | MethodLineCountCheck: 14 | line_count: 20 15 | MethodNameCheck: 16 | pattern: !ruby/regexp /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ 17 | ModuleLineCountCheck: 18 | line_count: 300 19 | ModuleNameCheck: 20 | pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ 21 | ParameterNumberCheck: 22 | parameter_count: 5 23 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/grouping.rb: -------------------------------------------------------------------------------- 1 | %w(table).each do |path| 2 | MetricFu.metrics_require { "hotspots/analysis/#{path}" } 3 | end 4 | module MetricFu 5 | class Grouping 6 | def initialize(table, opts) 7 | column_name = opts.fetch(:by) 8 | hash = {} 9 | if column_name.to_sym == :metric # special optimized case 10 | hash = table.group_by_metric 11 | else 12 | raise "Unexpected column_name #{column_name}" 13 | end 14 | @arr = hash.to_a 15 | end 16 | 17 | def each 18 | @arr.each do |value, rows| 19 | yield value, rows 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/metric_fu/cli/client.rb: -------------------------------------------------------------------------------- 1 | require "metric_fu" 2 | require "metric_fu/cli/helper" 3 | require "metric_fu/cli/parser" 4 | module MetricFu 5 | module Cli 6 | class Client 7 | def initialize 8 | @helper = MetricFu::Cli::Helper.new 9 | end 10 | 11 | def shutdown 12 | @helper.shutdown 13 | end 14 | 15 | def run(argv = ARGV.dup) 16 | options = @helper.process_options(argv) 17 | mf_debug "Got options #{options.inspect}" 18 | if options[:run] 19 | @helper.run(options) 20 | else 21 | STDOUT.puts @helper.usage 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flay/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricFlay < Metric 3 | def name 4 | :flay 5 | end 6 | 7 | def default_run_options 8 | { dirs_to_flay: MetricFu::Io::FileSystem.directory("code_dirs"), 9 | # MetricFu has been setting the minimum score as 100 for 10 | # a long time. This is a really big number, considering 11 | # the default is 16. Setting it to nil to use the Flay default. 12 | minimum_score: nil, 13 | } 14 | end 15 | 16 | def has_graph? 17 | true 18 | end 19 | 20 | def enable 21 | super 22 | end 23 | 24 | def activate 25 | super 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/saikuro/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricSaikuro < Metric 3 | def name 4 | :saikuro 5 | end 6 | 7 | def default_run_options 8 | { 9 | output_directory: MetricFu::Io::FileSystem.scratch_directory(name), 10 | input_directory: MetricFu::Io::FileSystem.directory("code_dirs"), 11 | cyclo: "", 12 | filter_cyclo: "0", 13 | warn_cyclo: "5", 14 | error_cyclo: "7", 15 | formater: "text", 16 | } 17 | end 18 | 19 | def has_graph? 20 | false 21 | end 22 | 23 | def enable 24 | super 25 | end 26 | 27 | def activate 28 | super 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flog/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricFlog < Metric 3 | def name 4 | :flog 5 | end 6 | 7 | def default_run_options 8 | { dirs_to_flog: MetricFu::Io::FileSystem.directory("code_dirs"), continue: true, all: true, quiet: true } 9 | end 10 | 11 | def has_graph? 12 | true 13 | end 14 | 15 | def enable 16 | if MetricFu.configuration.mri? 17 | super 18 | else 19 | MetricFu.logger.debug("Flog is only available in MRI due to flog tasks") 20 | end 21 | end 22 | 23 | def activate 24 | activate_library "flog" 25 | activate_library "flog_cli" 26 | super 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/stats/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::StatsHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{stat_name stat_value} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :stats 10 | end 11 | 12 | def map_strategy 13 | :absent 14 | end 15 | 16 | def reduce_strategy 17 | :absent 18 | end 19 | 20 | def score_strategy 21 | :absent 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | data.each do |key, value| 27 | next if value.is_a?(Array) 28 | table << { 29 | "metric" => name, 30 | "stat_name" => key, 31 | "stat_value" => value 32 | } 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rcov/hotspot_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/metric" } 3 | MetricFu.metrics_require { "hotspots/hotspot" } 4 | MetricFu.metrics_require { "hotspots/analysis/record" } 5 | MetricFu.metrics_require { "rcov/hotspot" } 6 | 7 | describe MetricFu::RcovHotspot do 8 | describe "map" do 9 | let(:zero_row) do 10 | MetricFu::Record.new({ "percentage_uncovered" => 0.0 }, nil) 11 | end 12 | 13 | let(:non_zero_row) do 14 | MetricFu::Record.new({ "percentage_uncovered" => 0.75 }, nil) 15 | end 16 | 17 | it { expect(subject.map(zero_row)).to eql(0.0) } 18 | it { expect(subject.map(non_zero_row)).to eql(0.75) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/record.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class Record 3 | attr_reader :data 4 | 5 | def initialize(data, _columns) 6 | @data = data 7 | end 8 | 9 | def method_missing(name, *args, &block) 10 | key = name.to_s 11 | if key == "fetch" 12 | @data.send(name, *args, &block) 13 | elsif @data.has_key?(key) 14 | @data[key] 15 | else 16 | super(name, *args, &block) 17 | end 18 | end 19 | 20 | def []=(key, value) 21 | @data[key] = value 22 | end 23 | 24 | def [](key) 25 | @data[key] 26 | end 27 | 28 | def has_key?(key) 29 | @data.has_key?(key) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/metric_fu/templates/report_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.lib_require { "templates/report" } 3 | 4 | describe MetricFu::Templates::Report do 5 | # TODO: This test only shows how the code works and that it doesn't blow up. 6 | # Perhaps it should test something more specific? 7 | it "Reads in a source file, and produces an annotated HTML report" do 8 | lines = { "2" => [{ type: :reek, description: "Bad Param Names" }] } 9 | source_file = File.join(MetricFu.root_dir, "spec", "dummy", "lib", "bad_encoding.rb") 10 | report = MetricFu::Templates::Report.new(source_file, lines) 11 | expect { 12 | rendered_report = report.render 13 | }.not_to raise_error 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /gem_tasks/usage_test.rake: -------------------------------------------------------------------------------- 1 | ROOT_PATH = File.expand_path("..", File.dirname(__FILE__)) 2 | require File.join(ROOT_PATH, 'spec', 'support', 'usage_test') 3 | LIB_PATH = File.join(ROOT_PATH, 'lib') 4 | BIN_PATH = File.join(ROOT_PATH, 'bin') 5 | EXAMPLE_FILES = [ 6 | File.join(ROOT_PATH, 'README.md'), 7 | File.join(ROOT_PATH, 'DEV.md') 8 | ] 9 | task "load_path" do 10 | $LOAD_PATH.unshift(LIB_PATH) 11 | $VERBOSE = nil 12 | ENV['PATH'] = "#{BIN_PATH}:#{ENV['PATH']}" 13 | ENV['CC_BUILD_ARTIFACTS'] = 'turn_off_browser_opening' 14 | end 15 | desc "Test that documentation usage works" 16 | task "usage_test" => %w[load_path] do 17 | usage_test = UsageTest.new 18 | usage_test.test_files(EXAMPLE_FILES) 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/suite.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | def directory(name) 3 | MetricFu::Io::FileSystem.directory(name) 4 | end 5 | 6 | def scratch_directory(name) 7 | File.join(MetricFu::Io::FileSystem.artifact_dir, "scratch", name) 8 | end 9 | 10 | def artifact_test_dir 11 | File.join(MetricFu::APP_ROOT, "tmp", "metric_fu", "test") 12 | end 13 | 14 | # Let's shift the output directories so that we don't interfere with 15 | # existing historical metric data. 16 | MetricFu::Io::FileSystem.artifact_dir = artifact_test_dir 17 | MetricFu::Io::FileSystem.set_directories 18 | 19 | def setup_fs 20 | cleanup_fs 21 | MetricFu::Io::FileSystem.set_directories 22 | end 23 | 24 | def cleanup_fs 25 | FileUtils.rm_rf(artifact_test_dir) 26 | end 27 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: 3 | - Visual Studio 2019 4 | 5 | skip_tags: true 6 | 7 | environment: 8 | matrix: 9 | - RUBY_VERSION: 25 10 | - RUBY_VERSION: 25-x64 11 | - RUBY_VERSION: 26 12 | - RUBY_VERSION: 26-x64 13 | - RUBY_VERSION: 27 14 | - RUBY_VERSION: 27-x64 15 | 16 | install: 17 | - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH% 18 | - gem install bundler 19 | - bundle config force_ruby_platform true 20 | - bundle install --path=vendor/bundle --retry=3 --jobs=3 21 | 22 | cache: 23 | - vendor/bundle 24 | 25 | build: off 26 | 27 | before_test: 28 | - ruby -v 29 | - gem -v 30 | - bundle -v 31 | 32 | test_script: 33 | - bundle exec rspec 34 | - bundle exec ruby -Ilib bin/metric_fu --no-open 35 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/saikuro.yml: -------------------------------------------------------------------------------- 1 | :saikuro: 2 | :files: 3 | - :classes: 4 | - :complexity: 0 5 | :methods: [] 6 | :lines: 3 7 | :class_name: Shorty 8 | - :complexity: 19 9 | :methods: 10 | - :complexity: 9 11 | :lines: 6 12 | :name: Shorty::Supr#self.handle_full_or_hash_option 13 | - :complexity: 1 14 | :lines: 9 15 | :name: Shorty::Supr#initialize 16 | :lines: 92 17 | :class_name: Shorty::Supr 18 | :filename: supr.rb 19 | - :classes: 20 | - :complexity: 12 21 | :methods: 22 | - :complexity: 8 23 | :lines: 10 24 | :name: Shorty::Bitly#info 25 | :lines: 104 26 | :class_name: Shorty::Bitly 27 | :filename: bitly.rb 28 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rails_best_practices/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricRailsBestPractices < Metric 3 | def name 4 | :rails_best_practices 5 | end 6 | 7 | def default_run_options 8 | { 9 | silent: true, 10 | exclude: [] 11 | } 12 | end 13 | 14 | def has_graph? 15 | true 16 | end 17 | 18 | def enable 19 | if MetricFu.configuration.supports_ripper? 20 | super if MetricFu.configuration.rails? 21 | else 22 | MetricFu.logger.debug("Rails Best Practices is only available in MRI 1.9. It requires ripper") 23 | end 24 | end 25 | 26 | def activate 27 | activate_library("rails_best_practices") 28 | super 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/roodi/report.html.erb: -------------------------------------------------------------------------------- 1 |

Roodi Results

2 | 3 |

Roodi parses your Ruby code and warns you about design issues you have based on the checks that is has configured.

4 | 5 | <%= render_partial 'graph', {:graph_name => 'roodi'} %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | <% count = 0 %> 13 | <% @roodi[:problems].each do |problem| %> 14 | 15 | 16 | 17 | 18 | <% count += 1 %> 19 | <% end %> 20 |
File PathWarning
<%= link_to_filename(problem[:file], problem[:line]) %><%= problem[:problem] %>
21 | 22 | <%= render_partial 'report_footer' %> 23 | -------------------------------------------------------------------------------- /etc/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | `git clone https://gist.github.com/7209165.git scripts` 4 | 5 | Generate list of contributors in descending number of code changes 6 | 7 | ```sh 8 | ruby scripts/git-rank-contributors.rb > CONTRIBUTORS 9 | ``` 10 | 11 | Generate entity diagram (gotta ensure all the code runs) 12 | 13 | ```sh 14 | ruby -Ilib -rmetric_fu -rmetric_fu/cli/client -rmetric_fu/metrics/rcov/simplecov_formatter -e "cli = MetricFu::Cli::Client.new; begin; MetricFu.configuration.configure_metric(:rcov){|rcov| rcov.coverage_file = MetricFu.run_path.join('coverage/rcov/rcov.txt'); rcov.enable; rcov.activate}; cli.run; rescue SystemExit; end; ARGV.clear; ARGV.concat(%w[metric_fu MetricFu SimpleCov::Formatter::MetricFu]); load 'scripts/erd.rb'" && mv erd.* etc/ 15 | ``` 16 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/syntax.css: -------------------------------------------------------------------------------- 1 | table { background: #fff; color: #000; } 2 | .ruby .normal { color: #000; } 3 | .ruby .comment { color: #005; font-style: italic; } 4 | .ruby .keyword { color: #A44; font-weight: bold; } 5 | .ruby .method { color: #44f; } 6 | .ruby .class { color: #b1713d; } 7 | .ruby .module { color: #050; } 8 | .ruby .punct { color: #668; font-weight: bold; } 9 | .ruby .symbol { color: #00f; } 10 | .ruby .string { color: #4a4; } 11 | .ruby .char { color: #F07; } 12 | .ruby .ident { color: #000; } 13 | .ruby .constant { color: #b1713d; } 14 | .ruby .regex { color: #B66; background: #FEF; } 15 | .ruby .number { color: #F99; } 16 | .ruby .attribute { color: #f84; } 17 | .ruby .global { color: #7FB; } 18 | .ruby .expr { color: #227; } 19 | .ruby .escape { color: #277; } -------------------------------------------------------------------------------- /spec/metric_fu/calculate_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.lib_require { "calculate" } 3 | 4 | describe MetricFu::Calculate do 5 | describe "returns a percent rounded to the nearest integer" do 6 | specify "3 / 10 == 30" do 7 | expect(MetricFu::Calculate.integer_percent(3, 10)).to eq(30) 8 | end 9 | specify "3.0 / 10 == 30" do 10 | expect(MetricFu::Calculate.integer_percent(3.0, 10)).to eq(30) 11 | end 12 | it "raises an ArgumentError on non-numeric input" do 13 | expect { 14 | MetricFu::Calculate.integer_percent("", 10) 15 | }.to raise_error(ArgumentError) 16 | end 17 | it "returns 0 when the denominator is 0" do 18 | expect(MetricFu::Calculate.integer_percent(3, 0)).to eq(0) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/ranking.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | module MetricFu 3 | class Ranking 4 | extend Forwardable 5 | 6 | def initialize 7 | @items_to_score = {} 8 | end 9 | 10 | def top 11 | sorted_items 12 | end 13 | 14 | def percentile(item) 15 | index = sorted_items.index(item) 16 | worse_item_count = (length - (index + 1)) 17 | worse_item_count.to_f / length 18 | end 19 | 20 | def_delegator :@items_to_score, :has_key?, :scored? 21 | def_delegators :@items_to_score, :[], :[]=, :length, :each, :delete, :fetch 22 | 23 | private 24 | 25 | def sorted_items 26 | @sorted_items ||= @items_to_score.sort_by { |_item, score| -score }.map { |item, _score| item } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/default.css: -------------------------------------------------------------------------------- 1 | table { 2 | margin-top: 20px; 3 | border-collapse: collapse; 4 | border: 1px solid #666; 5 | background: #fff; 6 | margin-bottom: 20px; 7 | } 8 | 9 | table tr.light { 10 | background: #fff; 11 | } 12 | 13 | table tr.dark { 14 | background: #f9f9f9; 15 | } 16 | 17 | table tr:hover { 18 | background: #FFFFC0; 19 | } 20 | 21 | table td, table th { 22 | padding: 4px; 23 | font-size: 11px; 24 | } 25 | table th { 26 | text-align: center; 27 | color: #337022; 28 | background: #DDFFCC; 29 | font-weight: bold; 30 | border: #99D688 1px solid; 31 | } 32 | 33 | table td { 34 | border: #d0d0d0 1px solid; 35 | } 36 | 37 | table td.score { 38 | text-align: right; 39 | } 40 | 41 | .warning { 42 | background: yellow; 43 | } 44 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/cane/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for cane" do 5 | it_behaves_like "configured" do 6 | if MetricFu.configuration.mri? 7 | it "should set @cane to " + 8 | ':dirs_to_cane => @code_dirs, :abc_max => 15, :line_length => 80, :no_doc => "n", :no_readme => "y"' do 9 | load_metric "cane" 10 | expect(MetricFu::Metric.get_metric(:cane).run_options).to eq( 11 | 12 | dirs_to_cane: directory("code_dirs"), 13 | filetypes: ["rb"], 14 | abc_max: 15, 15 | line_length: 80, 16 | no_doc: "n", 17 | no_readme: "n" 18 | ) 19 | end 20 | end 21 | end # end it_behaves 22 | end 23 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/stats/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricStats < Metric 3 | def name 4 | :stats 5 | end 6 | 7 | def default_run_options 8 | { 9 | # returns a list of directories that contains the glob of files that have the file_pattern in the file names 10 | additional_test_directories: [{ glob_pattern: File.join(".", "spec", "**", "*_spec.rb"), file_pattern: "spec" }], 11 | additional_app_directories: [{ glob_pattern: File.join(".", "engines", "**", "*.rb"), file_pattern: "" }], 12 | } 13 | end 14 | 15 | def has_graph? 16 | true 17 | end 18 | 19 | def enable 20 | super 21 | end 22 | 23 | def activate 24 | activate_library "code_metrics" 25 | super 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rails_best_practices/report.html.erb: -------------------------------------------------------------------------------- 1 |

Rails Best Practices Results

2 | 3 |

rails_best_practices is a code metric tool for rails projects.

4 | 5 | <%= render_partial 'graph', {:graph_name => 'rails_best_practices'} %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | <% count = 0 %> 13 | <% @rails_best_practices[:problems].each do |problem| %> 14 | 15 | 16 | 17 | 18 | <% count += 1 %> 19 | <% end %> 20 |
File PathWarning
<%= link_to_filename(problem[:file], problem[:line]) %>><%= problem[:problem] %>
21 | 22 | <%= render_partial 'report_footer' %> 23 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/roodi/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::RoodiHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{problems} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :roodi 10 | end 11 | 12 | def map_strategy 13 | :present 14 | end 15 | 16 | def reduce_strategy 17 | :sum 18 | end 19 | 20 | def score_strategy 21 | :percentile 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | Array(data[:problems]).each do |problem| 27 | table << { 28 | "metric" => name, 29 | "problems" => problem[:problem], 30 | "file_path" => problem[:file] 31 | } 32 | end 33 | end 34 | 35 | def present_group(group) 36 | occurences = group.size 37 | "found #{occurences} design problems" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/fixtures/saikuro/app/controllers/users_controller.rb_cyclo.html: -------------------------------------------------------------------------------- 1 | -- START UsersController -- 2 | Type:Class Name:UsersController Complexity:25 Lines:111 3 | Type:Def Name:signup Complexity:1 Lines:3 4 | Type:Def Name:new Complexity:1 Lines:2 5 | Type:Def Name:edit Complexity:1 Lines:3 6 | Type:Def Name:update Complexity:2 Lines:8 7 | Type:Def Name:index Complexity:1 Lines:2 8 | Type:Def Name:create Complexity:4 Lines:15 9 | Type:Def Name:thank_you Complexity:2 Lines:10 10 | Type:Def Name:destroy Complexity:1 Lines:3 11 | Type:Def Name:add_primary_site Complexity:1 Lines:4 12 | Type:Def Name:users_have_changed Complexity:3 Lines:8 13 | Type:Def Name:after_create_page Complexity:2 Lines:6 14 | Type:Def Name:sanitize_params Complexity:3 Lines:3 15 | Type:Def Name:authorize_user Complexity:3 Lines:5 16 | -- END UsersController -- 17 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flay/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class FlayGrapher < Grapher 4 | attr_accessor :flay_score, :labels 5 | 6 | def self.metric 7 | :flay 8 | end 9 | 10 | def initialize 11 | super 12 | @flay_score = [] 13 | @labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:flay] 18 | @flay_score.push(metrics[:flay][:total_score].to_i) 19 | @labels.update(@labels.size => date) 20 | end 21 | end 22 | 23 | def title 24 | "Flay: duplication" 25 | end 26 | 27 | def data 28 | [ 29 | ["flay", @flay_score.join(",")] 30 | ] 31 | end 32 | 33 | def output_filename 34 | "flay.js" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/metric_fu/logging/mf_debugger.rb: -------------------------------------------------------------------------------- 1 | warn "MfDebugger if deprecated. Please use MetricFu.logger" 2 | MetricFu.lib_require { "logger" } 3 | module MfDebugger 4 | extend self 5 | class Logger 6 | def self.debug_on 7 | warn "MfDebugger if deprecated. Please use MetricFu.logger" 8 | MetricFu.logger.debug_on 9 | end 10 | def self.debug_on=(bool) 11 | warn "MfDebugger if deprecated. Please use MetricFu.logger" 12 | MetricFu.logger.level = bool ? "debug" : "info" 13 | end 14 | def self.log(msg, &_block) 15 | warn "MfDebugger if deprecated. Please use MetricFu.logger" 16 | MetricFu.logger.info msg 17 | end 18 | def self.debug(msg, &_block) 19 | warn "MfDebugger if deprecated. Please use MetricFu.logger" 20 | MetricFu.logger.debug msg 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/cane/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricCane < Metric 3 | def name 4 | :cane 5 | end 6 | 7 | def default_run_options 8 | { 9 | dirs_to_cane: MetricFu::Io::FileSystem.directory("code_dirs"), 10 | abc_max: 15, 11 | line_length: 80, 12 | no_doc: "n", 13 | no_readme: "n", 14 | filetypes: ["rb"] 15 | } 16 | end 17 | 18 | def has_graph? 19 | true 20 | end 21 | 22 | def enable 23 | if MetricFu.configuration.supports_ripper? && !MetricFu.configuration.ruby18? 24 | super 25 | else 26 | MetricFu.logger.debug("Cane is only available in MRI. It requires ripper and 1.9 hash syntax support") 27 | end 28 | end 29 | 30 | def activate 31 | super 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rcov/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for rcov" do 5 | it_behaves_like "configured" do 6 | it "should set rcov run_options" do 7 | load_metric "rcov" 8 | expect( 9 | MetricFu::Metric.get_metric(:rcov).run_options 10 | ).to eq( 11 | 12 | environment: "test", 13 | external: nil, 14 | test_files: Dir["{spec,test}/**/*_{spec,test}.rb"], 15 | rcov_opts: [ 16 | "--sort coverage", 17 | "--no-html", 18 | "--text-coverage", 19 | "--no-color", 20 | "--profile", 21 | "--exclude-only '.*'", 22 | '--include-file "\Aapp,\Alib"', 23 | "-Ispec" 24 | ], 25 | ) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/roodi/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class RoodiGrapher < Grapher 4 | attr_accessor :roodi_count, :labels 5 | 6 | def self.metric 7 | :roodi 8 | end 9 | 10 | def initialize 11 | super 12 | @roodi_count = [] 13 | @labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:roodi] 18 | @roodi_count.push(metrics[:roodi][:problems].size) 19 | @labels.update(@labels.size => date) 20 | end 21 | end 22 | 23 | def title 24 | "Roodi: design problems" 25 | end 26 | 27 | def data 28 | [ 29 | ["roodi", @roodi_count.join(",")] 30 | ] 31 | end 32 | 33 | def output_filename 34 | "roodi.js" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class RcovGrapher < Grapher 4 | attr_accessor :rcov_percent, :labels 5 | 6 | def self.metric 7 | :rcov 8 | end 9 | 10 | def initialize 11 | super 12 | self.rcov_percent = [] 13 | self.labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:rcov] 18 | rcov_percent.push(metrics[:rcov][:global_percent_run]) 19 | labels.update(labels.size => date) 20 | end 21 | end 22 | 23 | def title 24 | "Rcov: code coverage" 25 | end 26 | 27 | def data 28 | [ 29 | ["rcov", @rcov_percent.join(",")] 30 | ] 31 | end 32 | 33 | def output_filename 34 | "rcov.js" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/metric_fu/reporter.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class Reporter 3 | def initialize(formatters = nil) 4 | @formatters = Array(formatters) 5 | end 6 | 7 | def start 8 | notify :start 9 | end 10 | 11 | def finish 12 | notify :finish 13 | end 14 | 15 | def start_metric(metric) 16 | mf_log "** STARTING METRIC #{metric}" 17 | notify :start_metric, metric 18 | end 19 | 20 | def finish_metric(metric) 21 | mf_log "** ENDING METRIC #{metric}" 22 | notify :finish_metric, metric 23 | end 24 | 25 | def display_results 26 | notify :display_results 27 | end 28 | 29 | protected 30 | 31 | def notify(event, *args) 32 | @formatters.each do |formatter| 33 | formatter.send(event, *args) if formatter.respond_to?(event) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | using_git = File.exist?(File.expand_path('../.git/', __FILE__)) 3 | if using_git 4 | require 'bundler/setup' 5 | end 6 | require 'rake' 7 | require 'simplecov' 8 | 9 | Dir['./gem_tasks/*.rake'].each do |task| 10 | import(task) 11 | end 12 | 13 | require 'rspec/core/rake_task' 14 | desc "Run all specs in spec directory" 15 | RSpec::Core::RakeTask.new(:spec) do |t| 16 | t.verbose = false 17 | 18 | t.pattern = "spec/**/*_spec.rb" 19 | # we require spec_helper so we don't get an RSpec warning about 20 | # examples being defined before configuration. 21 | t.ruby_opts = "-I./spec -r./spec/capture_warnings -rspec_helper" 22 | t.rspec_opts = %w[--format progress] if (ENV['FULL_BUILD'] || !using_git) 23 | end 24 | 25 | require File.expand_path File.join(File.dirname(__FILE__),'lib/metric_fu') 26 | 27 | task :default => :spec 28 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/cane/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class CaneGrapher < Grapher 4 | attr_accessor :cane_violations, :labels 5 | 6 | def self.metric 7 | :cane 8 | end 9 | 10 | def initialize 11 | super 12 | @cane_violations = [] 13 | @labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:cane] 18 | @cane_violations.push(metrics[:cane][:total_violations].to_i) 19 | @labels.update(@labels.size => date) 20 | end 21 | end 22 | 23 | def title 24 | "Cane: code quality threshold violations" 25 | end 26 | 27 | def data 28 | [ 29 | ["cane", @cane_violations.join(",")] 30 | ] 31 | end 32 | 33 | def output_filename 34 | "cane.js" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/support/deferred_garbaged_collection.rb: -------------------------------------------------------------------------------- 1 | class DeferredGarbageCollection 2 | DEFERRED_GC_THRESHOLD = (ENV["DEFER_GC"] || 15.0).to_f 3 | 4 | @@last_gc_run = Time.now 5 | 6 | def self.start 7 | GC.disable if DEFERRED_GC_THRESHOLD > 0 8 | end 9 | 10 | def self.reconsider 11 | if DEFERRED_GC_THRESHOLD > 0 && Time.now - @@last_gc_run >= DEFERRED_GC_THRESHOLD 12 | GC.enable 13 | GC.start 14 | GC.disable 15 | @@last_gc_run = Time.now 16 | end 17 | end 18 | 19 | def self.configure(config) 20 | return if defined?(JRUBY_VERSION) 21 | config.before(:all) do 22 | DeferredGarbageCollection.start 23 | end 24 | 25 | config.after(:all) do 26 | DeferredGarbageCollection.reconsider 27 | end 28 | end 29 | end 30 | 31 | RSpec.configure do |config| 32 | DeferredGarbageCollection.configure(config) 33 | end 34 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/javascripts/highcharts_graph.js: -------------------------------------------------------------------------------- 1 | createGraphElement("div"); 2 | 3 | if(document.getElementById('graph')) { 4 | var chart = new Highcharts.Chart({ 5 | chart: { 6 | animation: false, 7 | renderTo: 'graph' 8 | }, 9 | legend: { 10 | align: 'center', 11 | verticalAlign: 'top', 12 | y: 25 13 | }, 14 | plotOptions: { 15 | line: { 16 | animation: false, 17 | lineWidth: 3, 18 | marker: { 19 | radius: 6 20 | }, 21 | pointPlacement: 'on' 22 | } 23 | }, 24 | title: { 25 | text: graph_title 26 | }, 27 | xAxis: { 28 | categories: graph_labels, 29 | tickmarkPlacement: 'on' 30 | }, 31 | yAxis: { 32 | maxPadding: 0, 33 | min: 0, 34 | minPadding: 0 35 | }, 36 | series: graph_series 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.5.2 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;font-variant:normal;}sup {vertical-align:text-top;}sub {vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} 8 | -------------------------------------------------------------------------------- /spec/support/test_fixtures.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | class TestFixtures 3 | attr_reader :fixtures_path 4 | 5 | def initialize 6 | @loaded_data = {} 7 | @fixtures_path = Pathname(MetricFu.root_dir).join("spec", "fixtures") 8 | end 9 | 10 | def load_metric(path) 11 | retrieve_data(path) do |path| 12 | YAML.load_file(fixture_path(path)) 13 | end 14 | end 15 | 16 | def load_file(path) 17 | retrieve_data(path) do |path| 18 | File.read(fixture_path(path)) 19 | end 20 | end 21 | 22 | def fixture_path(path) 23 | fixtures_path.join(*Array(path)) 24 | end 25 | 26 | private 27 | 28 | def retrieve_data(path) 29 | @loaded_data.fetch(path) do 30 | @loaded_data[path] = yield(path) 31 | end 32 | end 33 | end 34 | FIXTURE = TestFixtures.new 35 | HOTSPOT_DATA = ->(paths) { 36 | FIXTURE.load_metric(["hotspots"].concat(Array(paths))) 37 | } 38 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/hotspots/analysis/ranking_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/analysis/ranking" } 3 | 4 | describe MetricFu::Ranking do 5 | context "with many items" do 6 | specify "#top" do 7 | ranking = Ranking.new 8 | ranking[:a] = 10 9 | ranking[:b] = 50 10 | ranking[:c] = 1 11 | expect(ranking.top).to eq([:b, :a, :c]) 12 | end 13 | 14 | specify "lowest item is at 0 percentile" do 15 | ranking = Ranking.new 16 | ranking[:a] = 10 17 | ranking[:b] = 50 18 | expect(ranking.percentile(:a)).to eq(0) 19 | end 20 | 21 | specify "highest item is at high percentile" do 22 | ranking = Ranking.new 23 | ranking[:a] = 10 24 | ranking[:b] = 50 25 | ranking[:c] = 0 26 | ranking[:d] = 5 27 | expect(ranking.percentile(:b)).to eq(0.75) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/configuration.rb: -------------------------------------------------------------------------------- 1 | MetricFu.lib_require { "templates/metrics_template" } 2 | module MetricFu::Templates 3 | class Configuration 4 | FILE_PREFIX = "file:/" 5 | 6 | def initialize 7 | @options = {} 8 | @options[:template_class] = MetricFu::Templates::MetricsTemplate 9 | @options[:darwin_txmt_protocol_no_thanks] = true 10 | # turning off syntax_highlighting may avoid some UTF-8 issues 11 | @options[:syntax_highlighting] = true 12 | @options[:link_prefix] = FILE_PREFIX 13 | end 14 | 15 | [:template_class, :link_prefix, :syntax_highlighting, :darwin_txmt_protocol_no_thanks].each do |option| 16 | define_method("#{option}=") do |arg| 17 | @options[option] = arg 18 | end 19 | end 20 | 21 | def option(name) 22 | @options.fetch(name.to_sym) { raise "No such template option: #{name}" } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/metric_fu/reporting/graphs/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Bluff graphers responding to #graph!" do 4 | before do 5 | setup_fs 6 | end 7 | after do 8 | cleanup_fs 9 | end 10 | it "should write chart file" do 11 | graphs = {} 12 | available_graphs = MetricFu::Metric.enabled_metrics.select(&:has_graph?).map(&:name) 13 | available_graphs.each do |graph| 14 | grapher_name = graph.to_s.gsub("MetricFu::", "").gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } 15 | grapher_name = grapher_name + "Grapher" 16 | graphs[graph] = MetricFu.const_get(grapher_name).new 17 | end 18 | graphs.each do |key, val| 19 | val.graph! 20 | output_dir = File.expand_path(File.join(MetricFu::Io::FileSystem.directory("output_directory"))) 21 | expect { File.read(File.join(output_dir, "#{key.to_s.downcase}.js")) }.not_to raise_error 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/reek/report.html.erb: -------------------------------------------------------------------------------- 1 |

Reek Results

2 | 3 |

Reek detects common code smells in ruby code.

4 | 5 | <%= render_partial 'graph', {:graph_name => 'reek'} %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% count = 0 %> 15 | <% @reek[:matches].each do |match| %> 16 | <% match[:code_smells].each do |smell| %> 17 | 18 | 19 | 22 | 25 | 28 | 29 | <% count += 1 %> 30 | <% end %> 31 | <% end %> 32 | 33 |
File PathMethodDescriptionType
<%= link_to_filename(match[:file_path]) %> 20 | <%= smell[:method] %> 21 | 23 | <%= smell[:message] %> 24 | 26 | <%= smell[:type] %> 27 |
34 | 35 | <%= render_partial 'report_footer' %> 36 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/stats/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class StatsGrapher < Grapher 4 | attr_accessor :loc_counts, :lot_counts, :labels 5 | 6 | def self.metric 7 | :stats 8 | end 9 | 10 | def initialize 11 | super 12 | self.loc_counts = [] 13 | self.lot_counts = [] 14 | self.labels = {} 15 | end 16 | 17 | def get_metrics(metrics, date) 18 | if metrics && metrics[:stats] 19 | loc_counts.push(metrics[:stats][:codeLOC].to_i) 20 | lot_counts.push(metrics[:stats][:testLOC].to_i) 21 | labels.update(labels.size => date) 22 | end 23 | end 24 | 25 | def title 26 | "Stats: LOC & LOT" 27 | end 28 | 29 | def data 30 | [ 31 | ["LOC", @loc_counts.join(",")], 32 | ["LOT", @lot_counts.join(",")], 33 | ] 34 | end 35 | 36 | def output_filename 37 | "stats.js" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/matcher_create_file.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :create_file do |expected| 2 | match do |block| 3 | @before = exists?(expected) 4 | block.call 5 | @after = exists?(expected) 6 | !@before && @after 7 | end 8 | 9 | failure_message do |_block| 10 | existed_before_message expected do 11 | "The file #{expected.inspect} was not created" 12 | end 13 | end 14 | 15 | failure_message_when_negated do |_block| 16 | existed_before_message expected do 17 | "The file #{expected.inspect} was created" 18 | end 19 | end 20 | 21 | def supports_block_expectations? 22 | true 23 | end 24 | 25 | def exists?(expected) 26 | # Allows us to use wildcard checks for existence. 27 | !Dir.glob(expected).empty? 28 | end 29 | 30 | def existed_before_message(expected) 31 | if @before 32 | "The file #{expected.inspect} existed before, so this test doesn't work" 33 | else 34 | yield 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flay/report.html.erb: -------------------------------------------------------------------------------- 1 |

Flay Results

2 | 3 |

Flay analyzes ruby code for structural similarities.

4 | 5 | <%= render_partial 'graph', {:graph_name => 'flay'} %> 6 | 7 |

Total Score (lower is better): <%= @flay[:total_score] %>

8 |
Scores less than <%= MetricFu::Metric.get_metric('flay').run_options[:minimum_score] %> are not shown or part of the total
9 | 10 | 11 | 12 | 13 | 14 | 15 | <% count = 0 %> 16 | <% @flay[:matches].each do |match| %> 17 | 18 | 23 | 24 | 25 | <% count += 1 %> 26 | <% end %> 27 |
FilesMatches
19 | <% match[:matches].each do |file| %> 20 | <%= link_to_filename(file[:name], file[:line]) %>
21 | <% end %> 22 |
<%= match[:reason] %>
28 | 29 | <%= render_partial 'report_footer' %> 30 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/report.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Analyzed File Report 7 | 8 | 9 | 10 | <% @data.each_with_index do |line, idx| %> 11 | <% line_number = (idx + 1).to_s %> 12 | 13 | 24 | 27 | 28 | <% end %> 29 |
14 | <% if @lines.has_key?(line_number) %> 15 |
    16 | <% @lines[line_number].each do |problem| %> 17 |
  • <%= "#{problem[:description]} » #{problem[:type]}" %>
  • 18 | <% end %> 19 |
20 | <% else %> 21 |   22 | <% end %> 23 |
25 | <%= line_for_display(line, line_number) %> 26 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/support/helper_methods.rb: -------------------------------------------------------------------------------- 1 | def enable_hotspots 2 | MetricFu.configure 3 | hotspot_metrics = MetricFu::Metric.metrics.map(&:name) 4 | hotspot_metrics.each do |metric_name| 5 | path = "#{metric_name}/hotspot" 6 | begin 7 | MetricFu.metrics_require { path } 8 | rescue LoadError 9 | # No hotspot, but that's ok 10 | end 11 | end 12 | end 13 | 14 | def metric_not_activated?(metric_name) 15 | MetricFu.configuration.configure_metrics 16 | metric = MetricFu::Metric.get_metric(metric_name.intern) 17 | if (metric.activate rescue false) # may fail if ripper not supported 18 | false 19 | else 20 | p "Skipping #{metric_name} tests, not activated" 21 | true 22 | end 23 | end 24 | 25 | def breaks_when?(bool) 26 | p "Skipping tests in #{caller[0]}. They unnecessarily break the build." if bool 27 | bool 28 | end 29 | 30 | def compare_paths(path1, path2) 31 | expect(File.join(MetricFu.root_dir, path1)).to eq(File.join(MetricFu.root_dir, path2)) 32 | end 33 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/churn/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::ChurnHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{times_changed} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :churn 10 | end 11 | 12 | def map_strategy 13 | :present 14 | end 15 | 16 | def reduce_strategy 17 | :sum 18 | end 19 | 20 | def score_strategy 21 | :calculate_score 22 | end 23 | 24 | def calculate_score(metric_ranking, item) 25 | flat_churn_score = 0.50 26 | metric_ranking.scored?(item) ? flat_churn_score : 0 27 | end 28 | 29 | def generate_records(data, table) 30 | return if data == nil 31 | Array(data[:changes]).each do |change| 32 | table << { 33 | "metric" => :churn, 34 | "times_changed" => change[:times_changed], 35 | "file_path" => change[:file_path] 36 | } 37 | end 38 | end 39 | 40 | def present_group(group) 41 | "detected high level of churn (changed #{group[0].times_changed} times)" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/churn/generator.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class ChurnGenerator < Generator 3 | def self.metric 4 | :churn 5 | end 6 | 7 | ### 8 | # options available are what can be passed to churn_calculator 9 | # https://github.com/danmayer/churn#library-options 10 | ### 11 | def emit 12 | @output = run(options) 13 | end 14 | 15 | def analyze 16 | if @output.nil? || @output.size.zero? 17 | @churn = { churn: {} } 18 | else 19 | @churn = @output 20 | end 21 | @churn 22 | end 23 | 24 | # ensure hash only has the :churn key 25 | def to_h 26 | { churn: @churn[:churn] } 27 | end 28 | 29 | # @param args [Hash] churn metric run options 30 | # @return [Hash] churn results 31 | def run(args) 32 | # @note passing in false to report will return a hash 33 | # instead of the default String 34 | ::Churn::ChurnCalculator.new(args).report(false) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rcov/generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "rcov/generator" } 3 | require "shared/test_coverage" 4 | 5 | describe MetricFu::RcovGenerator, "configured as rcov" do 6 | it_behaves_like "rcov test coverage generator", :rcov do 7 | describe "emit" do 8 | before :each do 9 | options = { external: nil } 10 | @test_coverage = MetricFu::RcovGenerator.new(@default_options.merge(options)) 11 | end 12 | 13 | it "should set the RAILS_ENV" do 14 | expect(MetricFu::Utility).to receive(:rm_rf).with(MetricFu::RcovGenerator.metric_directory, verbose: false) 15 | expect(MetricFu::Utility).to receive(:mkdir_p).with(MetricFu::RcovGenerator.metric_directory) 16 | options = { environment: "metrics", external: nil } 17 | @test_coverage = MetricFu::RcovGenerator.new(@default_options.merge(options)) 18 | expect(@test_coverage.command).to include("RAILS_ENV=metrics") 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/rcov_line.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class RCovLine 3 | attr_accessor :content, :was_run 4 | 5 | def initialize(content, was_run) 6 | @content = content 7 | @was_run = was_run 8 | end 9 | 10 | def to_h 11 | { content: @content, was_run: @was_run } 12 | end 13 | 14 | def covered? 15 | @was_run == 1 16 | end 17 | 18 | def missed? 19 | @was_run == 0 20 | end 21 | 22 | def ignored? 23 | @was_run.nil? 24 | end 25 | 26 | def self.line_coverage(lines) 27 | lines.map { |line| line[:was_run] } 28 | end 29 | 30 | def self.covered_lines(line_coverage) 31 | line_coverage.count(1) 32 | end 33 | 34 | def self.missed_lines(line_coverage) 35 | line_coverage.count(0) 36 | end 37 | 38 | def self.ignored_lines(line_coverage) 39 | line_coverage.count(nil) 40 | end 41 | 42 | def css_class 43 | return "rcov_not_run" if missed? 44 | 45 | "rcov_run" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rails_best_practices/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class RailsBestPracticesGrapher < Grapher 4 | attr_accessor :rails_best_practices_count, :labels 5 | 6 | def self.metric 7 | :rails_best_practices 8 | end 9 | 10 | def initialize 11 | super 12 | @rails_best_practices_count = [] 13 | @labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:rails_best_practices] 18 | size = (metrics[:rails_best_practices][:problems] || []).size 19 | @rails_best_practices_count.push(size) 20 | @labels.update(@labels.size => date) 21 | end 22 | end 23 | 24 | def title 25 | "Rails Best Practices: design problems" 26 | end 27 | 28 | def data 29 | [ 30 | ["rails_best_practices", @rails_best_practices_count.join(",")] 31 | ] 32 | end 33 | 34 | def output_filename 35 | "rails_best_practices.js" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/generator.rb: -------------------------------------------------------------------------------- 1 | MetricFu.metrics_require { "hotspots/hotspot_analyzer" } 2 | module MetricFu 3 | class HotspotsGenerator < Generator 4 | def self.metric 5 | :hotspots 6 | end 7 | 8 | def initialize(options = {}) 9 | MetricFu::Metric.enabled_metrics.each do |metric| 10 | require_hotspot(metric.name) 11 | end 12 | super 13 | end 14 | 15 | def emit 16 | # no-op 17 | end 18 | 19 | def analyze 20 | analyzer = MetricFu::HotspotAnalyzer.new(MetricFu.result.result_hash) 21 | @hotspots = analyzer.hotspots 22 | end 23 | 24 | def to_h 25 | result = { hotspots: {} } 26 | @hotspots.each do |granularity, hotspots| 27 | result[:hotspots][granularity.to_s] = hotspots.map(&:to_hash) 28 | end 29 | result 30 | end 31 | 32 | private 33 | 34 | def require_hotspot(metric_name) 35 | require "metric_fu/metrics/#{metric_name}/hotspot" 36 | rescue LoadError 37 | mf_debug "*** No hotspot for #{metric_name}" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/three_metrics_on_same_file.yml: -------------------------------------------------------------------------------- 1 | :saikuro: 2 | :files: 3 | - :classes: 4 | - :complexity: 19 5 | :methods: 6 | - :complexity: 1 7 | :lines: 9 8 | :name: Client#client_requested_sync 9 | - :complexity: 1 10 | :lines: 9 11 | :name: Client#method_that_is_not_mentioned_elsewhere_in_stats 12 | :lines: 92 13 | :class_name: Devver::Client 14 | :filename: client.rb 15 | :reek: 16 | :matches: 17 | - :file_path: lib/client/client.rb 18 | :code_smells: 19 | - :type: Large Class 20 | :message: has at least 27 methods 21 | :method: Devver::Client 22 | - :type: Long Method 23 | :message: has approx 6 statements 24 | :method: Devver::Client#client_requested_sync 25 | :flog: 26 | :total: 1817.6 27 | :pages: 28 | - :path: /lib/client/client.rb 29 | :highest_score: 37.9 30 | :average_score: 13.6 31 | :scanned_methods: 32 | - :operators: 33 | - :operator: "[]" 34 | :score: 11.1 35 | :score: 37.9 36 | :name: Client#client_requested_sync 37 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/saikuro/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for saikuro" do 5 | it_behaves_like "configured" do 6 | it "should set @saikuro to { :output_directory => @scratch_directory + '/saikuro', 7 | :input_directory => @code_dirs, 8 | :cyclo => '', 9 | :filter_cyclo => '0', 10 | :warn_cyclo => '5', 11 | :error_cyclo => '7', 12 | :formater => 'text' }" do 13 | load_metric "saikuro" 14 | expect(MetricFu::Metric.get_metric(:saikuro).run_options).to eq( 15 | output_directory: "#{scratch_directory}/saikuro", 16 | input_directory: ["lib"], 17 | cyclo: "", 18 | filter_cyclo: "0", 19 | warn_cyclo: "5", 20 | error_cyclo: "7", 21 | formater: "text" 22 | ) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008,2009,2010,2011 Jake Scruggs 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/metric_fu/metrics/saikuro/parsing_element.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class SaikuroParsingElement 3 | TYPE_REGEX = /Type:(.*) Name/ 4 | NAME_REGEX = /Name:(.*) Complexity/ 5 | COMPLEXITY_REGEX = /Complexity:(.*) Lines/ 6 | LINES_REGEX = /Lines:(.*)/ 7 | 8 | attr_reader :complexity, :lines, :defs, :element_type 9 | attr_accessor :name 10 | 11 | def initialize(line) 12 | @line = line 13 | @element_type = line.match(TYPE_REGEX)[1].strip 14 | @name = line.match(NAME_REGEX)[1].strip 15 | @complexity = line.match(COMPLEXITY_REGEX)[1].strip 16 | @lines = line.match(LINES_REGEX)[1].strip 17 | @defs = [] 18 | end 19 | 20 | def <<(line) 21 | @defs << MetricFu::SaikuroParsingElement.new(line) 22 | end 23 | 24 | def to_h 25 | base = { name: @name, complexity: @complexity.to_i, lines: @lines.to_i } 26 | unless @defs.empty? 27 | defs = @defs.map do |my_def| 28 | my_def = my_def.to_h 29 | my_def.delete(:defs) 30 | my_def 31 | end 32 | base[:defs] = defs 33 | end 34 | base 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/support/matcher_create_files.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :create_files do |expected| 2 | match do |block| 3 | @before = false 4 | @after = true 5 | expected.each do |file| 6 | @before |= exists?(file) 7 | end 8 | block.call 9 | expected.each do |file| 10 | @after &= exists?(file) 11 | end 12 | !@before && @after 13 | end 14 | 15 | failure_message do |_block| 16 | existed_before_message expected do 17 | "One or more files in [#{expected.inspect}] was not created." 18 | end 19 | end 20 | 21 | failure_message_when_negated do |_block| 22 | existed_before_message expected do 23 | "The files in [#{expected.inspect}] were created." 24 | end 25 | end 26 | 27 | def supports_block_expectations? 28 | true 29 | end 30 | 31 | def exists?(expected) 32 | # Allows us to use wildcard checks for existence. 33 | !Dir.glob(expected).empty? 34 | end 35 | 36 | def existed_before_message(expected) 37 | if @before 38 | "One or more files in [#{expected.inspect}] existed before, so this test doesn't work" 39 | else 40 | yield 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/shared/configured.rb: -------------------------------------------------------------------------------- 1 | shared_examples "configured" do 2 | def get_new_config 3 | ENV["CC_BUILD_ARTIFACTS"] = nil 4 | @config = MetricFu.configuration 5 | @config.reset 6 | MetricFu.configuration.configure_metric(:rcov) do |rcov| 7 | rcov.enabled = true 8 | end 9 | MetricFu.configure 10 | allow(MetricFu::Io::FileSystem).to receive(:create_directories) # no need to create directories for the tests 11 | @config 12 | end 13 | 14 | def directory(name) 15 | MetricFu::Io::FileSystem.directory(name) 16 | end 17 | 18 | def base_directory 19 | directory("base_directory") 20 | end 21 | 22 | def output_directory 23 | directory("output_directory") 24 | end 25 | 26 | def scratch_directory 27 | directory("scratch_directory") 28 | end 29 | 30 | def template_directory 31 | directory("template_directory") 32 | end 33 | 34 | def template_class 35 | MetricFu::Formatter::Templates.option("template_class") 36 | end 37 | 38 | def metric_fu_root 39 | directory("root_directory") 40 | end 41 | 42 | def load_metric(metric) 43 | load File.join(MetricFu.metrics_dir, metric, "metric.rb") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :bundler do 2 | watch("Gemfile") 3 | watch("Gemfile.lock") 4 | watch(%w{.+.gemspec\z}) 5 | end 6 | 7 | guard :rspec, cli: File.read(".rspec").split.push("--fail-fast").join(" "), keep_failed: false do 8 | # Run all specs if configuration is modified 9 | watch(".rspec") { "spec" } 10 | watch("Guardfile") { "spec" } 11 | watch("Gemfile.lock") { "spec" } 12 | watch("spec/spec_helper.rb") { "spec" } 13 | 14 | # Run all specs if supporting files are modified 15 | watch(%r{\Aspec/(?:fixtures|lib|support|shared)/.+\.rb\z}) { "spec" } 16 | 17 | # Run unit specs if associated lib code is modified 18 | watch(%r{\Alib/(.+)\.rb\z}) { |m| "spec/#{m[1]}_spec.rb" } 19 | watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { "spec" } 20 | 21 | # Run a spec if it is modified 22 | watch(%r{\Aspec/.+_spec\.rb$\z}) 23 | end 24 | 25 | # TODO enable once we're ready to handle all the violations :) 26 | # guard :rubocop, cli: %w[--config config/rubocop.yml] do 27 | # watch(%r{.+\.(?:rb|rake)\z}) 28 | # watch(%r{\Aconfig/rubocop\.yml\z}) { |m| File.dirname(m[0]) } 29 | # watch(%r{(?:.+/)?\.rubocop\.yml\z}) { |m| File.dirname(m[0]) } 30 | # end 31 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flog/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::FlogHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{score} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :flog 10 | end 11 | 12 | def map_strategy 13 | :score 14 | end 15 | 16 | def reduce_strategy 17 | :average 18 | end 19 | 20 | def score_strategy 21 | :identity 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | Array(data[:method_containers]).each do |method_container| 27 | Array(method_container[:methods]).each do |entry| 28 | file_path = entry[1][:path].sub(%r{^/}, "") if entry[1][:path] 29 | location = MetricFu::Location.for(entry.first) 30 | table << { 31 | "metric" => name, 32 | "score" => entry[1][:score], 33 | "file_path" => file_path, 34 | "class_name" => location.class_name, 35 | "method_name" => location.method_name 36 | } 37 | end 38 | end 39 | end 40 | 41 | def present_group(group) 42 | occurences = group.size 43 | complexity = get_mean(group.column("score")) 44 | "#{'average ' if occurences > 1}complexity is %.1f" % complexity 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/stats/report.html.erb: -------------------------------------------------------------------------------- 1 |

Lines of Code/Tests Metric Results

2 | 3 | <%= render_partial 'graph', {:graph_name => 'stats'} %> 4 | 5 |

Lines of Code/Tests Metrics Results

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Lines of CodeLines of TestCode to test ratio
<%= @stats[:codeLOC] %><%= @stats[:testLOC] %>1:<%= @stats[:code_to_test_ratio] %>
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | <% count = 0 %> 30 | <% @stats[:lines].each do |line| %> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <% count += 1 %> 41 | <% end %> 42 |
NameLinesLOCClassesMethodsMethods per classLOC per method
<%= line[:name] %><%= line[:lines] %><%= line[:loc] %><%= line[:classes] %><%= line[:methods] %><%= line[:methods_per_class] %><%= line[:loc_per_method] %>
43 | 44 | <%= render_partial 'report_footer' %> 45 | -------------------------------------------------------------------------------- /lib/metric_fu/formatter.rb: -------------------------------------------------------------------------------- 1 | require "metric_fu/constantize" 2 | module MetricFu 3 | module Formatter 4 | BUILTIN_FORMATS = { 5 | "html" => ["MetricFu::Formatter::HTML", "Generates a templated HTML report using the configured template class and graph engine."], 6 | "yaml" => ["MetricFu::Formatter::YAML", "Generates the raw output as yaml"] 7 | } 8 | DEFAULT = [[:html]] 9 | 10 | class << self 11 | include MetricFu::Constantize 12 | 13 | def class_for(format) 14 | if (builtin = BUILTIN_FORMATS[format.to_s]) 15 | constantize(builtin[0]) 16 | else 17 | constantize(format.to_s) 18 | end 19 | end 20 | end 21 | 22 | module Templates 23 | MetricFu.lib_require { "templates/metrics_template" } 24 | 25 | module_function 26 | 27 | def templates_configuration=(templates_configuration) 28 | @templates_configuration = templates_configuration 29 | end 30 | 31 | def option(name) 32 | templates_configuration.option(name) 33 | end 34 | 35 | def templates_configuration 36 | @templates_configuration ||= MetricFu::Templates::Configuration.new 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/analyzed_problems.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class HotspotAnalyzedProblems 3 | MetricFu.metrics_require { "hotspots/analysis/ranked_problem_location" } 4 | 5 | def initialize(hotspot_rankings, analyzer_tables) 6 | @hotspot_rankings = hotspot_rankings 7 | @analyzer_tables = analyzer_tables 8 | end 9 | 10 | def worst_items 11 | worst_items = {} 12 | worst_items[:files] = worst(@hotspot_rankings.worst_files, :file) 13 | worst_items[:classes] = worst(@hotspot_rankings.worst_classes, :class) 14 | worst_items[:methods] = worst(@hotspot_rankings.worst_methods, :method) 15 | worst_items 16 | end 17 | 18 | private 19 | 20 | # @param rankings [Array] 21 | # @param granularity [Symbol] one of :class, :method, :file 22 | def worst(rankings, granularity) 23 | rankings.map do |ranked_item_name| 24 | sub_table = get_sub_table(granularity, ranked_item_name) 25 | MetricFu::HotspotRankedProblemLocation.new(sub_table, granularity) 26 | end 27 | end 28 | 29 | def get_sub_table(granularity, ranked_item_name) 30 | tables = @analyzer_tables.tables_for(granularity) 31 | tables[ranked_item_name] 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::RcovHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{percentage_uncovered} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :rcov 10 | end 11 | 12 | def map_strategy 13 | :percentage_uncovered 14 | end 15 | 16 | def reduce_strategy 17 | :average 18 | end 19 | 20 | def score_strategy 21 | :identity 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | data.each do |file_name, info| 27 | next if (file_name == :global_percent_run) || (info[:methods].nil?) 28 | info[:methods].each do |method_name, percentage_uncovered| 29 | location = MetricFu::Location.for(method_name) 30 | table << { 31 | "metric" => :rcov, 32 | "file_path" => file_name, 33 | "class_name" => location.class_name, 34 | "method_name" => location.method_name, 35 | "percentage_uncovered" => percentage_uncovered 36 | } 37 | end 38 | end 39 | end 40 | 41 | def present_group(group) 42 | occurences = group.size 43 | average_code_uncoverage = get_mean(group.column("percentage_uncovered")) 44 | "#{'average ' if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Benjamin Fleischer 2 | Jake Scruggs 3 | Édouard Brière 4 | Richard Huang 5 | Grant McInnes 6 | Petrik 7 | Dan Mayer 8 | Robin Curry 9 | Sean Soper 10 | Delwyn de Villiers 11 | Martin Gotink 12 | Carl Youngblood 13 | Andre Arko 14 | Chris Griego 15 | Sathish 16 | Michael Stark 17 | Nick Quaranto 18 | dan sinclair 19 | joshuacronemeyer 20 | jscruggs 21 | David Chelimsky 22 | adrien 23 | Jay Zeschin 24 | Greg Allen 25 | Alex Rothenberg 26 | calveto 27 | Avdi Grimm 28 | Kevin Rutherford 29 | Randy Souza 30 | GeorgeErickson 31 | Josef Šimánek 32 | Andy Gregorowicz 33 | Tarsoly András 34 | Benjamin Fleischer / Paul Swagerty 35 | Nick Veys 36 | unknown 37 | Micah Geisel 38 | Extrovert 39 | Ben Turner 40 | Andrew Timberlake 41 | KAKUTANI Shintaro 42 | Guilherme Souza 43 | Przemysław Dąbek 44 | carlost 45 | David Barri 46 | Beau Fabry 47 | Chris Mason 48 | Andrew Selder 49 | iain 50 | Mark Wilden 51 | Alessandro Dias 52 | Lars E. Hoeg 53 | Eric Wollesen 54 | Adam Bair 55 | Jinzhu 56 | Matthew Van Horn 57 | Diego Carrion 58 | saltracer 59 | benlovell 60 | ff-cviradiya 61 | khall 62 | Stefan Huber 63 | Todd A. Jacobs 64 | jayzes 65 | Hans Hasselberg 66 | Scyllinice 67 | Bitdeli Chef 68 | René Föhring 69 | Matthew Gordon 70 | factorylabs 71 | Mike Ball 72 | Guilherme Simões 73 | Paul Elliott 74 | Joel Nimety 75 | Chris Ian Fiel 76 | Mike Szyndel 77 | -------------------------------------------------------------------------------- /spec/metric_fu/reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.lib_require { "reporter" } 3 | 4 | describe MetricFu::Reporter do 5 | context "given a single formatter" do 6 | before do 7 | @formatter = double("formatter") 8 | allow(@formatter).to receive(:to_a).and_return([@formatter]) 9 | @reporter = Reporter.new(@formatter) 10 | end 11 | 12 | it "notifies the formatter" do 13 | expect(@formatter).to receive(:start) 14 | expect(@formatter).to receive(:finish) 15 | @reporter.start 16 | @reporter.finish 17 | end 18 | 19 | it "only sends notifications when supported by formatter" do 20 | allow(@formatter).to receive(:respond_to?).with(:display_results).and_return(false) 21 | expect(@formatter).not_to receive(:display_results) 22 | @reporter.display_results 23 | end 24 | end 25 | 26 | context "given multiple formatters" do 27 | before do 28 | @formatters = [double("formatter"), double("formatter")] 29 | @reporter = Reporter.new(@formatters) 30 | end 31 | 32 | it "notifies all formatters" do 33 | @formatters.each do |formatter| 34 | expect(formatter).to receive(:start) 35 | expect(formatter).to receive(:finish) 36 | end 37 | @reporter.start 38 | @reporter.finish 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/report.html.erb: -------------------------------------------------------------------------------- 1 | 4 |

Rcov Code Coverage Results

5 | 6 |

C0 code coverage information.

7 | 8 | <%= render_partial 'graph', {:graph_name => 'rcov'} %> 9 | 10 |

Total Coverage: <%= @rcov.delete(:global_percent_run) %>%

11 | 12 | 13 | 14 | 15 | 16 | <% count = 0 %> 17 | <% @rcov.sort_by{ |k,v| v[:percent_run] }.each do |fname, file| %> 18 | 19 | 20 | 21 | 22 | <% count += 1 %> 23 | <% end %> 24 |
File PathPercent run
<%= fname %><%= file[:percent_run] %>
25 | 26 | <% @rcov.sort_by{ |k,v| v[:percent_run] }.each do |fname, file| %> 27 |

<%= fname %>

28 |
29 | 30 | <% file[:lines].each_with_index do |line, index| %> 31 | 32 | <% rcov_line = RCovLine.new(line[:content], line[:was_run]) %> 33 | 34 | 35 | <% end %> 36 |
<%= link_to_filename(fname, index + 1, "
#{line[:content]}
") %>
37 |
38 | <% end %> 39 | 40 | <%= render_partial 'report_footer' %> 41 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rails_best_practices/generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "rails_best_practices/generator" } 3 | 4 | describe RailsBestPracticesGenerator do 5 | break if metric_not_activated?(:rails_best_practices) 6 | 7 | describe "emit method" do 8 | let(:analyzer) { ::RailsBestPractices::Analyzer.new(".", "silent" => true) } 9 | context "RailsBestPractices provides the expected API" do 10 | it { expect(analyzer).to respond_to :analyze } 11 | it { expect(analyzer).to respond_to :errors } 12 | end 13 | end 14 | 15 | describe "analyze method" do 16 | let(:error) { ::RailsBestPractices::Core::Error.new } 17 | context "RailsBestPractices provdies the expected API" do 18 | it { expect(error).to respond_to :filename } 19 | it { expect(error).to respond_to :line_number } 20 | it { expect(error).to respond_to :message } 21 | it { expect(error).to respond_to :url } 22 | end 23 | end 24 | 25 | describe "to_h method" do 26 | it "should put things into a hash" do 27 | MetricFu::Configuration.run {} 28 | practices = MetricFu::RailsBestPracticesGenerator.new 29 | practices.instance_variable_set(:@rails_best_practices_results, "the_practices") 30 | expect(practices.to_h[:rails_best_practices]).to eq("the_practices") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flay/generator.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class FlayGenerator < Generator 3 | def self.metric 4 | :flay 5 | end 6 | 7 | def emit 8 | args = "#{minimum_duplication_mass} #{dirs_to_flay}" 9 | @output = run!(args) 10 | end 11 | 12 | def analyze 13 | @matches = @output.chomp.split("\n\n").map { |m| m.split("\n ") } 14 | end 15 | 16 | def to_h 17 | { flay: calculate_result(@matches) } 18 | end 19 | 20 | # TODO: move into analyze method 21 | def calculate_result(matches) 22 | total_score = matches.shift.first.split("=").last.strip 23 | target = [] 24 | matches.each do |problem| 25 | reason = problem.shift.strip 26 | lines_info = problem.map do |full_line| 27 | name, line = full_line.split(":").map(&:strip) 28 | { name: name, line: line } 29 | end 30 | target << [reason: reason, matches: lines_info] 31 | end 32 | { 33 | total_score: total_score, 34 | matches: target.flatten 35 | } 36 | end 37 | 38 | private 39 | 40 | def minimum_duplication_mass 41 | flay_mass = options[:minimum_score] 42 | return "" unless flay_mass 43 | 44 | "--mass #{flay_mass} " 45 | end 46 | 47 | def dirs_to_flay 48 | options[:dirs_to_flay].join(" ") 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flay/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::FlayHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{flay_reason flay_matching_reason} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :flay 10 | end 11 | 12 | def map_strategy 13 | :present 14 | end 15 | 16 | def reduce_strategy 17 | :sum 18 | end 19 | 20 | def score_strategy 21 | :percentile 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | Array(data[:matches]).each do |match| 27 | problems = match[:reason] 28 | matching_reason = problems.gsub(/^[0-9]+\) /, "").gsub(/\:[0-9]+/, "") 29 | files = [] 30 | locations = [] 31 | match[:matches].each do |file_match| 32 | file_path = file_match[:name].sub(%r{^/}, "") 33 | locations << "#{file_path}:#{file_match[:line]}" 34 | files << file_path 35 | end 36 | files = files.uniq 37 | files.each do |file| 38 | table << { 39 | "metric" => name, 40 | "file_path" => file, 41 | "flay_reason" => problems + " files: #{locations.join(', ')}", 42 | "flay_matching_reason" => matching_reason 43 | } 44 | end 45 | end 46 | end 47 | 48 | def present_group(group) 49 | occurences = group.size 50 | "found #{occurences} code duplications" 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/roodi/generator.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class RoodiGenerator < Generator 3 | def self.metric 4 | :roodi 5 | end 6 | 7 | def emit 8 | files_to_analyze = options[:dirs_to_roodi].map { |dir| Dir[File.join(dir, "**/*.rb")] } 9 | files = remove_excluded_files(files_to_analyze.flatten) 10 | config = options[:roodi_config] ? "-config=#{options[:roodi_config]}" : "" 11 | args = "#{config} #{files.join(' ')}" 12 | @output = run!(args) 13 | end 14 | 15 | def analyze 16 | @output = MetricFu::Utility.strip_escape_codes(@output) 17 | @matches = @output.chomp.split("\n").map { |m| m.split(" - ") } 18 | total = @matches.pop 19 | @matches.reject! { |array| array.size < 2 } 20 | @matches.map! do |match| 21 | file, line = match[0].split(":") 22 | problem = match[1] 23 | { file: file, line: line, problem: problem } 24 | end 25 | @roodi_results = { total: total, problems: @matches } 26 | end 27 | 28 | def to_h 29 | { roodi: @roodi_results } 30 | end 31 | 32 | def per_file_info(out) 33 | @matches.each do |match| 34 | out[match[:file]] ||= {} 35 | out[match[:file]][match[:line]] ||= [] 36 | out[match[:file]][match[:line]] << { type: :roodi, description: match[:problem] } 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /certs/bf4.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmjCCAoKgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQ8wDQYDVQQDDAZnaXRo 3 | dWIxITAfBgoJkiaJk/IsZAEZFhFiZW5qYW1pbmZsZWlzY2hlcjETMBEGCgmSJomT 4 | 8ixkARkWA2NvbTAeFw0xNTAxMjIxMzAyNTNaFw0xNjAxMjIxMzAyNTNaMEkxDzAN 5 | BgNVBAMMBmdpdGh1YjEhMB8GCgmSJomT8ixkARkWEWJlbmphbWluZmxlaXNjaGVy 6 | MRMwEQYKCZImiZPyLGQBGRYDY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEA7V1VZBU7Aft01XAoK8I8tdClfv3H/NIauiV0jfyNtXtZEWwaZ6ooZNLk 8 | 8kmIUsO2xI7I/B3es6w7le9q9xdEowlYjiR/X/yazNvufu5kpM4f6Ri1AKN8xvPk 9 | LFlR8aOAd9LptcusYDfE+BjhmAvnLTgMGODcDLJIaJzLJaRywTLUuFv4digpFwCm 10 | Zps9VheJnL4hkgI5BDn6DVjxHSCMRnccQM/kX9L34lbP9KkHXXEtQgkQYpElHbnd 11 | MtR753aPeLfOBxSGzsso+6Lhe+fz8huD05mzgWaEZN40e6M7dA9FRSsEzL32ZOad 12 | 0z13MZWj3Yg5srV/cZvzCDCdVvRphwIDAQABo4GMMIGJMAkGA1UdEwQCMAAwCwYD 13 | VR0PBAQDAgSwMB0GA1UdDgQWBBQvUrPExdvmdz0Vau0dH3hRh1YQfDAnBgNVHREE 14 | IDAegRxnaXRodWJAYmVuamFtaW5mbGVpc2NoZXIuY29tMCcGA1UdEgQgMB6BHGdp 15 | dGh1YkBiZW5qYW1pbmZsZWlzY2hlci5jb20wDQYJKoZIhvcNAQEFBQADggEBAEWo 16 | g1soMaRTT/OfFklTuP+odV0w+2qJSfJhOY5bIebDjqxb9BN7hZJ9L6WXhcXCvl6r 17 | kuXjpcC05TIv1DoWWaSjGK2ADmEBDNVhaFepYidAYuUQN4+ZjVH/gS9V9OkBcE8h 18 | 3ZwRv+9RkXM0uY1FwuGI6jgbgPeR1AkkfJnhOPohkG+VN5bFo9aK/Stw8Nwhuuiz 19 | axCPD3cmaJBguufRXSMC852SDiBT7AtI4Gl2Fyr+0M5TzXHKbQ9xRBxwfE1bWDd6 20 | lEs7ndJ1/vd/Hy0zQ1tIRWyql+ITLhqMi161Pw5flsYpQvPlRLR5pGJ4eD0/JdKE 21 | ZG9WSFH7QcGLY65mEYc= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/cane/violations.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | module CaneViolations 3 | class AbcComplexity 4 | def self.parse(violation_list) 5 | violation_list.split(/\n/).map do |violation| 6 | file, method, complexity = violation.split 7 | { file: file, method: method, complexity: complexity } 8 | end 9 | end 10 | end 11 | 12 | class LineStyle 13 | def self.parse(violation_list) 14 | violation_list.split(/\n/).map do |violation| 15 | line, description = violation.split(/\s{2,}/).reject { |x|x.strip == "" } 16 | { line: line, description: description } 17 | end 18 | end 19 | end 20 | 21 | class Comment 22 | def self.parse(violation_list) 23 | violation_list.split(/\n/).map do |violation| 24 | line, class_name = violation.split 25 | { line: line, class_name: class_name } 26 | end 27 | end 28 | end 29 | 30 | class Documentation 31 | def self.parse(violation_list) 32 | violation_list.split(/\n/).map do |violation| 33 | { description: violation.strip } 34 | end 35 | end 36 | end 37 | 38 | class Others 39 | def self.parse(violation_list) 40 | violation_list.split(/\n/).map do |violation| 41 | { description: violation.strip } 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/saikuro/hotspot.rb: -------------------------------------------------------------------------------- 1 | class MetricFu::SaikuroHotspot < MetricFu::Hotspot 2 | COLUMNS = %w{lines complexity} 3 | 4 | def columns 5 | COLUMNS 6 | end 7 | 8 | def name 9 | :saikuro 10 | end 11 | 12 | def map_strategy 13 | :complexity 14 | end 15 | 16 | def reduce_strategy 17 | :average 18 | end 19 | 20 | def score_strategy 21 | :identity 22 | end 23 | 24 | def generate_records(data, table) 25 | return if data == nil 26 | data[:files].each do |file| 27 | file_name = file[:filename] 28 | file[:classes].each do |klass| 29 | location = MetricFu::Location.for(klass[:class_name]) 30 | offending_class = location.class_name 31 | klass[:methods].each do |match| 32 | offending_method = MetricFu::Location.for(match[:name]).method_name 33 | table << { 34 | "metric" => name, 35 | "lines" => match[:lines], 36 | "complexity" => match[:complexity], 37 | "class_name" => offending_class, 38 | "method_name" => offending_method, 39 | "file_path" => file_name, 40 | } 41 | end 42 | end 43 | end 44 | end 45 | 46 | def present_group(group) 47 | occurences = group.size 48 | complexity = get_mean(group.column("complexity")) 49 | "#{'average ' if occurences > 1}complexity is %.1f" % complexity 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # add lib to the load path just like rubygems does 2 | $:.unshift File.expand_path("../../lib", __FILE__) 3 | require "simplecov" 4 | 5 | require "date" 6 | require "test_construct" 7 | require "json" 8 | require "pry-nav" 9 | 10 | require "metric_fu" 11 | include MetricFu 12 | def mf_log(msg); mf_debug(msg); end 13 | 14 | # Requires supporting ruby files with custom matchers and macros, etc, 15 | # in spec/support/ and its subdirectories. 16 | Dir[MetricFu.root_dir + "/spec/support/**/*.rb"].each { |f| require f } 17 | 18 | RSpec.configure do |config| 19 | config.filter_run focus: true 20 | config.run_all_when_everything_filtered = true 21 | # Skip specs tagged `:slow` unless SLOW_SPECS is set 22 | config.filter_run_excluding :slow unless ENV["SLOW_SPECS"] 23 | # End specs on first failure if FAIL_FAST is set 24 | config.fail_fast = ENV.include?("FAIL_FAST") 25 | config.order = :rand 26 | config.color = true 27 | config.expect_with :rspec do |expectations| 28 | expectations.syntax = :expect 29 | end 30 | config.mock_with :rspec do |mocks| 31 | mocks.syntax = :expect 32 | mocks.verify_partial_doubles = true 33 | end 34 | 35 | # :suite after/before all specs 36 | # :each every describe block 37 | # :all every it block 38 | 39 | config.after(:suite) do 40 | cleanup_fs 41 | end 42 | 43 | config.append_after do 44 | MetricFu.reset 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/metric_fu/metric_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MetricFu::Metric do 4 | before do 5 | @metric = MetricFu::Metric.get_metric(:flog) 6 | # @original_options = @metric.run_options.dup 7 | end 8 | 9 | # it "can have its run_options over-written" do 10 | # new_options = {:foo => 'bar'} 11 | # @metric.run_options = new_options 12 | # expect(@original_options).to_not eq(new_options) 13 | # expect(@metric.run_options).to eq(new_options) 14 | # end 15 | 16 | # it "can have its run_options modified" do 17 | # new_options = {:foo => 'bar'} 18 | # @metric.run_options.merge!(new_options) 19 | # expect(@metric.run_options).to eq(@original_options.merge(new_options)) 20 | # end 21 | 22 | context "given a valid configurable option" do 23 | before do 24 | allow(@metric).to receive(:default_run_options).and_return(foo: "baz") 25 | end 26 | 27 | it "can be configured as an attribute" do 28 | @metric.foo = "qux" 29 | expect(@metric.run_options[:foo]).to eq("qux") 30 | end 31 | end 32 | 33 | context "given an invalid configurable option" do 34 | before do 35 | allow(@metric).to receive(:default_run_options).and_return({}) 36 | end 37 | 38 | it "raises an error" do 39 | expect { @metric.foo = "bar" }.to raise_error(RuntimeError, /not a valid configuration option/) 40 | end 41 | end 42 | 43 | after do 44 | @metric.configured_run_options.clear 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/metric.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class MetricRcov < Metric 3 | def name 4 | :rcov 5 | end 6 | 7 | def default_run_options 8 | { 9 | environment: "test", 10 | test_files: Dir["{spec,test}/**/*_{spec,test}.rb"], 11 | rcov_opts: rcov_opts, 12 | external: nil, 13 | } 14 | end 15 | 16 | def coverage_file=(coverage_file) 17 | configured_run_options.update(external: coverage_file) 18 | end 19 | 20 | def has_graph? 21 | true 22 | end 23 | 24 | def enable 25 | if external_coverage_file? 26 | super 27 | else 28 | mf_debug("RCov is not available. See README") 29 | end 30 | end 31 | 32 | def activate 33 | super 34 | end 35 | 36 | def external_coverage_file? 37 | if coverage_file = run_options[:external] 38 | File.exist?(coverage_file) || 39 | mf_log("Configured RCov file #{coverage_file.inspect} does not exist") 40 | else 41 | false 42 | end 43 | end 44 | 45 | private 46 | 47 | def rcov_opts 48 | rcov_opts = [ 49 | "--sort coverage", 50 | "--no-html", 51 | "--text-coverage", 52 | "--no-color", 53 | "--profile", 54 | "--exclude-only '.*'", 55 | '--include-file "\Aapp,\Alib"' 56 | ] 57 | rcov_opts << "-Ispec" if File.exist?("spec") 58 | rcov_opts 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rails_best_practices/generator.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | class RailsBestPracticesGenerator < Generator 3 | def self.metric 4 | :rails_best_practices 5 | end 6 | 7 | def initialize(options = {}) 8 | super(MetricFu::Utility.stringify_keys(options)) 9 | end 10 | 11 | def emit 12 | mf_debug "** Rails Best Practices" 13 | path = "." 14 | analyzer = ::RailsBestPractices::Analyzer.new(path, options) 15 | analyzer.analyze 16 | @output = analyzer.errors 17 | end 18 | 19 | def analyze 20 | @problems = @output.collect do |problem| 21 | { 22 | file: problem.filename, 23 | line: problem.line_number, 24 | problem: problem.message, 25 | url: problem.url 26 | } 27 | end 28 | total = ["Found #{@problems.count} errors."] 29 | @rails_best_practices_results = { total: total, problems: @problems } 30 | end 31 | 32 | def to_h 33 | { rails_best_practices: @rails_best_practices_results } 34 | end 35 | 36 | def per_file_info(out) 37 | @rails_best_practices_results[:problems].each do |problem| 38 | next if problem[:file] == "" || problem[:problem].nil? 39 | 40 | lines = problem[:line].split(/\s*,\s*/) 41 | lines.each do |line| 42 | out[problem[:file]][line] << { type: :rails_best_practices, description: problem[:problem] } 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/reek/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class ReekGrapher < Grapher 4 | attr_accessor :reek_count, :labels 5 | 6 | def self.metric 7 | :reek 8 | end 9 | 10 | def initialize 11 | super 12 | @reek_count = {} 13 | @labels = {} 14 | end 15 | 16 | def get_metrics(metrics, date) 17 | if metrics && metrics[:reek] 18 | counter = @labels.size 19 | @labels.update(@labels.size => date) 20 | 21 | metrics[:reek][:matches].each do |reek_chunk| 22 | reek_chunk[:code_smells].each do |code_smell| 23 | # speaking of code smell... 24 | @reek_count[code_smell[:type]] = [] if @reek_count[code_smell[:type]].nil? 25 | if @reek_count[code_smell[:type]][counter].nil? 26 | @reek_count[code_smell[:type]][counter] = 1 27 | else 28 | @reek_count[code_smell[:type]][counter] += 1 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | def title 36 | "Reek: code smells" 37 | end 38 | 39 | def data 40 | @reek_count.map do |name, count| 41 | [name, nil_counts_to_zero(count).join(",")] 42 | end 43 | end 44 | 45 | def output_filename 46 | "reek.js" 47 | end 48 | 49 | private 50 | 51 | def nil_counts_to_zero(counts) 52 | counts.map { |count| count || 0 } 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/metric_fu/formatter/syntax.rb: -------------------------------------------------------------------------------- 1 | require "coderay" 2 | MetricFu.lib_require { "utility" } 3 | # CodeRay options 4 | # used to analyze source code, because object Tokens is a list of tokens with specified types. 5 | # :tab_width – tabulation width in spaces. Default: 8 6 | # :css – how to include the styles (:class и :style). Default: :class) 7 | # 8 | # :wrap – wrap result in html tag :page, :div, :span or not to wrap (nil) 9 | # 10 | # :line_numbers – how render line numbers (:table, :inline, :list or nil) 11 | # 12 | # :line_number_start – first line number 13 | # 14 | # :bold_every – make every n-th line number bold. Default: 10 15 | module MetricFu 16 | module Formatter 17 | class Syntax 18 | def initialize 19 | @options = { css: :class, style: :alpha } 20 | @line_number_options = { line_numbers: :inline, line_number_start: 0 } 21 | end 22 | 23 | def highlight(ruby_text, line_number) 24 | tokens = tokenize(ruby_text) 25 | tokens.div(highlight_options(line_number)) 26 | end 27 | 28 | def highlight_options(line_number) 29 | line_number = line_number.to_i 30 | if line_number > 0 31 | @options.merge(@line_number_options.merge(line_number_start: line_number)) 32 | else 33 | @options 34 | end 35 | end 36 | 37 | private 38 | 39 | def tokenize(ruby_text) 40 | ascii_text = MetricFu::Utility.clean_ascii_text(ruby_text) 41 | tokens = CodeRay.scan(ascii_text, :ruby) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/table.rb: -------------------------------------------------------------------------------- 1 | %w(record).each do |path| 2 | MetricFu.metrics_require { "hotspots/analysis/#{path}" } 3 | end 4 | 5 | module MetricFu 6 | class Table 7 | include Enumerable 8 | 9 | def initialize(opts = {}) 10 | @rows = [] 11 | @columns = opts.fetch(:column_names) 12 | 13 | @make_index = opts.fetch(:make_index) { true } 14 | @metric_index = {} 15 | end 16 | 17 | def <<(row) 18 | record = nil 19 | if row.is_a?(MetricFu::Record) 20 | record = row 21 | else 22 | record = MetricFu::Record.new(row, @columns) 23 | end 24 | @rows << record 25 | updated_key_index(record) if @make_index 26 | end 27 | 28 | def each 29 | @rows.each do |row| 30 | yield row 31 | end 32 | end 33 | 34 | def size 35 | length 36 | end 37 | 38 | def length 39 | @rows.length 40 | end 41 | 42 | def [](index) 43 | @rows[index] 44 | end 45 | 46 | def column(column_name) 47 | arr = [] 48 | @rows.each do |row| 49 | arr << row[column_name] 50 | end 51 | arr 52 | end 53 | 54 | def group_by_metric 55 | @metric_index.to_a 56 | end 57 | 58 | private 59 | 60 | def updated_key_index(record) 61 | if record.has_key?("metric") 62 | @metric_index[record.metric] ||= MetricFu::Table.new(column_names: @columns, make_index: false) 63 | @metric_index[record.metric] << record 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/metric_fu/formatter/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for formatters" do 5 | it_behaves_like "configured" do 6 | describe "#configure_formatter" do 7 | before(:each) { get_new_config } 8 | 9 | context "given a built-in formatter" do 10 | before do 11 | @config.configure_formatter("html") 12 | end 13 | 14 | it "adds to the list of formatters" do 15 | expect(@config.formatters.first).to be_an_instance_of(MetricFu::Formatter::HTML) 16 | end 17 | end 18 | 19 | context "given a custom formatter by class name" do 20 | before do 21 | stub_const("MyCustomFormatter", Class.new { def initialize(*); end }) 22 | @config.configure_formatter("MyCustomFormatter") 23 | end 24 | 25 | it "adds to the list of formatters" do 26 | expect(@config.formatters.first).to be_an_instance_of(MyCustomFormatter) 27 | end 28 | end 29 | 30 | context "given multiple formatters" do 31 | before do 32 | stub_const("MyCustomFormatter", Class.new { def initialize(*); end }) 33 | @config.configure_formatter("html") 34 | @config.configure_formatter("yaml") 35 | @config.configure_formatter("MyCustomFormatter") 36 | end 37 | 38 | it "adds each to the list of formatters" do 39 | expect(@config.formatters.count).to eq(3) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/metric_fu/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MetricFu::Formatter do 4 | describe "formatter class loading" do 5 | context "given a built-in formatter (string)" do 6 | subject { MetricFu::Formatter.class_for("html") } 7 | 8 | it "returns the formatter class" do 9 | expect(subject).to eq(MetricFu::Formatter::HTML) 10 | end 11 | end 12 | 13 | context "given a built-in formatter (symbol)" do 14 | subject { MetricFu::Formatter.class_for(:yaml) } 15 | 16 | it "returns the formatter class" do 17 | expect(subject).to eq(MetricFu::Formatter::YAML) 18 | end 19 | end 20 | 21 | context "given an unknown built-in formatter" do 22 | subject { MetricFu::Formatter.class_for(:unknown) } 23 | 24 | it "raises an error" do 25 | expect { subject }.to raise_error(NameError) 26 | end 27 | end 28 | 29 | context "given a custom formatter that exists" do 30 | subject { MetricFu::Formatter.class_for("MyCustomFormatter") } 31 | 32 | before do 33 | stub_const("MyCustomFormatter", Class.new { def initialize(*); end }) 34 | end 35 | 36 | it "returns the formatter class" do 37 | expect(subject).to eq(MyCustomFormatter) 38 | end 39 | end 40 | 41 | context "given a custom formatter that doesnt exist" do 42 | subject { MetricFu::Formatter.class_for("MyNonExistentCustomFormatter") } 43 | 44 | it "raises an error" do 45 | expect { subject }.to raise_error(NameError) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/metric_fu/logger.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | require "forwardable" 3 | module MetricFu 4 | def self.logger 5 | @logger ||= ::MetricFu::Logger.new($stdout) 6 | end 7 | 8 | class Logger 9 | extend Forwardable 10 | MfLogger = ::Logger 11 | 12 | def initialize(stdout) 13 | @logger = MfLogger.new(stdout) 14 | self.debug_on = false 15 | self.formatter = ->(_severity, _time, _progname, msg) { "#{msg}\n" } 16 | self.level = "info" 17 | end 18 | 19 | def debug_on=(bool) 20 | self.level = bool ? "debug" : "info" 21 | end 22 | 23 | def debug_on 24 | @logger.level == MfLogger::DEBUG 25 | end 26 | 27 | def_delegators :@logger, :info, :warn, :error, :fatal, :unknown 28 | 29 | LEVELS = { 30 | "debug" => MfLogger::DEBUG, 31 | "info" => MfLogger::INFO, 32 | "warn" => MfLogger::WARN, 33 | "error" => MfLogger::ERROR, 34 | "fatal" => MfLogger::FATAL, 35 | "unknown" => MfLogger::UNKNOWN, 36 | } 37 | 38 | def level=(level) 39 | @logger.level = LEVELS.fetch(level.to_s.downcase) { level } 40 | end 41 | 42 | def formatter=(formatter) 43 | @logger.formatter = formatter 44 | end 45 | 46 | def log(msg) 47 | @logger.info "*" * 5 + msg.to_s 48 | end 49 | 50 | def debug(msg) 51 | @logger.debug "*" * 5 + msg.to_s 52 | end 53 | end 54 | end 55 | # For backward compatibility 56 | def mf_debug(msg, &block) 57 | MetricFu.logger.debug(msg, &block) 58 | end 59 | 60 | def mf_log(msg, &block) 61 | MetricFu.logger.log(msg, &block) 62 | end 63 | -------------------------------------------------------------------------------- /spec/capture_warnings.rb: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/metric_fu/metric_fu/master/spec/capture_warnings.rb 2 | require "rubygems" if RUBY_VERSION =~ /^1\.8/ 3 | require "bundler/setup" 4 | require "rspec/core" 5 | require "rspec/expectations" 6 | require "tempfile" 7 | require "fileutils" 8 | 9 | stderr_file = Tempfile.new("app.stderr") 10 | app_root ||= Dir.pwd 11 | output_dir = File.join(app_root, "tmp") 12 | FileUtils.mkdir_p(output_dir) 13 | bundle_dir = File.join(app_root, "bundle") 14 | 15 | RSpec.configure do |config| 16 | config.before(:suite) do 17 | $stderr.reopen(stderr_file.path) 18 | $VERBOSE = true 19 | end 20 | 21 | config.after(:suite) do 22 | stderr_file.rewind 23 | lines = stderr_file.read.split("\n").uniq 24 | stderr_file.close! 25 | 26 | $stderr.reopen(STDERR) 27 | 28 | app_warnings, other_warnings = lines.partition { |line| 29 | line.include?(app_root) && !line.include?(bundle_dir) 30 | } 31 | 32 | if app_warnings.any? 33 | puts <<-WARNINGS 34 | #{'-' * 30} app warnings: #{'-' * 30} 35 | 36 | #{app_warnings.join("\n")} 37 | 38 | #{'-' * 75} 39 | WARNINGS 40 | end 41 | 42 | if other_warnings.any? 43 | output_file = File.join(output_dir, "warnings.txt") 44 | File.write(output_file, other_warnings.join("\n") << "\n") 45 | puts 46 | puts "Non-app warnings written to tmp/warnings.txt" 47 | puts 48 | end 49 | 50 | # fail the build... 51 | if app_warnings.any? 52 | abort "Failing build due to app warnings: #{app_warnings.inspect}" 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/several_metrics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :reek: 3 | :matches: 4 | - :file_path: lib/client/client.rb 5 | :code_smells: 6 | - :type: Large Class 7 | :message: has at least 27 methods 8 | :method: Devver::Client 9 | - :type: Long Method 10 | :message: has approx 6 statements 11 | :method: Devver::Client#client_requested_sync 12 | :flog: 13 | :method_containers: 14 | - :highest_score: 61.5870319141946 15 | :path: /lib/client/client.rb 16 | :methods: 17 | Client#client_requested_sync: 18 | :path: /lib/client/client.rb 19 | :score: 37.9270319141946 20 | :operators: 21 | :+: 1.70000000000001 22 | :/: 1.80000000000001 23 | :method_at_line: 1.90000000000001 24 | :puts: 1.70000000000001 25 | :assignment: 33.0000000000001 26 | :in_method?: 1.70000000000001 27 | :message: 1.70000000000001 28 | :branch: 12.6 29 | :<<: 3.40000000000001 30 | :each: 1.50000000000001 31 | :lit_fixnum: 1.45 32 | :raise: 1.80000000000001 33 | :each_pair: 1.3 34 | :*: 1.60000000000001 35 | :to_f: 2.00000000000001 36 | :each_with_index: 3.00000000000001 37 | :[]: 22.3000000000001 38 | :new: 1.60000000000001 39 | :average_score: 11.1209009055421 40 | :total_score: 1817.6 41 | :name: Client#client_requested_sync 42 | :churn: 43 | :changes: 44 | - :file_path: lib/client/client.rb 45 | :times_changed: 54 46 | - :file_path: lib/client/foo.rb 47 | :times_changed: 52 48 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/generator.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :reek: 3 | :matches: 4 | - :file_path: lib/client/client.rb:42 5 | :code_smells: 6 | - :type: Large Class 7 | :message: has at least 27 methods 8 | :method: Devver::Client 9 | - :type: Long Method 10 | :message: has approx 6 statements 11 | :method: Devver::Client#client_requested_sync 12 | :flog: 13 | :method_containers: 14 | - :highest_score: 61.5870319141946 15 | :path: /lib/client/client.rb 16 | :methods: 17 | Client#client_requested_sync: 18 | :path: /lib/client/client.rb:42 19 | :score: 37.9270319141946 20 | :operators: 21 | :+: 1.70000000000001 22 | :/: 1.80000000000001 23 | :method_at_line: 1.90000000000001 24 | :puts: 1.70000000000001 25 | :assignment: 33.0000000000001 26 | :in_method?: 1.70000000000001 27 | :message: 1.70000000000001 28 | :branch: 12.6 29 | :<<: 3.40000000000001 30 | :each: 1.50000000000001 31 | :lit_fixnum: 1.45 32 | :raise: 1.80000000000001 33 | :each_pair: 1.3 34 | :*: 1.60000000000001 35 | :to_f: 2.00000000000001 36 | :each_with_index: 3.00000000000001 37 | :[]: 22.3000000000001 38 | :new: 1.60000000000001 39 | :average_score: 11.1209009055421 40 | :total_score: 1817.6 41 | :name: Client#client_requested_sync 42 | :churn: 43 | :changes: 44 | - :file_path: lib/client/client.rb 45 | :times_changed: 54 46 | - :file_path: lib/client/foo.rb 47 | :times_changed: 52 48 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/report.rb: -------------------------------------------------------------------------------- 1 | MetricFu.lib_require { "formatter/syntax" } 2 | MetricFu.lib_require { "templates/template" } 3 | 4 | # Creates an HTML document for a given analyzed file, 5 | # with scored metrics annotating the relevant line. 6 | module MetricFu 7 | module Templates 8 | class Report < MetricFu::Template 9 | # @param file [String] the analyzed file to annotate 10 | # @param lines [Hash] of line number [String] keyed to an list [[Array] of metrics for that line. Each metric in the list is a hash containing the keys :type => metric_name, :descrption => metric_score 11 | # @example file and lines 12 | # file: "lib/metric_fu/gem_version.rb 13 | # lines: {"30"=>[{:type=>:flog, :description=>"Score of 22.43"}], "42"=>[{:type=>:flog, :description=>"Score of 8.64"}]} 14 | def initialize(file, lines) 15 | @file = file 16 | @lines = lines 17 | @data = File.open(file, "rb") { |f| f.readlines } 18 | end 19 | 20 | def render 21 | erbify("report") 22 | end 23 | 24 | def convert_ruby_to_html(ruby_text, line_number) 25 | MetricFu::Formatter::Syntax.new.highlight(ruby_text, line_number) 26 | end 27 | 28 | def line_for_display(line, line_number) 29 | if MetricFu::Formatter::Templates.option("syntax_highlighting") 30 | line_for_display = convert_ruby_to_html(line, line_number) 31 | else 32 | "#{line_number}#{line}" 33 | end 34 | end 35 | 36 | def template_directory 37 | File.dirname(__FILE__) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/metric_fu_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "spec_helper" 3 | 4 | describe MetricFu do 5 | before do 6 | MetricFu.run_dir = Dir.pwd 7 | MetricFu.report_name = nil 8 | end 9 | 10 | describe ".with_run_dir" do 11 | it "temporarily uses a specified `run_dir`" do 12 | expect(MetricFu.run_dir).to eq(Dir.pwd) 13 | 14 | MetricFu.with_run_dir "spec/dummy" do 15 | expect(MetricFu.run_dir).to eq("spec/dummy") 16 | end 17 | 18 | expect(MetricFu.run_dir).to eq(Dir.pwd) 19 | end 20 | end 21 | 22 | specify "the default report_name is the run directory base name" do 23 | MetricFu.with_run_dir "spec/support" do 24 | expect(MetricFu.report_name).to eq("support") 25 | end 26 | end 27 | 28 | specify "the user can set the report_name" do 29 | original_report_name = MetricFu.report_name 30 | 31 | MetricFu.report_name = "override" 32 | expect(MetricFu.report_name).to eq("override") 33 | 34 | MetricFu.report_name = original_report_name 35 | end 36 | 37 | it "has a global report time (corresponding to the time of the VCS code state)" do 38 | expect(MetricFu.report_time - Time.now).to be_within(0.1).of(0) 39 | end 40 | 41 | it "has a global current time (corresponding to report generation time)" do 42 | expect(MetricFu.current_time - Time.now).to be_within(0.1).of(0) 43 | end 44 | 45 | it "has a global report id" do 46 | expect(MetricFu.report_id).to eq(Time.now.strftime("%Y%m%d")) 47 | end 48 | 49 | it "has a global report fingerprint (corresponding to VCS code state)" do 50 | expect(MetricFu.report_fingerprint.to_i - Time.now.to_i).to be_within(0.1).of(0) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | source "https://rubygems.org" 3 | 4 | if RUBY_VERSION >= '1.9.3' 5 | gem "rubocop", platforms: :mri, groups: [:test, :local_development] 6 | gem "activesupport", "~> 4.2" 7 | 8 | if RUBY_VERSION > '2.3' 9 | gem "json", ">= 2.0" 10 | else 11 | gem "json", "~> 1.7" 12 | end 13 | if RUBY_VERSION =~ /^2\.0\..*/ 14 | gem "unparser", "0.2.4" 15 | elsif RUBY_VERSION =~ /^1\.9\.3.*/ 16 | gem "unparser", "0.2.4" 17 | gem "json_pure", "2.0.1" 18 | gem "mime-types", "2.99.3" 19 | gem "rest-client", "1.8.0" 20 | gem "addressable", "2.4.0" 21 | gem "ffi", "1.9.14" # windows support 22 | end 23 | elsif RUBY_VERSION =~ /^1\.9\.2.*/ 24 | # because of https://github.com/railsbp/rails_best_practices/blob/master/rails_best_practices.gemspec 25 | gem "activesupport", "~> 3.2" 26 | # because of https://github.com/troessner/reek/issues/334 27 | gem "reek", "~> 1.4.0" 28 | # rbp -> as -> i18n 29 | gem "i18n", "0.6.11" 30 | gem "parallel", "= 1.3.3" # 1.3.4 disallows 1.9.2 31 | gem "unparser", "0.1.5" 32 | gem "json_pure", "2.0.1" 33 | gem "mime-types", "2.99.3" 34 | gem "rest-client", "1.8.0" 35 | gem "json", "~> 1.7" 36 | gem "addressable", "2.4.0" 37 | gem "rainbow", "2.1.0" 38 | gem "ffi", "1.9.14" # windows support 39 | end 40 | 41 | gemspec path: File.expand_path("..", __FILE__) 42 | 43 | platform :jruby do 44 | group :jruby do 45 | gem "jruby-openssl", "~> 0.9.17" 46 | end 47 | end 48 | 49 | group :test, :local_development do 50 | gem "pry" 51 | gem "pry-nav" 52 | end 53 | 54 | # Added by devtools 55 | group :development do 56 | gem "rake", "~> 10.1.0" 57 | gem "yard", "~> 0.8.7", group: :yard 58 | end 59 | -------------------------------------------------------------------------------- /spec/fixtures/hotspots/generator_analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | files: 3 | - location: 4 | class_name: 5 | method_name: 6 | file_path: lib/client/client.rb:42 7 | file_name: lib/client/client.rb 8 | line_number: '42' 9 | hash_key: '["lib/client/client.rb:42", nil, nil]' 10 | details: 11 | flog: complexity is 37.9 12 | reek: found 2 code smells 13 | - location: 14 | class_name: 15 | method_name: 16 | file_path: lib/client/client.rb 17 | file_name: lib/client/client.rb 18 | line_number: 19 | hash_key: '["lib/client/client.rb", nil, nil]' 20 | details: 21 | churn: detected high level of churn (changed 54 times) 22 | - location: 23 | class_name: 24 | method_name: 25 | file_path: lib/client/foo.rb 26 | file_name: lib/client/foo.rb 27 | line_number: 28 | hash_key: '["lib/client/foo.rb", nil, nil]' 29 | details: 30 | churn: detected high level of churn (changed 52 times) 31 | classes: 32 | - location: 33 | class_name: Client 34 | method_name: 35 | file_path: lib/client/client.rb:42 36 | file_name: lib/client/client.rb 37 | line_number: '42' 38 | hash_key: '["lib/client/client.rb:42", "Client", nil]' 39 | details: 40 | flog: complexity is 37.9 41 | reek: found 2 code smells 42 | methods: 43 | - location: 44 | class_name: Client 45 | method_name: Client#client_requested_sync 46 | file_path: lib/client/client.rb:42 47 | file_name: lib/client/client.rb 48 | line_number: '42' 49 | hash_key: '["lib/client/client.rb:42", "Client", "Client#client_requested_sync"]' 50 | simple_method_name: '#client_requested_sync' 51 | details: 52 | flog: complexity is 37.9 53 | reek: found 1 code smells 54 | -------------------------------------------------------------------------------- /lib/metric_fu/reporting/graphs/grapher.rb: -------------------------------------------------------------------------------- 1 | require "multi_json" 2 | module MetricFu 3 | class Grapher 4 | @graphers = [] 5 | # @return all subclassed graphers [Array] 6 | def self.graphers 7 | @graphers 8 | end 9 | 10 | def self.inherited(subclass) 11 | @graphers << subclass 12 | end 13 | 14 | def self.get_grapher(metric) 15 | graphers.find { |grapher|grapher.metric.to_s == metric.to_s } 16 | end 17 | 18 | attr_accessor :output_directory 19 | 20 | def initialize(opts = {}) 21 | self.output_directory = opts[:output_directory] 22 | end 23 | 24 | def output_directory 25 | @output_directory || MetricFu::Io::FileSystem.directory("output_directory") 26 | end 27 | 28 | def get_metrics(_metrics, _sortable_prefix) 29 | not_implemented 30 | end 31 | 32 | def graph! 33 | labels = MultiJson.dump(@labels) 34 | content = <<-EOS 35 | var graph_title = '#{title}'; 36 | #{build_data(data)} 37 | var graph_labels = #{labels}; 38 | EOS 39 | File.open(File.join(output_directory, output_filename), "w") { |f| f << content } 40 | end 41 | 42 | def title 43 | not_implemented 44 | end 45 | 46 | def date 47 | not_implemented 48 | end 49 | 50 | def output_filename 51 | not_implemented 52 | end 53 | 54 | private 55 | 56 | def build_data(data) 57 | "var graph_series = [" << Array(data).map do |label, datum| 58 | "{name: '#{label}', data: [#{datum}]}" 59 | end.join(",") << "];" 60 | end 61 | 62 | def not_implemented 63 | raise "#{__LINE__} in #{__FILE__} from #{caller[0]}" 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/metric_fu/reporting/graphs/graph_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.reporting_require { "graphs/graph" } 3 | 4 | describe MetricFu do 5 | describe "responding to #graph" do 6 | it "should return an instance of Graph" do 7 | expect(MetricFu.graph).to be_a(Graph) 8 | end 9 | end 10 | end 11 | 12 | describe MetricFu::Graph do 13 | before(:each) do 14 | @graph = MetricFu::Graph.new 15 | end 16 | 17 | describe "setting the date on the graph" do 18 | # TODO better test 19 | it "should set the date once for one data point" do 20 | metric_file = "metric_fu/tmp/_data/20101105.yml" 21 | expect(MetricFu::Utility).to receive(:glob).and_return([metric_file].sort) 22 | expect(MetricFu::Utility).to receive(:load_yaml).with(metric_file).and_return("Metrics") 23 | double_grapher = double 24 | expect(double_grapher).to receive(:get_metrics).with("Metrics", "11/5") 25 | expect(double_grapher).to receive(:graph!) 26 | 27 | @graph.graphers = [double_grapher] 28 | @graph.generate 29 | end 30 | 31 | # TODO better test 32 | it "should set the date when the data directory isn't in the default place" do 33 | metric_file = "/some/kind/of/weird/directory/somebody/configured/_data/20101105.yml" 34 | expect(MetricFu::Utility).to receive(:glob).and_return([metric_file].sort) 35 | expect(MetricFu::Utility).to receive(:load_yaml).with(metric_file).and_return("Metrics") 36 | double_grapher = double 37 | expect(double_grapher).to receive(:get_metrics).with("Metrics", "11/5") 38 | expect(double_grapher).to receive(:graph!) 39 | 40 | @graph.graphers = [double_grapher] 41 | @graph.generate 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/metric_fu/reporting/result_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.reporting_require { "result" } 3 | 4 | describe MetricFu do 5 | describe "#result" do 6 | it "should return an instance of Result" do 7 | expect(MetricFu.result.instance_of?(Result)).to be(true) 8 | end 9 | end 10 | end 11 | 12 | describe MetricFu::Result do 13 | before(:each) do 14 | @result = MetricFu::Result.new 15 | end 16 | 17 | describe "#as_yaml" do 18 | it "should call #result_hash" do 19 | result_hash = double("result_hash") 20 | expect(result_hash).to receive(:to_yaml) 21 | 22 | expect(@result).to receive(:result_hash).and_return(result_hash) 23 | @result.as_yaml 24 | end 25 | end 26 | 27 | describe "#result_hash" do 28 | end 29 | 30 | describe "#add" do 31 | it "should add a passed hash to the result_hash instance variable" do 32 | result_type = double("result_type") 33 | allow(result_type).to receive(:to_s).and_return("type") 34 | 35 | result_inst = double("result_inst") 36 | expect(result_type).to receive(:new).and_return(result_inst) 37 | 38 | expect(result_inst).to receive(:generate_result).and_return(a: "b") 39 | expect(result_inst).to receive(:respond_to?).and_return(false) 40 | 41 | expect(MetricFu::Generator).to receive(:get_generator). 42 | with(result_type).and_return(result_type) 43 | result_hash = double("result_hash") 44 | expect(result_hash).to receive(:merge!).with(a: "b") 45 | expect(@result).to receive(:result_hash).and_return(result_hash) 46 | expect(@result).to receive(:metric_options_for_result_type).with(result_type) 47 | @result.add(result_type) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/churn/report.html.erb: -------------------------------------------------------------------------------- 1 |

Source Control Churn Results

2 |

Files that change a lot in your project may be bad a sign. 3 | This task uses your source control log to identify those files. 4 |

5 | 6 | 7 | 8 | 9 | 10 | <% count = 0 %> 11 | <% if @churn[:changes] %> 12 | <% @churn[:changes].each do |change| %> 13 | 14 | 15 | 16 | 17 | <% count += 1 %> 18 | <% end %> 19 | <% end %> 20 |
File PathTimes Changed
<%= link_to_filename(change[:file_path]) %><%= change[:times_changed] %>
21 | 22 | <% if @churn[:class_churn] %> 23 |

Classes that change a lot in your project may be bad a sign.

24 | 25 | 26 | 27 | 28 | 29 | <% count = 0 %> 30 | <% @churn[:class_churn].each do |change| %> 31 | 32 | 33 | 34 | 35 | <% count += 1 %> 36 | <% end %> 37 |
File PathTimes Changed
<%= link_to_filename(change['klass']['file']) %> <%= change['klass']['klass'] %><%= change['times_changed'] %>
38 | <% end %> 39 | 40 | <% if @churn[:method_churn] %> 41 |

Methods that change a lot in your project may be bad a sign.

42 | 43 | 44 | 45 | 46 | 47 | <% count = 0 %> 48 | <% @churn[:method_churn].each do |change| %> 49 | 50 | 51 | 52 | 53 | <% count += 1 %> 54 | <% end %> 55 |
File PathTimes Changed
<%= link_to_filename(change['method']['file']) %> <%= change['method']['method'] %><%= change['times_changed'] %>
56 | <% end %> 57 | 58 | <%= render_partial 'report_footer' %> 59 | -------------------------------------------------------------------------------- /spec/metric_fu/formatter/yaml_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.formatter_require { "yaml" } 3 | 4 | describe MetricFu::Formatter::YAML do 5 | before do 6 | setup_fs 7 | 8 | config = MetricFu.configuration 9 | 10 | if config.mri? 11 | @metric1 = :cane 12 | else 13 | @metric1 = :stats 14 | config.templates_configuration do |c| 15 | c.syntax_highlighting = false 16 | end 17 | end 18 | allow(MetricFu::Metric.get_metric(@metric1)).to receive(:run_external).and_return("") 19 | @metric2 = :hotspots 20 | MetricFu.result.add(@metric1) 21 | MetricFu.result.add(@metric2) 22 | end 23 | 24 | context "In general" do 25 | it "creates a report yaml file" do 26 | expect { 27 | MetricFu::Formatter::YAML.new.finish 28 | }.to create_file("#{directory('base_directory')}/report.yml") 29 | end 30 | end 31 | 32 | context "given a custom output file" do 33 | before do 34 | @output = "customreport.yml" 35 | end 36 | 37 | it "creates a report yaml file to the custom output path" do 38 | expect { 39 | MetricFu::Formatter::YAML.new(output: @output).finish 40 | }.to create_file("#{directory('base_directory')}/customreport.yml") 41 | end 42 | end 43 | 44 | context "given a custom output stream" do 45 | before do 46 | @output = $stdout 47 | end 48 | 49 | it "creates a report yaml in the custom stream" do 50 | out = MetricFu::Utility.capture_output { 51 | MetricFu::Formatter::YAML.new(output: @output).finish 52 | } 53 | expect(out).to include ":#{@metric1}:" 54 | expect(out).to include ":#{@metric2}:" 55 | end 56 | end 57 | 58 | after do 59 | cleanup_fs 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/layout.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | metrics 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 |
31 | <%= @html %> 32 |
33 | 34 | <% graph_engine = MetricFu.configuration.graph_engine.to_s %> 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by `rubocop --auto-gen-config` 2 | # on 2015-01-27 12:05:28 -0600 using RuboCop version 0.28.0. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the offenses are removed from the code base. 5 | # Note that changes in the inspected code, or installation of new 6 | # versions of RuboCop, may require this file to be generated again. 7 | 8 | # Offense count: 2 9 | # Configuration parameters: AlignWith, SupportedStyles. 10 | Lint/EndAlignment: 11 | Enabled: false 12 | 13 | # Offense count: 2 14 | Lint/EnsureReturn: 15 | Enabled: false 16 | 17 | # Offense count: 1 18 | Lint/RescueException: 19 | Enabled: false 20 | 21 | # Offense count: 10 22 | Lint/ShadowingOuterLocalVariable: 23 | Enabled: false 24 | 25 | # Offense count: 1 26 | Lint/UselessAccessModifier: 27 | Enabled: false 28 | 29 | # Offense count: 27 30 | Lint/UselessAssignment: 31 | Enabled: false 32 | 33 | # Offense count: 44 34 | Metrics/AbcSize: 35 | Max: 56 36 | 37 | # Offense count: 393 38 | # Configuration parameters: AllowURI, URISchemes. 39 | Metrics/LineLength: 40 | Max: 709 41 | 42 | # Offense count: 3 43 | Metrics/PerceivedComplexity: 44 | Max: 13 45 | 46 | # Offense count: 4 47 | # Cop supports --auto-correct. 48 | # Configuration parameters: EnforcedStyle, SupportedStyles. 49 | Style/AndOr: 50 | Enabled: false 51 | 52 | # Offense count: 2 53 | Style/EmptyElse: 54 | Enabled: false 55 | 56 | # Offense count: 2 57 | # Configuration parameters: NamePrefix, NamePrefixBlacklist. 58 | Style/PredicateName: 59 | Enabled: false 60 | 61 | # Offense count: 1 62 | Style/RescueModifier: 63 | Enabled: false 64 | 65 | # Offense count: 4 66 | # Cop supports --auto-correct. 67 | # Configuration parameters: AllowAsExpressionSeparator. 68 | Style/Semicolon: 69 | Enabled: false 70 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/flay/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "flay/grapher" } 3 | 4 | describe FlayGrapher do 5 | before :each do 6 | @flay_grapher = MetricFu::FlayGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to flay_score and labels" do 11 | expect(@flay_grapher).to respond_to(:flay_score) 12 | expect(@flay_grapher).to respond_to(:labels) 13 | end 14 | 15 | describe "responding to #initialize" do 16 | it "should initialise flay_score and labels" do 17 | expect(@flay_grapher.flay_score).to eq([]) 18 | expect(@flay_grapher.labels).to eq({}) 19 | end 20 | end 21 | 22 | describe "responding to #get_metrics" do 23 | context "when metrics were not generated" do 24 | before(:each) do 25 | @metrics = FIXTURE.load_metric("metric_missing.yml") 26 | @date = "1/2" 27 | end 28 | 29 | it "should not push to flay_score" do 30 | expect(@flay_grapher.flay_score).not_to receive(:push) 31 | @flay_grapher.get_metrics(@metrics, @date) 32 | end 33 | 34 | it "should not update labels with the date" do 35 | expect(@flay_grapher.labels).not_to receive(:update) 36 | @flay_grapher.get_metrics(@metrics, @date) 37 | end 38 | end 39 | 40 | context "when metrics have been generated" do 41 | before(:each) do 42 | @metrics = FIXTURE.load_metric("20090630.yml") 43 | @date = "1/2" 44 | end 45 | 46 | it "should push to flay_score" do 47 | expect(@flay_grapher.flay_score).to receive(:push).with(476) 48 | @flay_grapher.get_metrics(@metrics, @date) 49 | end 50 | 51 | it "should update labels with the date" do 52 | expect(@flay_grapher.labels).to receive(:update).with(0 => "1/2") 53 | @flay_grapher.get_metrics(@metrics, @date) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rcov/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "rcov/grapher" } 3 | 4 | describe RcovGrapher do 5 | before :each do 6 | @rcov_grapher = MetricFu::RcovGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to rcov_percent and labels" do 11 | expect(@rcov_grapher).to respond_to(:rcov_percent) 12 | expect(@rcov_grapher).to respond_to(:labels) 13 | end 14 | 15 | describe "responding to #initialize" do 16 | it "should initialise rcov_percent and labels" do 17 | expect(@rcov_grapher.rcov_percent).to eq([]) 18 | expect(@rcov_grapher.labels).to eq({}) 19 | end 20 | end 21 | 22 | describe "responding to #get_metrics" do 23 | context "when metrics were not generated" do 24 | before(:each) do 25 | @metrics = FIXTURE.load_metric("metric_missing.yml") 26 | @date = "1/2" 27 | end 28 | 29 | it "should not push to rcov_percent" do 30 | expect(@rcov_grapher.rcov_percent).not_to receive(:push) 31 | @rcov_grapher.get_metrics(@metrics, @date) 32 | end 33 | 34 | it "should not update labels with the date" do 35 | expect(@rcov_grapher.labels).not_to receive(:update) 36 | @rcov_grapher.get_metrics(@metrics, @date) 37 | end 38 | end 39 | 40 | context "when metrics have been generated" do 41 | before(:each) do 42 | @metrics = FIXTURE.load_metric("20090630.yml") 43 | @date = "1/2" 44 | end 45 | 46 | it "should push to rcov_percent" do 47 | expect(@rcov_grapher.rcov_percent).to receive(:push).with(49.6) 48 | @rcov_grapher.get_metrics(@metrics, @date) 49 | end 50 | 51 | it "should update labels with the date" do 52 | expect(@rcov_grapher.labels).to receive(:update).with(0 => "1/2") 53 | @rcov_grapher.get_metrics(@metrics, @date) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/roodi/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "roodi/grapher" } 3 | 4 | describe RoodiGrapher do 5 | before :each do 6 | @roodi_grapher = MetricFu::RoodiGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to roodi_count and labels" do 11 | expect(@roodi_grapher).to respond_to(:roodi_count) 12 | expect(@roodi_grapher).to respond_to(:labels) 13 | end 14 | 15 | describe "responding to #initialize" do 16 | it "should initialise roodi_count and labels" do 17 | expect(@roodi_grapher.roodi_count).to eq([]) 18 | expect(@roodi_grapher.labels).to eq({}) 19 | end 20 | end 21 | 22 | describe "responding to #get_metrics" do 23 | context "when metrics were not generated" do 24 | before(:each) do 25 | @metrics = FIXTURE.load_metric("metric_missing.yml") 26 | @date = "1/2" 27 | end 28 | 29 | it "should not push to roodi_count" do 30 | expect(@roodi_grapher.roodi_count).not_to receive(:push) 31 | @roodi_grapher.get_metrics(@metrics, @date) 32 | end 33 | 34 | it "should not update labels with the date" do 35 | expect(@roodi_grapher.labels).not_to receive(:update) 36 | @roodi_grapher.get_metrics(@metrics, @date) 37 | end 38 | end 39 | 40 | context "when metrics have been generated" do 41 | before(:each) do 42 | @metrics = FIXTURE.load_metric("20090630.yml") 43 | @date = "1/2" 44 | end 45 | 46 | it "should push to roodi_count" do 47 | expect(@roodi_grapher.roodi_count).to receive(:push).with(13) 48 | @roodi_grapher.get_metrics(@metrics, @date) 49 | end 50 | 51 | it "should update labels with the date" do 52 | expect(@roodi_grapher.labels).to receive(:update).with(0 => "1/2") 53 | @roodi_grapher.get_metrics(@metrics, @date) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/metric_fu/reporting/result.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | # MetricFu.result memoizes access to a Result object, that will be 3 | # used throughout the lifecycle of the MetricFu app. 4 | def self.result 5 | @result ||= Result.new 6 | end 7 | 8 | # = Result 9 | # 10 | # The Result class is responsible for one thing: 11 | # 12 | # It tracks the results generated by each metric used in this test run. 13 | class Result 14 | # Renders the result of the result_hash into a yaml serialization 15 | # ready for writing out to a file. 16 | # 17 | # @return YAML 18 | # A YAML object containing the results of the result generation 19 | # process 20 | def as_yaml 21 | result_hash.to_yaml 22 | end 23 | 24 | def per_file_data 25 | @per_file_data ||= Hash.new do |hash, filename| 26 | hash[filename] = Hash.new do |h, line| 27 | h[line] = Array.new 28 | end 29 | end 30 | end 31 | 32 | def result_hash #:nodoc: 33 | @result_hash ||= {} 34 | end 35 | 36 | # Adds a hash from a passed result, produced by one of the Generator 37 | # classes to the aggregate result_hash managed by this hash. 38 | # 39 | # @param result_type Hash 40 | # The hash to add to the aggregate result_hash 41 | def add(result_type) 42 | mf_debug "result requested #{result_type}" 43 | metric_options = metric_options_for_result_type(result_type) 44 | generator_class = MetricFu::Generator.get_generator(result_type) 45 | mf_debug "result class found #{generator_class}" 46 | generator = generator_class.new(metric_options) 47 | 48 | result_hash.merge!(generator.generate_result) 49 | 50 | generator.per_file_info(per_file_data) if generator.respond_to?(:per_file_info) 51 | end 52 | 53 | private 54 | 55 | def metric_options_for_result_type(result_type) 56 | MetricFu::Metric.get_metric(result_type).run_options 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/metric_fu/templates/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for templates" do 5 | it_behaves_like "configured" do 6 | describe "when there is no CC_BUILD_ARTIFACTS environment variable" do 7 | before(:each) do 8 | ENV["CC_BUILD_ARTIFACTS"] = nil 9 | get_new_config 10 | end 11 | 12 | it "should set @template_directory to the lib/templates relative " + "to @metric_fu_root_directory" do 13 | expected_template_dir = MetricFu.root.join("lib", "templates").to_s 14 | expect(template_directory).to eq(expected_template_dir) 15 | end 16 | 17 | it "should set @template_class to MetricFu::Templates::MetricsTemplate by default" do 18 | expect(template_class).to eq(MetricFu::Templates::MetricsTemplate) 19 | end 20 | 21 | describe "when a templates configuration is given" do 22 | before do 23 | class DummyTemplate; end 24 | 25 | @config.templates_configuration do |config| 26 | config.template_class = DummyTemplate 27 | config.link_prefix = "http:/" 28 | config.syntax_highlighting = false 29 | config.darwin_txmt_protocol_no_thanks = false 30 | end 31 | end 32 | 33 | it "should set given template_class" do 34 | expect(template_class).to eq(DummyTemplate) 35 | end 36 | 37 | it "should set given link_prefix" do 38 | expect(MetricFu::Formatter::Templates.option("link_prefix")).to eq("http:/") 39 | end 40 | 41 | it "should set given darwin_txmt_protocol_no_thanks" do 42 | expect(MetricFu::Formatter::Templates.option("darwin_txmt_protocol_no_thanks")).to be_falsey 43 | end 44 | 45 | it "should set given syntax_highlighting" do 46 | expect(MetricFu::Formatter::Templates.option("syntax_highlighting")).to be_falsey 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flog/report.html.erb: -------------------------------------------------------------------------------- 1 |

Flog Results

2 |

Flog measures code complexity.

3 | 4 | <%= render_partial 'graph', {:graph_name => 'flog'} %> 5 | 6 |

Total Flog score for all methods: <%= round_to_tenths @flog[:total]%>

7 |

Average Flog score for all methods: <%= round_to_tenths @flog[:average]%>

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @flog[:method_containers].each do |method_container| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 |
FileTotal scoreMethodsAverage scoreHighest score
<%= method_container[:path] %><%= round_to_tenths method_container[:total_score] %><%= method_container[:methods].size %><%= round_to_tenths method_container[:average_score] %><%= round_to_tenths method_container[:highest_score] %>
27 | 28 | <% @flog[:method_containers].each do |method_container| %> 29 |

<%= link_to_filename(method_container[:path]) %>

30 | 31 | <% method_container[:methods].each do |full_method_name, method_info| %> 32 | <% path, line = method_info[:path].split(":") if method_info[:path] %> 33 |

<%= link_to_filename(path, line, full_method_name) %>

34 |

Total Score: <%= round_to_tenths method_info[:score]%>

35 | 36 | 37 | 38 | 39 | 40 | <% method_info[:operators].each do |operator, score| %> 41 | 42 | 43 | 44 | 45 | <% end %> 46 |
ScoreOperator
<%= round_to_tenths score %><%= operator %>
47 | <% end %> 48 | <% end %> 49 | 50 | <%= render_partial 'report_footer' %> 51 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/rcov/generator.rb: -------------------------------------------------------------------------------- 1 | MetricFu.lib_require { "utility" } 2 | MetricFu.lib_require { "calculate" } 3 | MetricFu.data_structures_require { "line_numbers" } 4 | require_relative "rcov_format_coverage" 5 | require_relative "rcov_line" 6 | require_relative "external_client" 7 | 8 | module MetricFu 9 | class RcovGenerator < MetricFu::Generator 10 | def self.metric 11 | :rcov 12 | end 13 | 14 | def emit 15 | if run_rcov? 16 | mf_debug "** Running the specs/tests in the [#{options[:environment]}] environment" 17 | mf_debug "** #{command}" 18 | `#{command}` 19 | end 20 | end 21 | 22 | def command 23 | @command ||= default_command 24 | end 25 | 26 | def command=(command) 27 | @command = command 28 | end 29 | 30 | def reset_output_location 31 | MetricFu::Utility.rm_rf(metric_directory, verbose: false) 32 | MetricFu::Utility.mkdir_p(metric_directory) 33 | end 34 | 35 | def default_command 36 | require "rake" 37 | reset_output_location 38 | test_files = FileList[*options[:test_files]].join(" ") 39 | rcov_opts = options[:rcov_opts].join(" ") 40 | %(RAILS_ENV=#{options[:environment]} rcov #{test_files} #{rcov_opts} >> #{default_output_file}) 41 | end 42 | 43 | def analyze 44 | rcov_text = load_output 45 | formatter = MetricFu::RCovFormatCoverage.new(rcov_text) 46 | @rcov = formatter.to_h 47 | end 48 | 49 | def to_h 50 | { 51 | rcov: @rcov 52 | } 53 | end 54 | 55 | private 56 | 57 | # We never run rcov anymore 58 | def run_rcov? 59 | false 60 | end 61 | 62 | def load_output 63 | MetricFu::RCovTestCoverageClient.new(output_file).load 64 | end 65 | 66 | def output_file 67 | options.fetch(:external) 68 | end 69 | 70 | # Only used if run_rcov? is true 71 | def default_output_file 72 | output_file || File.join(metric_directory, "rcov.txt") 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/metric_fu/templates/css/buttons.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | buttons.css 4 | * Gives you some great CSS-only buttons. 5 | 6 | Created by Kevin Hale [particletree.com] 7 | * particletree.com/features/rediscovering-the-button-element 8 | 9 | See Readme.txt in this folder for instructions. 10 | 11 | -------------------------------------------------------------- */ 12 | 13 | button { 14 | display:block; 15 | float:left; 16 | margin:0 0.583em 0.667em 0; 17 | padding:5px 10px 5px 7px; /* Links */ 18 | 19 | border:1px solid #dedede; 20 | border-top:1px solid #eee; 21 | border-left:1px solid #eee; 22 | 23 | background-color:#f5f5f5; 24 | font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif; 25 | font-size:100%; 26 | line-height:130%; 27 | text-decoration:none; 28 | font-weight:bold; 29 | color:#565656; 30 | cursor:pointer; 31 | } 32 | button { 33 | width:auto; 34 | overflow:visible; 35 | padding:4px 10px 3px 7px; /* IE6 */ 36 | } 37 | button[type] { 38 | padding:4px 10px 4px 7px; /* Firefox */ 39 | line-height:17px; /* Safari */ 40 | } 41 | *:first-child+html button[type] { 42 | padding:4px 10px 3px 7px; /* IE7 */ 43 | } 44 | button img { 45 | margin:0 3px -3px 0 !important; 46 | padding:0; 47 | border:none; 48 | width:16px; 49 | height:16px; 50 | float:none; 51 | } 52 | 53 | 54 | /* Button colors 55 | -------------------------------------------------------------- */ 56 | 57 | /* Standard */ 58 | button:hover { 59 | background-color:#dff4ff; 60 | border:1px solid #c2e1ef; 61 | color:#336699; 62 | } 63 | 64 | /* Positive */ 65 | body .positive { 66 | color:#529214; 67 | } 68 | button.positive:hover { 69 | background-color:#E6EFC2; 70 | border:1px solid #C6D880; 71 | color:#529214; 72 | } 73 | 74 | /* Negative */ 75 | body .negative { 76 | color:#d12f19; 77 | } 78 | button.negative:hover { 79 | background:#fbe3e4; 80 | border:1px solid #fbc2c4; 81 | color:#d12f19; 82 | } 83 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rails_best_practices/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "shared/configured" 3 | 4 | describe MetricFu::Configuration, "for rails_best_practices" do 5 | it_behaves_like "configured" do 6 | describe "if #rails? is true " do 7 | before(:each) do 8 | @config = MetricFu.configuration 9 | allow(@config).to receive(:rails?).and_return(true) 10 | @config.reset 11 | MetricFu.configure 12 | %w(rails_best_practices).each do |metric| 13 | load_metric metric 14 | end 15 | end 16 | 17 | describe "#set_graphs " do 18 | it "should set the graphs to include rails_best_practices" do 19 | expect(MetricFu::Metric.get_metric(:rails_best_practices).has_graph?).to be_truthy 20 | end 21 | end 22 | 23 | it "should default @rails_best_practices to { :silent => true }" do 24 | load_metric "rails_best_practices" 25 | rbp = MetricFu::MetricRailsBestPractices.new 26 | expect(rbp.run_options).to eq(exclude: [], silent: true) 27 | end 28 | 29 | it "can configure @rails_best_practices 'exclude' using the sugar" do 30 | load_metric "rails_best_practices" 31 | rbp = MetricFu::Metric.get_metric(:rails_best_practices) 32 | rbp.exclude = ["config/chef"] 33 | expect(rbp.run_options).to eq( 34 | exclude: ["config/chef"], 35 | silent: true 36 | ) 37 | 38 | end 39 | end 40 | 41 | describe "if #rails? is false " do 42 | before(:each) do 43 | get_new_config 44 | allow(@config).to receive(:rails?).and_return(false) 45 | %w(rails_best_practices).each do |metric| 46 | load_metric metric 47 | end 48 | end 49 | 50 | it "should set the registered code_dirs to ['lib']" do 51 | expect(directory("code_dirs")).to eq(["lib"]) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/metric_fu/cli/helper.rb: -------------------------------------------------------------------------------- 1 | require "metric_fu" 2 | require "metric_fu/cli/parser" 3 | MetricFu.lib_require { "run" } 4 | # see https://github.com/grosser/pru/blob/master/bin/pru 5 | module MetricFu 6 | module Cli 7 | def self.immediate_shutdown! 8 | exit(1) 9 | end 10 | def self.complete! 11 | exit(0) 12 | end 13 | class Helper 14 | def initialize 15 | @metric_fu = MetricFu::Run.new 16 | end 17 | 18 | def run(options = {}) 19 | @metric_fu.run(options) 20 | complete 21 | end 22 | 23 | def version 24 | MetricFu::VERSION 25 | end 26 | 27 | def shutdown 28 | out "\nShutting down. Bye" 29 | MetricFu::Cli.immediate_shutdown! 30 | end 31 | 32 | def banner 33 | "MetricFu: A Fistful of code metrics" 34 | end 35 | 36 | def usage 37 | <<-EOS 38 | #{banner} 39 | Use --help for help 40 | EOS 41 | end 42 | 43 | def executable_name 44 | "metric_fu" 45 | end 46 | 47 | def metrics 48 | MetricFu::Metric.metrics.map(&:name).sort_by(&:to_s) 49 | end 50 | 51 | def process_options(argv = []) 52 | options = MetricFu::Cli::MicroOptParse::Parser.new do |p| 53 | p.banner = banner 54 | p.version = version 55 | p.option :run, "Run all metrics with defaults", default: true 56 | metrics.each do |metric| 57 | p.option metric.to_sym, "Enables or disables #{metric}", default: true # , :value_in_set => [true, false] 58 | end 59 | p.option :open, "Open report in browser (if supported by formatter)", default: true 60 | end.process!(argv) 61 | options 62 | end 63 | 64 | private 65 | 66 | def out(text) 67 | STDOUT.puts text 68 | end 69 | 70 | def error(text) 71 | STDERR.puts text 72 | end 73 | 74 | def complete 75 | out "all done" 76 | MetricFu::Cli.complete! 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/metric_fu/utility.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "fileutils" 3 | module MetricFu 4 | module Utility 5 | module_function 6 | 7 | ESCAPE_CODES_PATTERN = Regexp.new('\e\[(?:\d;)?\d{1,2}m') 8 | 9 | # Removes non-ASCII characters 10 | def clean_ascii_text(text) 11 | if text.respond_to?(:encode) 12 | # avoids invalid multi-byte escape error 13 | ascii_text = text.encode("ASCII", invalid: :replace, undef: :replace, replace: "") 14 | # see http://www.ruby-forum.com/topic/183413 15 | pattern = Regexp.new('[\x80-\xff]', nil, "n") 16 | ascii_text.gsub(pattern, "") 17 | else 18 | text 19 | end 20 | end 21 | 22 | def stringify_keys(hash) 23 | result = {} 24 | hash.each do |key, value| 25 | result[key.to_s] = value 26 | end 27 | result 28 | end 29 | 30 | def strip_escape_codes(text) 31 | text.gsub(ESCAPE_CODES_PATTERN, "") 32 | end 33 | 34 | def rm_rf(*args) 35 | FileUtils.rm_rf(*args) 36 | end 37 | 38 | def mkdir_p(dir, **args) 39 | FileUtils.mkdir_p(dir, **args) 40 | end 41 | 42 | def glob(*args) 43 | Dir.glob(*args) 44 | end 45 | 46 | def load_yaml(file) 47 | YAML.load_file(file) 48 | end 49 | 50 | def binread(file) 51 | File.binread(file) 52 | end 53 | 54 | # From episode 029 of Ruby Tapas by Avdi 55 | # https://rubytapas.dpdcart.com/subscriber/post?id=88 56 | def capture_output(stream = STDOUT, &_block) 57 | old_stdout = stream.clone 58 | pipe_r, pipe_w = IO.pipe 59 | pipe_r.sync = true 60 | output = "" 61 | reader = Thread.new do 62 | begin 63 | loop do 64 | output << pipe_r.readpartial(1024) 65 | end 66 | rescue EOFError 67 | end 68 | end 69 | stream.reopen(pipe_w) 70 | yield 71 | ensure 72 | stream.reopen(old_stdout) 73 | pipe_w.close 74 | reader.join 75 | pipe_r.close 76 | return output 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/saikuro/report.html.erb: -------------------------------------------------------------------------------- 1 |

Saikuro Results

2 |

Saikuro analyzes ruby code for cyclomatic complexity.

3 | 4 |

Analyzed Methods

5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @saikuro[:methods].each do |method| %> 12 | 13 | 14 | 15 | 16 | 17 | <% end %> 18 |
Method NameComplexity# Lines
<%= method[:name] %><%= method[:complexity] %><%= method[:lines] %>
19 | 20 | 21 | 22 |

Analyzed Classes

23 | 24 | 25 | 26 | 27 | 28 | 29 | <% @saikuro[:classes].each do |klass| %> 30 | 31 | 32 | 33 | 34 | 35 | <% end %> 36 |
Class NameComplexity# Lines
<%= klass[:name] %><%= klass[:complexity] %><%= klass[:lines] %>
37 | 38 | 39 |

Analyzed Files

40 | <% @saikuro[:files].each do |file| %> 41 | <% file[:classes].each do |klass| %> 42 | <% if !klass[:methods].empty? %> 43 |

<%=link_to_filename(file[:filename])%>

44 |

Class : <%= klass[:class_name] %>

45 |
Total complexity : <%= klass[:complexity] %>
46 |
Total lines : <%= klass[:lines] %>
47 | 48 | 49 | 50 | 51 | 52 | 53 | <% klass[:methods].each do |method| %> 54 | 55 | 58 | 61 | 64 | 65 | <% end %> 66 |
MethodComplexity# Lines
56 | <%= method[:name] %> 57 | 59 | <%= method[:complexity] %> 60 | 62 | <%= method[:lines] %> 63 |
67 | <% end %> 68 | <% end %> 69 | <% end %> 70 | 71 | <%= render_partial 'report_footer' %> 72 | -------------------------------------------------------------------------------- /lib/metric_fu/constantize.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | module Constantize 3 | # Copied from ActiveSupport and modified so as not to introduce a dependency. 4 | # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L220 5 | def constantize(camel_cased_word) 6 | tries ||= 2 7 | names = camel_cased_word.split("::") 8 | names.shift if names.empty? || names.first.empty? 9 | 10 | names.inject(Object) do |constant, name| 11 | if constant == Object 12 | constant.const_get(name) 13 | else 14 | candidate = constant.const_get(name) 15 | next candidate if constant.const_defined?(name, false) 16 | next candidate unless Object.const_defined?(name) 17 | 18 | # Go down the ancestors to check it it's owned 19 | # directly before we reach Object or the end of ancestors. 20 | constant = constant.ancestors.inject do |const, ancestor| 21 | break const if ancestor == Object 22 | break ancestor if ancestor.const_defined?(name, false) 23 | const 24 | end 25 | 26 | # owner is in Object, so raise 27 | constant.const_get(name, false) 28 | end 29 | end 30 | rescue NameError 31 | # May need to attempt to require the file and try again. 32 | begin 33 | require underscore(camel_cased_word) 34 | rescue LoadError => e 35 | mf_log e.message 36 | end 37 | 38 | if (tries -= 1).zero? 39 | raise 40 | else 41 | retry 42 | end 43 | end 44 | 45 | # Copied from active_support 46 | # https://github.com/rails/rails/blob/51cd6bb829c418c5fbf75de1dfbb177233b1b154/activesupport/lib/active_support/inflector/methods.rb#L88 47 | def underscore(camel_cased_word) 48 | word = camel_cased_word.to_s.dup 49 | word.gsub!(/::/, "/") 50 | word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') 51 | word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') 52 | word.tr!("-", "_") 53 | word.downcase! 54 | word 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/metric_fu/tasks/metric_fu.rake: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "metric_fu/run" 3 | namespace :metrics do 4 | def options_tip(task_name) 5 | "with options, for example: rake metrics:#{task_name}['cane: {abc_max: 81}']" 6 | end 7 | desc "Generate all metrics reports, or #{options_tip('all')}" 8 | task :all, [:options] do |_t, args| 9 | MetricFu.run(process_options(args.options)) 10 | end 11 | 12 | desc "Run only specified ;-separated metrics, for example, metrics:only[cane;flog] or #{options_tip('only')}" 13 | task :only, [:metrics, :options] do |_t, args| 14 | requested_metrics = args.metrics.to_s.split(";").map(&:strip) 15 | enabled_metrics = MetricFu::Metric.enabled_metrics.map(&:name) 16 | metrics_to_run = enabled_metrics.select { |metric| requested_metrics.include?(metric.to_s) } 17 | MetricFu.run_only(metrics_to_run, process_options(args.options)) 18 | end 19 | 20 | MetricFu::Metric.enabled_metrics.each do |metric| 21 | name = metric.name 22 | desc "Generate report for #{name}, or #{options_tip('cane')}" 23 | task name, [:options] do |_t, args| 24 | MetricFu.run_only(name, process_options(args.options)) 25 | end 26 | end 27 | 28 | private 29 | 30 | # from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/keys.rb 31 | class Hash 32 | # Destructively, recursively convert all keys to symbols, as long as they respond 33 | # to +to_sym+. 34 | def recursively_symbolize_keys! 35 | keys.each do |key| 36 | value = delete(key) 37 | new_key = key.intern # rescue 38 | self[new_key] = (value.is_a?(Hash) ? value.dup.recursively_symbolize_keys! : value) 39 | end 40 | self 41 | end 42 | end 43 | 44 | def process_options(options) 45 | return {} if options.nil? or options.empty? 46 | options = YAML.load(options) 47 | if options.is_a?(Hash) 48 | p "Got options #{options.recursively_symbolize_keys!.inspect}" 49 | options 50 | else 51 | raise "Invalid options #{options.inspect}, is a #{options.class}, should be a Hash" 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/analysis/ranked_problem_location.rb: -------------------------------------------------------------------------------- 1 | MetricFu.lib_require { "errors/analysis_error" } 2 | MetricFu.metrics_require { "hotspots/analysis/problems" } 3 | module MetricFu 4 | class HotspotRankedProblemLocation 5 | MetricFu.data_structures_require { "location" } 6 | attr_reader :sub_table, :granularity 7 | def initialize(sub_table, granularity) 8 | @sub_table = sub_table 9 | @granularity = granularity 10 | end 11 | 12 | def to_hash 13 | { 14 | "location" => location.to_hash, 15 | "details" => MetricFu::Utility.stringify_keys(problems), 16 | } 17 | end 18 | 19 | # @todo redo as item,value, options = {} 20 | # Note that the other option for 'details' is :detailed (this isn't 21 | # at all clear from this method itself 22 | def problems 23 | @problems ||= MetricFu::HotspotProblems.new(sub_table).problems 24 | end 25 | 26 | def location 27 | @location ||= case granularity 28 | when :class then class_location 29 | when :method then method_location 30 | when :file then file_location 31 | else raise ArgumentError, "Item must be :class, :method, or :file" 32 | end 33 | end 34 | 35 | def file_path 36 | first_row.file_path 37 | end 38 | 39 | def class_name 40 | first_row.class_name 41 | end 42 | 43 | def method_name 44 | first_row.method_name 45 | end 46 | 47 | def file_location 48 | MetricFu::Location.get(file_path, nil, nil) 49 | end 50 | 51 | def method_location 52 | MetricFu::Location.get(file_path, class_name, method_name) 53 | end 54 | 55 | def class_location 56 | MetricFu::Location.get(file_path, class_name, nil) 57 | end 58 | 59 | def first_row 60 | assert_sub_table_has_data 61 | @first_row ||= sub_table[0] 62 | end 63 | 64 | def assert_sub_table_has_data 65 | if (sub_table.length == 0) 66 | raise MetricFu::AnalysisError, "The #{item} '#{value}' does not have any rows in the analysis table" 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/hotspots/generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "hotspots/generator" } 3 | 4 | describe MetricFu::HotspotsGenerator do 5 | describe "analyze method" do 6 | it "should be empty on error" do 7 | hotspots = MetricFu::HotspotsGenerator.new 8 | hotspots.instance_variable_set(:@analyzer, nil) 9 | result = hotspots.analyze 10 | expect(result).to eq(files: [], classes: [], methods: []) 11 | end 12 | 13 | it "should put the changes into a hash" do 14 | hotspots = MetricFu::HotspotsGenerator.new 15 | hotspots.analyze 16 | result = hotspots.to_h[:hotspots] 17 | expected = HOTSPOT_DATA["generator_analysis.yml"] 18 | # ensure expected granularities 19 | expect(result.keys).to eq(expected.keys) 20 | 21 | # for each granularity's location details 22 | result.each do |granularity, location_details| 23 | # map 2d array for this granularity of [details, location] 24 | expected_result = expected.fetch(granularity).map { |ld| [ld.fetch("details"), ld.fetch("location")] } 25 | # verify all the location details for this granularity match elements of expected_result 26 | location_details.each do |location_detail| 27 | location = location_detail.fetch("location") 28 | details = location_detail.fetch("details") 29 | # get the location_detail array where the where the locations (second element) match 30 | expected_location_details = expected_result.rassoc(location) 31 | # get the details (first element) from the expected location_details array 32 | expected_details = expected_location_details[0] 33 | expect(details).to eq(expected_details) 34 | end 35 | end 36 | end 37 | 38 | # really testing the output of analyzed_problems#worst_items 39 | it "should return the worst item granularities: files, classes, methods" do 40 | hotspots = MetricFu::HotspotsGenerator.new 41 | yaml = HOTSPOT_DATA["generator.yml"] 42 | analyzer = HotspotAnalyzer.new(yaml) 43 | expect(hotspots.analyze.keys).to eq([:files, :classes, :methods]) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/metric_fu/reporting/graphs/graph.rb: -------------------------------------------------------------------------------- 1 | module MetricFu 2 | def self.graph 3 | @graph ||= Graph.new 4 | end 5 | 6 | class Graph 7 | attr_accessor :graphers 8 | 9 | def initialize 10 | self.graphers = [] 11 | end 12 | 13 | def add(metric_name, _graph_engine, output_directory = MetricFu::Io::FileSystem.directory("output_directory")) 14 | grapher = MetricFu::Grapher.get_grapher(metric_name). 15 | new.tap { |g| g.output_directory = output_directory } 16 | graphers.push grapher 17 | rescue NameError => e 18 | mf_log "#{e.message} called in MetricFu::Graph.add with #{graph_type}" 19 | end 20 | 21 | def generate 22 | return if graphers.empty? 23 | mf_log "Generating graphs" 24 | generate_graphs_for_files 25 | graph! 26 | rescue NameError => e 27 | mf_log "#{e.message} called in MetricFu::Graph generate" 28 | end 29 | 30 | private 31 | 32 | def metric_files 33 | MetricFu::Utility.glob( 34 | File.join(MetricFu::Io::FileSystem.directory("data_directory"), "*.yml") 35 | ).sort 36 | end 37 | 38 | def generate_graphs_for_files 39 | metric_files.each do |metric_file| 40 | generate_graphs_for_file(metric_file) 41 | end 42 | end 43 | 44 | def generate_graphs_for_file(metric_file) 45 | mf_log "Generating graphs for #{metric_file}" 46 | date_parts = year_month_day_from_filename(metric_file) 47 | metrics = MetricFu::Utility.load_yaml(metric_file) 48 | 49 | build_graph(metrics, "#{date_parts[:m]}/#{date_parts[:d]}") 50 | rescue NameError => e 51 | mf_log "#{e.message} called in MetricFu::Graph.generate with #{metric_file}" 52 | end 53 | 54 | def build_graph(metrics, sortable_prefix) 55 | graphers.each do |grapher| 56 | grapher.get_metrics(metrics, sortable_prefix) 57 | end 58 | end 59 | 60 | def graph! 61 | graphers.each(&:graph!) 62 | end 63 | 64 | def year_month_day_from_filename(path_to_file_with_date) 65 | date = path_to_file_with_date.match(/\/(\d+).yml$/)[1] 66 | { y: date[0..3].to_i, m: date[4..5].to_i, d: date[6..7].to_i } 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/hotspot_analyzer.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | MetricFu.metrics_require do 3 | [ 4 | "hotspots/hotspot", 5 | "hotspots/analysis/analyzer_tables", 6 | "hotspots/analysis/analyzed_problems", 7 | "hotspots/analysis/rankings" 8 | ] 9 | end 10 | 11 | module MetricFu 12 | class HotspotAnalyzer 13 | COMMON_COLUMNS = %w{metric} 14 | GRANULARITIES = %w{file_path class_name method_name} 15 | 16 | def tool_analyzers 17 | MetricFu::Hotspot.analyzers 18 | end 19 | 20 | def initialize(result_hash) 21 | # we can't depend on the result 22 | # returning a parsed yaml file as a hash? 23 | result_hash = YAML::load(result_hash) if result_hash.is_a?(String) 24 | setup(result_hash) 25 | end 26 | 27 | def hotspots 28 | analyzed_problems.worst_items 29 | end 30 | 31 | def analyzed_problems 32 | @analyzed_problems = MetricFu::HotspotAnalyzedProblems.new(@rankings, @analyzer_tables) 33 | end 34 | 35 | private 36 | 37 | # TODO clarify each of these steps in setup 38 | # extract into its own method 39 | # remove unnecessary constants, 40 | # turn into methods 41 | def setup(result_hash) 42 | # TODO There is likely a clash that will happen between 43 | # column names eventually. We should probably auto-prefix 44 | # them (e.g. "roodi_problem") 45 | analyzer_columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map(&:columns).flatten 46 | # though the tool_analyzers aren't returned, they are processed in 47 | # various places here, then by the analyzer tables 48 | # then by the rankings 49 | # to ultimately generate the hotspots 50 | @analyzer_tables = MetricFu::AnalyzerTables.new(analyzer_columns) 51 | tool_analyzers.each do |analyzer| 52 | analyzer.generate_records(result_hash[analyzer.name], @analyzer_tables.table) 53 | end 54 | @analyzer_tables.generate_records 55 | @rankings = MetricFu::HotspotRankings.new(@analyzer_tables.tool_tables) 56 | @rankings.calculate_scores(tool_analyzers, GRANULARITIES) 57 | # TODO does it not need to return something here? 58 | analyzed_problems 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/reek/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "reek/grapher" } 3 | 4 | describe ReekGrapher do 5 | before :each do 6 | @reek_grapher = MetricFu::ReekGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to reek_count and labels" do 11 | expect(@reek_grapher).to respond_to(:reek_count) 12 | expect(@reek_grapher).to respond_to(:labels) 13 | end 14 | 15 | describe "responding to #initialize" do 16 | it "should initialise reek_count and labels" do 17 | expect(@reek_grapher.reek_count).to eq({}) 18 | expect(@reek_grapher.labels).to eq({}) 19 | end 20 | end 21 | 22 | describe "responding to #get_metrics" do 23 | context "when metrics were not generated" do 24 | before(:each) do 25 | @metrics = FIXTURE.load_metric("metric_missing.yml") 26 | @date = "1/2" 27 | end 28 | 29 | it "should not set a hash of code smells to reek_count" do 30 | @reek_grapher.get_metrics(@metrics, @date) 31 | expect(@reek_grapher.reek_count).to eq({}) 32 | end 33 | 34 | it "should not update labels with the date" do 35 | expect(@reek_grapher.labels).not_to receive(:update) 36 | @reek_grapher.get_metrics(@metrics, @date) 37 | end 38 | end 39 | 40 | context "when metrics have been generated" do 41 | before(:each) do 42 | @metrics = FIXTURE.load_metric("20090630.yml") 43 | @date = "1/2" 44 | end 45 | 46 | it "should set a hash of code smells to reek_count" do 47 | @reek_grapher.get_metrics(@metrics, @date) 48 | expect(@reek_grapher.reek_count).to eq( 49 | "Uncommunicative Name" => [27], 50 | "Feature Envy" => [20], 51 | "Utility Function" => [15], 52 | "Long Method" => [26], 53 | "Nested Iterators" => [12], 54 | "Control Couple" => [4], 55 | "Duplication" => [48], 56 | "Large Class" => [1] 57 | ) 58 | end 59 | 60 | it "should update labels with the date" do 61 | expect(@reek_grapher.labels).to receive(:update).with(0 => "1/2") 62 | @reek_grapher.get_metrics(@metrics, @date) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/hotspots/report.html.erb: -------------------------------------------------------------------------------- 1 |

Hotspot Results

2 |

Meta analysis of your metrics to find hotspots in your code.

3 |
4 | 5 | <% if @hotspots.nil? || @hotspots.size.zero? %> 6 | No Hotspots were found. 7 | <% else %> 8 | 9 | <% granularities = %w[files classes methods] %> 10 | 11 | 12 | <% granularities.each do |granularity| %> 13 | 15 | <% end %> 16 | 17 | 18 | <% hotspots = [] %> 19 | <% granularities.each_index do |index| %> 20 | <% granularity = granularities[index] %> 21 | <% hotspots << @hotspots[granularity] %> 22 | <% end %> 23 | 24 | <% hotspots_length = 0 %> 25 | <% columns = [0, 1, 2] %> 26 | <% while (hotspots_length < hotspots[0].length || hotspots_length < hotspots[1].length || hotspots_length < hotspots[2].length) do %> 27 | 28 | <% columns.each do |column| %> 29 | <% item = hotspots[column].length >= hotspots_length ? hotspots[column][hotspots_length] : nil %> 30 | <% if item %> 31 | <% location = item.fetch('location') %> 32 | <% file, line = location.fetch('file_name'), location.fetch('line_number') %> 33 | 49 | <% else %> 50 | 51 | <% end %> 52 | <% end %> 53 | <% hotspots_length += 1 %> 54 | 55 | <% end %> 56 | 57 |
14 | <%= granularity.to_s.capitalize %>
34 | 35 | <%= display_location(location) %> 36 | 37 | <% unless per_file_data[file].empty? %> 38 | « 39 | ">annotate 40 | » 41 | <% end %> 42 |

43 | 44 | <% item.fetch('details').each do |metric, info| %> 45 | <%#= metric_link(@stat, metric, h(metric.to_s.capitalize)) + ": " + h(info)%>
46 | <%= "#{metric.to_s.capitalize}: #{info}" %>
47 | <% end %> 48 |
 
58 | <% end %> 59 | 60 | <%= render_partial 'report_footer' %> 61 | -------------------------------------------------------------------------------- /lib/metric_fu/gem_run.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "open3" 3 | require "shellwords" 4 | require "metric_fu" 5 | MetricFu.lib_require { "logger" } 6 | MetricFu.lib_require { "gem_version" } 7 | module MetricFu 8 | class GemRun 9 | attr_reader :output, :gem_name, :library_name, :version, :arguments 10 | def initialize(arguments = {}) 11 | @gem_name = arguments.fetch(:gem_name) 12 | @library_name = arguments.fetch(:metric_name) 13 | @version = arguments.fetch(:version) { MetricFu::GemVersion.for(library_name) } 14 | args = arguments.fetch(:args) 15 | @arguments = args.respond_to?(:scan) ? Shellwords.shellwords(args) : args 16 | @output = "" 17 | @errors = [] 18 | end 19 | 20 | def summary 21 | "RubyGem #{gem_name}, library #{library_name}, version #{version}, arguments #{arguments}" 22 | end 23 | 24 | def run 25 | @output = execute 26 | end 27 | 28 | def execute 29 | mf_debug "Running #{summary}" 30 | captured_output = "" 31 | captured_errors = "" 32 | thread = "" 33 | Open3.popen3("#{library_name}", *arguments) do |_stdin, stdout, stderr, wait_thr| 34 | captured_output << stdout.read.chomp 35 | captured_errors << stderr.read.chomp 36 | thread = wait_thr 37 | end 38 | rescue StandardError => run_error 39 | handle_run_error(run_error) 40 | rescue SystemExit => system_exit 41 | handle_system_exit(system_exit) 42 | ensure 43 | print_errors 44 | return captured_output, captured_errors, thread.value 45 | end 46 | 47 | def handle_run_error(run_error) 48 | @errors << "ERROR: #{run_error.inspect}" 49 | end 50 | 51 | def handle_system_exit(system_exit) 52 | status = system_exit.success? ? "SUCCESS" : "FAILURE" 53 | message = "#{status} with code #{system_exit.status}: " << 54 | "#{system_exit.message}: #{system_exit.backtrace.inspect}" 55 | if status == "SUCCESS" 56 | mf_debug message 57 | else 58 | @errors << message 59 | end 60 | end 61 | 62 | def print_errors 63 | return if @errors.empty? 64 | STDERR.puts "ERRORS running #{summary}" 65 | @errors.each do |error| 66 | STDERR.puts "\t" << error 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rails_best_practices/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "rails_best_practices/grapher" } 3 | 4 | describe RailsBestPracticesGrapher do 5 | before :each do 6 | @stats_grapher = MetricFu::RailsBestPracticesGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to rails_best_practices_count and labels" do 11 | expect(@stats_grapher).to respond_to(:rails_best_practices_count) 12 | expect(@stats_grapher).to respond_to(:labels) 13 | end 14 | 15 | describe "responding to #initialize" do 16 | it "should initialise rails_best_practices_count and labels" do 17 | expect(@stats_grapher.rails_best_practices_count).to eq([]) 18 | expect(@stats_grapher.labels).to eq({}) 19 | end 20 | end 21 | 22 | describe "responding to #get_metrics" do 23 | context "when metrics were not generated" do 24 | before(:each) do 25 | @metrics = FIXTURE.load_metric("metric_missing.yml") 26 | @date = "01022003" 27 | end 28 | 29 | it "should not push to rails_best_practices_count" do 30 | expect(@stats_grapher.rails_best_practices_count).not_to receive(:push) 31 | @stats_grapher.get_metrics(@metrics, @date) 32 | end 33 | 34 | it "should not update labels with the date" do 35 | expect(@stats_grapher.labels).not_to receive(:update) 36 | @stats_grapher.get_metrics(@metrics, @date) 37 | end 38 | end 39 | 40 | context "when metrics have been generated" do 41 | before(:each) do 42 | @metrics = FIXTURE.load_metric("20090630.yml") 43 | @date = "01022003" 44 | end 45 | 46 | it "should push to rails_best_practices_count" do 47 | expect(@stats_grapher.rails_best_practices_count).to receive(:push).with(2) 48 | @stats_grapher.get_metrics(@metrics, @date) 49 | end 50 | 51 | it "should push 0 to rails_best_practices_count when no problems were found" do 52 | expect(@stats_grapher.rails_best_practices_count).to receive(:push).with(0) 53 | @stats_grapher.get_metrics({ rails_best_practices: {} }, @date) 54 | end 55 | 56 | it "should update labels with the date" do 57 | expect(@stats_grapher.labels).to receive(:update).with(0 => "01022003") 58 | @stats_grapher.get_metrics(@metrics, @date) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | ## Contracted Interfaces 2 | 3 | ```ruby 4 | MetricFu.run_dir #=> Dir.pwd 5 | MetricFu.run_dir = 'some_path' 6 | MetricFu.run_path #=> Pathname(Dir.pwd) 7 | MetricFu.root_dir 8 | MetricFu.loader.load_user_configuration 9 | MetricFu.loader.loaded_files 10 | MetricFu.lib_require { 'utility' } 11 | MetricFu.lib_dir #=> metric_fu/lib 12 | MetricFu.lib_require('metrics') { 'flog/metric' } 13 | MetricFu.metrics_require {'flog/metric' } 14 | MetricFu.metrics_dir #=> metric_fu/lib/metrics 15 | MetricFu.formatter_require { 'html' } 16 | MetricFu.formatter_dir #=> metric_fu/lib/formatter 17 | MetricFu.reporting_require { 'result' } 18 | MetricFu.reporting_dir #=> metric_fu/lib/reporting 19 | MetricFu.logging_require { 'mf_debugger' } 20 | MetricFu.lib_require { 'logger' } 21 | MetricFu.logging_dir #=> metric_fu/lib/logging 22 | MetricFu.errors_require { 'analysis_error' } 23 | MetricFu.errors_dir #=> metric_fu/lib/errors 24 | MetricFu.data_structures_require { 'line_numbers' } 25 | MetricFu.data_structures_dir #=> metric_fu/lib/data_structures 26 | MetricFu.tasks_require { } # Doesn't work as expected. Don't use 27 | MetricFu.tasks_dir #=> metric_fu/lib/tasks 28 | 29 | MetricFu.configuration #=> MetricFu::Configuration.new 30 | MetricFu.configuration.configure_metrics # for each metric, yield to block or runs enable, activate 31 | MetricFu.configuration.configure_metric(:flog) do |metric| 32 | metric.run_options #=> metric.default_run_options.merge(metric.configured_run_options) 33 | metric.enable 34 | metric.enabled = true 35 | metric.activate 36 | metric.activated = true 37 | metric.name #=> :flog 38 | end 39 | ``` 40 | 41 | ## Templates 42 | 43 | ```ruby 44 | # Given 45 | template = MetricFu::Templates::MetricsTemplate.new 46 | 47 | # Render _report_footer.html.erb partial: 48 | 49 | template.render_partial('report_footer') 50 | 51 | # Render _graph.html.erb partial and set a graph_name instance variable: 52 | 53 | template.render_partial 'graph', {:graph_name => 'reek'} 54 | ``` 55 | 56 | ## Testing 57 | 58 | `bundle exec rspec` 59 | 60 | ## Forking 61 | 62 | ## Issues / Pull Requests 63 | 64 | * see [CONTRIBUTING](CONTRIBUTING.md) 65 | 66 | ## Building 67 | 68 | `rake build` or `rake install` 69 | 70 | ## Releasing 71 | 72 | 0. Run `rake usage_test` to make sure the examples are still valid 73 | 1. Update lib/metric_fu/version.rb 74 | 2. Update HISTORY.md 75 | 3. Update CONTRIBUTORS and erd per README in etc 76 | 3. `rake release` 77 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/stats/grapher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "stats/grapher" } 3 | 4 | describe StatsGrapher do 5 | before :each do 6 | @stats_grapher = MetricFu::StatsGrapher.new 7 | MetricFu.configuration 8 | end 9 | 10 | it "should respond to loc_counts and lot_counts and labels" do 11 | expect(@stats_grapher).to respond_to(:loc_counts) 12 | expect(@stats_grapher).to respond_to(:lot_counts) 13 | expect(@stats_grapher).to respond_to(:labels) 14 | end 15 | 16 | describe "responding to #initialize" do 17 | it "should initialise loc_counts and lot_counts and labels" do 18 | expect(@stats_grapher.loc_counts).to eq([]) 19 | expect(@stats_grapher.lot_counts).to eq([]) 20 | expect(@stats_grapher.labels).to eq({}) 21 | end 22 | end 23 | 24 | describe "responding to #get_metrics" do 25 | context "when metrics were not generated" do 26 | before(:each) do 27 | @metrics = FIXTURE.load_metric("metric_missing.yml") 28 | @date = "01022003" 29 | end 30 | 31 | it "should not push to loc_counts" do 32 | expect(@stats_grapher.loc_counts).not_to receive(:push) 33 | @stats_grapher.get_metrics(@metrics, @date) 34 | end 35 | 36 | it "should not push to lot_counts" do 37 | expect(@stats_grapher.lot_counts).not_to receive(:push) 38 | @stats_grapher.get_metrics(@metrics, @date) 39 | end 40 | 41 | it "should not update labels with the date" do 42 | expect(@stats_grapher.labels).not_to receive(:update) 43 | @stats_grapher.get_metrics(@metrics, @date) 44 | end 45 | end 46 | 47 | context "when metrics have been generated" do 48 | before(:each) do 49 | @metrics = FIXTURE.load_metric("20090630.yml") 50 | @date = "01022003" 51 | end 52 | 53 | it "should push to loc_counts" do 54 | expect(@stats_grapher.loc_counts).to receive(:push).with(15935) 55 | @stats_grapher.get_metrics(@metrics, @date) 56 | end 57 | 58 | it "should push to lot_counts" do 59 | expect(@stats_grapher.lot_counts).to receive(:push).with(7438) 60 | @stats_grapher.get_metrics(@metrics, @date) 61 | end 62 | 63 | it "should update labels with the date" do 64 | expect(@stats_grapher.labels).to receive(:update).with(0 => "01022003") 65 | @stats_grapher.get_metrics(@metrics, @date) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute: 2 | 3 | ## Bug reports / Issues 4 | 5 | * Is something broken or not working as expected? Check for an existing issue or [create a new one](https://github.com/metricfu/metric_fu/issues/new) 6 | * See [Quick guide to writing good bug reports](https://github.com/metricfu/metric_fu/wiki/Issues:-Quick-guide-to-writing-good-bug-reports) 7 | * IMPORTANT: Include the output of `metric_fu --debug-info` 8 | 9 | ## Code 10 | 11 | 1. Fork and clone the repo: `git clone git://github.com/yourghname/metric_fu.git && cd metric_fu` 12 | 2. Install the gem dependencies: `bundle install` 13 | 3. Make the changes you want and back them up with tests. 14 | * Run the tests (`bundle exec rspec`) 15 | * Run metric_fu on itself (`bundle exec ruby -Ilib bin/metric_fu`) 16 | 4. Update the HISTORY.md file with your changes and give yourself credit 17 | 5. Commit and create a pull request with details as to what has been changed and why 18 | * Use well-described, small (atomic) commits. 19 | * Include links to any relevant github issues. 20 | * *Don't* change the VERSION file. 21 | 6. Extra Credit: [Confirm it runs and tests pass on the rubies specified in the travis config](.travis.yml). I will otherwise confirm it runs on these. 22 | 23 | How I handle pull requests: 24 | 25 | * If the tests pass and the pull request looks good, I will merge it. 26 | * If the pull request needs to be changed, 27 | * you can change it by updating the branch you generated the pull request from 28 | * either by adding more commits, or 29 | * by force pushing to it 30 | * I can make any changes myself and manually merge the code in. 31 | 32 | ### Commit Messages 33 | 34 | * [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 35 | * [http://stopwritingramblingcommitmessages.com/](http://stopwritingramblingcommitmessages.com/) 36 | * [ThoughtBot style guide](https://github.com/thoughtbot/guides/tree/master/style#git) 37 | 38 | ### About Pull Requests (PR's) 39 | 40 | * [All Your Open Source Code Are Belong To Us](http://www.benjaminfleischer.com/2013/07/30/all-your-open-source-code-are-belong-to-us/) 41 | * [Using Pull Requests](https://help.github.com/articles/using-pull-requests) 42 | * [Github pull requests made easy](http://www.element84.com/github-pull-requests-made-easy.html) 43 | 44 | ## Documentation 45 | 46 | * If relevant, you may update [the metric_fu website](https://github.com/metricfu/metricfu.github.com) in a separate pull request to that repo 47 | * Update the wiki 48 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/rcov/rcov_line_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "metric_fu/metrics/rcov/rcov_line" 3 | 4 | describe MetricFu::RCovLine do 5 | describe "#to_h" do 6 | it "returns a hash with the content and was_run" do 7 | rcov_line = RCovLine.new("some content", 1) 8 | 9 | expect(rcov_line.to_h).to eq(content: "some content", was_run: 1) 10 | end 11 | end 12 | 13 | describe "#covered?" do 14 | it "returns true if was_run is 1" do 15 | rcov_line = RCovLine.new("", 1) 16 | 17 | expect(rcov_line.covered?).to eq(true) 18 | end 19 | 20 | it "returns false if was_run is 0" do 21 | rcov_line = RCovLine.new("", 0) 22 | 23 | expect(rcov_line.covered?).to eq(false) 24 | end 25 | 26 | it "returns false if was_run is nil" do 27 | rcov_line = RCovLine.new("", nil) 28 | 29 | expect(rcov_line.covered?).to eq(false) 30 | end 31 | end 32 | 33 | describe "#missed?" do 34 | it "returns true if was_run is 0" do 35 | rcov_line = RCovLine.new("", 0) 36 | 37 | expect(rcov_line.missed?).to eq(true) 38 | end 39 | 40 | it "returns false if was_run is 1" do 41 | rcov_line = RCovLine.new("", 1) 42 | 43 | expect(rcov_line.missed?).to eq(false) 44 | end 45 | 46 | it "returns false if was_run is nil" do 47 | rcov_line = RCovLine.new("", nil) 48 | 49 | expect(rcov_line.missed?).to eq(false) 50 | end 51 | end 52 | 53 | describe "#ignored?" do 54 | it "returns true if was_run is nil" do 55 | rcov_line = RCovLine.new("", nil) 56 | 57 | expect(rcov_line.ignored?).to eq(true) 58 | end 59 | 60 | it "returns false if was_run is 1" do 61 | rcov_line = RCovLine.new("", 1) 62 | 63 | expect(rcov_line.ignored?).to eq(false) 64 | end 65 | 66 | it "returns false if was_run is 0" do 67 | rcov_line = RCovLine.new("", 0) 68 | 69 | expect(rcov_line.ignored?).to eq(false) 70 | end 71 | end 72 | 73 | describe "#css_class" do 74 | it "returns 'rcov_run' for an ignored line" do 75 | rcov_line = RCovLine.new("", nil) 76 | expect(rcov_line.css_class).to eq("rcov_run") 77 | end 78 | 79 | it "returns 'rcov_not_run' for a missed line" do 80 | rcov_line = RCovLine.new("", 0) 81 | expect(rcov_line.css_class).to eq("rcov_not_run") 82 | end 83 | 84 | it "returns 'rcov_run' for a covered line" do 85 | rcov_line = RCovLine.new("", 1) 86 | expect(rcov_line.css_class).to eq("rcov_run") 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/metric_fu/metrics/flog/grapher.rb: -------------------------------------------------------------------------------- 1 | MetricFu.reporting_require { "graphs/grapher" } 2 | module MetricFu 3 | class FlogGrapher < Grapher 4 | attr_accessor :flog_average, :labels, :top_five_percent_average 5 | 6 | def self.metric 7 | :flog 8 | end 9 | 10 | def initialize 11 | super 12 | @flog_average = [] 13 | @labels = {} 14 | @top_five_percent_average = [] 15 | end 16 | 17 | def get_metrics(metrics, date) 18 | if metrics && metrics[:flog] 19 | @top_five_percent_average.push(calc_top_five_percent_average(metrics)) 20 | @flog_average.push(metrics[:flog][:average]) 21 | @labels.update(@labels.size => date) 22 | end 23 | end 24 | 25 | def title 26 | "Flog: code complexity" 27 | end 28 | 29 | def data 30 | [ 31 | ["average", @flog_average.join(",")], 32 | ["top 5% average", @top_five_percent_average.join(",")] 33 | ] 34 | end 35 | 36 | def output_filename 37 | "flog.js" 38 | end 39 | 40 | private 41 | 42 | def calc_top_five_percent_average(metrics) 43 | return calc_top_five_percent_average_legacy(metrics) if metrics[:flog][:pages] 44 | 45 | method_scores = metrics[:flog][:method_containers].inject([]) do |method_scores, container| 46 | method_scores += container[:methods].values.map { |v| v[:score] } 47 | end 48 | method_scores.sort!.reverse! 49 | 50 | number_of_methods_that_is_five_percent = (method_scores.size * 0.05).ceil 51 | 52 | total_for_five_percent = 53 | method_scores[0...number_of_methods_that_is_five_percent].inject(0) { |total, score| total += score } 54 | if number_of_methods_that_is_five_percent == 0 55 | 0.0 56 | else 57 | total_for_five_percent / number_of_methods_that_is_five_percent.to_f 58 | end 59 | end 60 | 61 | def calc_top_five_percent_average_legacy(metrics) 62 | methods = metrics[:flog][:pages].inject([]) { |methods, page| methods << page[:scanned_methods] } 63 | methods.flatten! 64 | methods = methods.sort_by { |method| method[:score] }.reverse 65 | 66 | number_of_methods_that_is_five_percent = (methods.size * 0.05).ceil 67 | 68 | total_for_five_percent = 69 | methods[0...number_of_methods_that_is_five_percent].inject(0) { |total, method| total += method[:score] } 70 | if number_of_methods_that_is_five_percent == 0 71 | 0.0 72 | else 73 | total_for_five_percent / number_of_methods_that_is_five_percent.to_f 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/metric_fu/metrics/saikuro/generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | MetricFu.metrics_require { "saikuro/generator" } 3 | 4 | describe MetricFu::SaikuroGenerator do 5 | STUB_TEST_DATA = lambda do |generator| 6 | # set test data dir; ensure it doesn't get cleared 7 | def generator.metric_directory 8 | FIXTURE.fixtures_path.join("saikuro").to_s 9 | end 10 | def generator.clear_scratch_files! 11 | # no-op 12 | end 13 | end 14 | describe "to_h method" do 15 | before do 16 | options = {} 17 | saikuro = MetricFu::SaikuroGenerator.new(options) 18 | STUB_TEST_DATA[saikuro] 19 | 20 | saikuro.analyze 21 | @output = saikuro.to_h 22 | end 23 | 24 | it "should find the filename of a file" do 25 | expect(@output[:saikuro][:files].first[:filename]).to eq("app/controllers/users_controller.rb") 26 | end 27 | 28 | it "should find the name of the classes" do 29 | expect(@output[:saikuro][:classes].first[:name]).to eq("UsersController") 30 | expect(@output[:saikuro][:classes][1][:name]).to eq("SessionsController") 31 | end 32 | 33 | it "should put the most complex method first" do 34 | expect(@output[:saikuro][:methods].first[:name]).to eq("UsersController#create") 35 | expect(@output[:saikuro][:methods].first[:complexity]).to eq(4) 36 | end 37 | 38 | it "should find the complexity of a method" do 39 | expect(@output[:saikuro][:methods].first[:complexity]).to eq(4) 40 | end 41 | 42 | it "should find the lines of a method" do 43 | expect(@output[:saikuro][:methods].first[:lines]).to eq(15) 44 | end 45 | end 46 | 47 | describe "per_file_info method" do 48 | before :all do 49 | options = {} 50 | @saikuro = MetricFu::SaikuroGenerator.new(options) 51 | STUB_TEST_DATA[@saikuro] 52 | @saikuro.analyze 53 | @output = @saikuro.to_h 54 | end 55 | 56 | it "doesn't try to get information if the file does not exist" do 57 | expect(@saikuro).to receive(:file_not_exists?).at_least(:once).and_return(true) 58 | @saikuro.per_file_info("ignore_me") 59 | end 60 | end 61 | 62 | describe MetricFu::SaikuroScratchFile do 63 | describe "getting elements from a Saikuro result file" do 64 | it "should parse nested START/END sections" do 65 | path = FIXTURE.fixtures_path.join("saikuro_sfiles", "thing.rb_cyclo.html").to_s 66 | sfile = MetricFu::SaikuroScratchFile.new path 67 | expect(sfile.elements.map(&:complexity).sort).to eql(["0", "0", "2"]) 68 | end 69 | end 70 | end 71 | end 72 | --------------------------------------------------------------------------------