├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── diagram.yml │ └── main.yml ├── .gitignore ├── .jrubyrc ├── .rubocop.yml ├── .standard.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.rails7.0 ├── Gemfile.rails7.1 ├── Gemfile.rails7.2 ├── Gemfile.rails8.0 ├── LICENSE ├── README.md ├── Rakefile ├── changes.md ├── config.ru ├── coverband.gemspec ├── diagram.svg ├── docs ├── assets │ └── logo │ │ ├── coverband_logo.png │ │ ├── green_head.svg │ │ ├── head_liner.png │ │ ├── head_liner.svg │ │ ├── heads.svg │ │ ├── red_head.svg │ │ └── yellow_head.svg ├── coverband-install-resize.gif ├── coverband_details.png ├── coverband_index.png ├── coverband_install.gif ├── coverband_view_tracker.png ├── coverband_web.png ├── coverband_web_ui.png ├── coverband_web_update.png ├── internal_formats.md └── resources.md ├── lib ├── alternative_coverband_patch.rb ├── coverband.rb └── coverband │ ├── adapters │ ├── base.rb │ ├── file_store.rb │ ├── hash_redis_store.rb │ ├── memcached_store.rb │ ├── null_store.rb │ ├── redis_store.rb │ ├── stdout_store.rb │ └── web_service_store.rb │ ├── at_exit.rb │ ├── collectors │ ├── abstract_tracker.rb │ ├── coverage.rb │ ├── delta.rb │ ├── route_tracker.rb │ ├── translation_tracker.rb │ ├── view_tracker.rb │ └── view_tracker_service.rb │ ├── configuration.rb │ ├── integrations │ ├── background.rb │ ├── background_middleware.rb │ ├── rack_server_check.rb │ ├── report_middleware.rb │ ├── resque.rb │ └── sidekiq_swarm.rb │ ├── reporters │ ├── base.rb │ ├── console_report.rb │ ├── html_report.rb │ ├── json_report.rb │ └── web.rb │ ├── utils │ ├── absolute_file_converter.rb │ ├── configuration_template.rb │ ├── dead_methods.rb │ ├── file_hasher.rb │ ├── file_list.rb │ ├── html_formatter.rb │ ├── jruby_ext.rb │ ├── lines_classifier.rb │ ├── method_definition_scanner.rb │ ├── rails6_ext.rb │ ├── railtie.rb │ ├── relative_file_converter.rb │ ├── result.rb │ ├── results.rb │ ├── source_file.rb │ └── tasks.rb │ └── version.rb ├── lua ├── install.sh ├── lib │ └── persist-coverage.lua └── test │ ├── bootstrap.lua │ ├── harness.lua │ ├── redis-call.lua │ └── test-persist-coverage.lua ├── public ├── application.css ├── application.js ├── colorbox │ ├── border.png │ ├── controls.png │ ├── loading.gif │ └── loading_background.png ├── dependencies.js ├── favicon.png ├── favicon_green.png ├── favicon_red.png ├── favicon_yellow.png ├── images │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ ├── ui-bg_flat_75_ffffff_40x100.png │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ ├── ui-bg_glass_65_ffffff_1x400.png │ ├── ui-bg_glass_75_dadada_1x400.png │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ ├── ui-bg_glass_95_fef1ec_1x400.png │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ ├── ui-icons_222222_256x240.png │ ├── ui-icons_2e83ff_256x240.png │ ├── ui-icons_454545_256x240.png │ ├── ui-icons_888888_256x240.png │ └── ui-icons_cd0a0a_256x240.png ├── loading.gif └── magnify.png ├── roadmap.md ├── test ├── benchmarks │ ├── .gitignore │ ├── benchmark.rake │ ├── coverage_fork.sh │ ├── dog.rb │ ├── graph_bench.sh │ └── init_rails.rake ├── big_dog.rb.erb ├── coverband │ ├── adapters │ │ ├── base_test.rb │ │ ├── file_store_test.rb │ │ ├── hash_redis_store_test.rb │ │ ├── memecached_store_test.rb │ │ ├── null_store_test.rb │ │ ├── redis_store_test.rb │ │ └── web_service_store_test.rb │ ├── at_exit_test.rb │ ├── collectors │ │ ├── coverage_test.rb │ │ ├── delta_test.rb │ │ ├── route_tracker_test.rb │ │ ├── translation_tracker_test.rb │ │ └── view_tracker_test.rb │ ├── configuration_test.rb │ ├── coverband_test.rb │ ├── integrations │ │ ├── background_middleware_test.rb │ │ ├── background_test.rb │ │ ├── rack_server_check_test.rb │ │ ├── report_middleware_test.rb │ │ ├── resque_worker_test.rb │ │ └── test_resque_job.rb │ ├── reporters │ │ ├── base_test.rb │ │ ├── console_test.rb │ │ ├── html_test.rb │ │ ├── json_test.rb │ │ └── web_test.rb │ ├── track_key_test.rb │ └── utils │ │ ├── absolute_file_converter_test.rb │ │ ├── dead_methods_test.rb │ │ ├── file_hasher_test.rb │ │ ├── file_list_test.rb │ │ ├── html_formatter_test.rb │ │ ├── lines_classifier_test.rb │ │ ├── method_definition_scanner_test.rb │ │ ├── relative_file_converter_test.rb │ │ ├── result_test.rb │ │ ├── results_test.rb │ │ ├── source_file_line_test.rb │ │ └── source_file_test.rb ├── dog.rb ├── dog.rb.erb ├── fake_app │ └── basic_rack.rb ├── fixtures │ ├── app │ │ ├── controllers │ │ │ └── sample_controller.rb │ │ └── models │ │ │ └── user.rb │ ├── casting_invitor.rb │ ├── never.rb │ ├── sample.rb │ ├── skipped.rb │ ├── skipped_and_executed.rb │ └── utf-8.rb ├── forked │ ├── rails_full_stack_test.rb │ ├── rails_full_stack_views_test.rb │ ├── rails_rake_full_stack_test.rb │ ├── rails_route_tracker_stack_test.rb │ └── rails_view_tracker_stack_test.rb ├── integration │ ├── full_stack_deferred_eager_test.rb │ ├── full_stack_send_deferred_eager_test.rb │ └── full_stack_test.rb ├── jruby_check.rb ├── rails7_dummy │ ├── Rakefile │ ├── app │ │ ├── controllers │ │ │ ├── dummy_controller.rb │ │ │ └── dummy_view_controller.rb │ │ └── views │ │ │ └── dummy_view │ │ │ ├── show.html.erb │ │ │ ├── show_haml.html.haml │ │ │ └── show_slim.html.slim │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── coverband.rb │ │ ├── coverband_missing_redis.rb │ │ ├── environment.rb │ │ ├── routes.rb │ │ └── secrets.yml │ └── tmp │ │ └── .keep ├── rails8_dummy │ ├── Rakefile │ ├── app │ │ ├── controllers │ │ │ ├── dummy_controller.rb │ │ │ └── dummy_view_controller.rb │ │ └── views │ │ │ └── dummy_view │ │ │ ├── show.html.erb │ │ │ ├── show_haml.html.haml │ │ │ └── show_slim.html.slim │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── coverband.rb │ │ ├── coverband_missing_redis.rb │ │ ├── environment.rb │ │ ├── routes.rb │ │ └── secrets.yml │ └── tmp │ │ ├── .keep │ │ └── local_secret.txt ├── rails_test_helper.rb ├── test_helper.rb └── unique_files.rb └── views ├── abstract_tracker.erb ├── data.erb ├── file_list.erb ├── layout.erb ├── nav.erb ├── settings.erb └── source_file.erb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/diagram.yml: -------------------------------------------------------------------------------- 1 | name: Create diagram 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | get_data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@master 13 | - name: Update diagram 14 | uses: githubocto/repo-visualizer@main 15 | with: 16 | excluded_paths: "ignore,.github" 17 | branch: diagram -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | # Controls when the action will run. 3 | # Github Actions multiple gemfile support? 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | # need to figure out how to use redis on macos github actions 16 | # os: [ubuntu, macos] 17 | os: [ubuntu] 18 | # remove until I sort out CI issues for truffle 19 | # truffleruby, 20 | # truffleruby-head, 21 | # removing jruby again to flaky 22 | gemfile: [ Gemfile.rails7.0, Gemfile.rails7.1, Gemfile.rails7.2, Gemfile.rails8.0 ] 23 | # need to add support for multiple gemfiles 24 | ruby: ["3.1", "3.2", "3.3", "3.4", "ruby-head"] 25 | redis-version: [4, 5, 6, 7] 26 | exclude: 27 | # Rails 8 requires ruby 3.2+. 28 | - gemfile: 'rails_8.0' 29 | ruby: '3.1' 30 | runs-on: ${{ matrix.os }}-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: supercharge/redis-github-action@1.8.0 34 | with: 35 | redis-version: ${{ matrix.redis-version }} 36 | - uses: ruby/setup-ruby@v1 37 | with: 38 | ruby-version: ${{ matrix.ruby }} 39 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 40 | - run: bundle exec rake test:all 41 | - run: "RUBYOPT='--enable=frozen-string-literal --debug=frozen-string-literal' bundle exec rake" 42 | starndardrb: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: ruby/setup-ruby@v1 47 | with: 48 | ruby-version: 3.1 49 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 50 | - run: bundle exec standardrb --format github 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .idea/ 7 | .ruby-version 8 | assets/.DS_Store 9 | .DS_Store 10 | Gemfile*lock 11 | InstalledFiles 12 | _yardoc 13 | coverage 14 | doc/ 15 | lib/bundler/man 16 | pkg 17 | rdoc 18 | spec/reports 19 | test/tmp 20 | test/version_tmp 21 | /tmp/ 22 | temp_results 23 | .byebug_history 24 | .env 25 | log/ 26 | test/unique_files 27 | test/rails7_dummy/tmp 28 | CLAUDE.md 29 | .claude/ -------------------------------------------------------------------------------- /.jrubyrc: -------------------------------------------------------------------------------- 1 | debug.fullTrace=true -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: standard 2 | 3 | inherit_gem: 4 | standard: 5 | - config/ruby-3.1.yml 6 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 3.1 2 | fix: false # default: false 3 | parallel: true # default: false 4 | format: progress # default: Standard::Formatter 5 | default_ignores: false # default: true 6 | 7 | ignore: # default: [] 8 | - "lib/**/*": 9 | - Style/IdenticalConditionalBranches # these are just easier to read sometimes 10 | - Style/IfInsideElse # these are just easier to read sometimes 11 | - Standard/SemanticBlocks # not valid in older Ruby 12 | - Style/Alias # This isn't always right and alias and alias_method can have different usage 13 | - Style/RedundantRegexpEscape # fix later, enforcement changed 14 | - Layout/ArrayAlignment # WTF all of master broken from a few changes in rubo 15 | - Performance/RegexpMatch # Rubocop / standardrb have this WRONG for Ruby 2.3/2.4 not compatiable 16 | - Style/GlobalStdStream # Rubocop / standardrb have this WRONG for Ruby 2.3/2.4 not compatiable 17 | - "vendor/**/*" 18 | - "pkg/**/*" 19 | - "test/**/*": 20 | - Layout/AlignHash 21 | - Style/GlobalVars 22 | - Lint/InterpolationCheck # a test is verifying comments 23 | - Standard/SemanticBlocks # not valid in older Ruby 24 | - Layout/ArrayAlignment # ruby 2.3 / modern seem to dissagree on rubocop/standardrb 25 | - "test/benchmarks/benchmark.rake": 26 | - Lint/UselessAssignment # oddity of memory benchmarking 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at danmayer@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See our [code of conduct](https://github.com/danmayer/coverband/blob/master/CODE_OF_CONDUCT.md) 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in coverband.gemspec 6 | gemspec 7 | gem "rails" # latest 8 | gem "haml" 9 | gem "slim" 10 | gem "webrick" 11 | -------------------------------------------------------------------------------- /Gemfile.rails7.0: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in coverband.gemspec 6 | gemspec 7 | gem 'rails', '~>7.0.0' 8 | gem "haml" 9 | gem "slim" 10 | gem "webrick" 11 | -------------------------------------------------------------------------------- /Gemfile.rails7.1: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in coverband.gemspec 6 | gemspec 7 | gem 'rails', '~>7.1.0' 8 | gem "haml" 9 | gem "slim" 10 | gem "webrick" -------------------------------------------------------------------------------- /Gemfile.rails7.2: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in coverband.gemspec 6 | gemspec 7 | gem 'rails', '~>7.2.0' 8 | gem "haml" 9 | gem "slim" 10 | gem "webrick" 11 | -------------------------------------------------------------------------------- /Gemfile.rails8.0: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in coverband.gemspec 6 | gemspec 7 | gem 'rails', '~>8.0.0' 8 | gem "haml" 9 | gem "slim" 10 | gem "webrick" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Dan Mayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | import "test/benchmarks/benchmark.rake" 5 | require "rubocop/rake_task" 6 | 7 | RuboCop::RakeTask.new 8 | 9 | task default: %i[test] 10 | 11 | task "test:all": %i[test forked_tests benchmarks:memory benchmarks] 12 | 13 | task :test 14 | require "rake/testtask" 15 | Rake::TestTask.new(:test) do |test| 16 | test.libs << "lib" << "test" 17 | # exclude benchmark from the tests as the way it functions resets code coverage during executions 18 | # test.pattern = 'test/unit/*_test.rb' 19 | # using test files opposed to pattern as it outputs which files are run 20 | test.test_files = FileList["test/integration/**/*_test.rb", "test/coverband/**/*_test.rb"] 21 | test.verbose = true 22 | end 23 | 24 | Rake::TestTask.new(:forked_tests) do |test| 25 | if RUBY_PLATFORM == "java" 26 | puts "forked tests not supported on JRuby" 27 | else 28 | test.libs << "lib" << "test" 29 | test.test_files = FileList["test/forked/**/*_test.rb"] 30 | test.verbose = true 31 | end 32 | end 33 | 34 | desc "load irb with this gem" 35 | task :console do 36 | puts "running console" 37 | exec "bundle console" 38 | end 39 | 40 | # This is really just for testing and development because without configuration 41 | # Coverband can't do much 42 | desc "start webserver" 43 | task :server do 44 | exec "rackup -I lib" 45 | end 46 | 47 | desc "publish gem with 2 factor auth, reminder how" 48 | task :publish_gem do 49 | exec "gem push pkg/coverband-4.2.3.XXX.gem" 50 | end 51 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require ::File.expand_path("../lib/coverband", __FILE__) 4 | run Coverband::Reporters::Web.new 5 | -------------------------------------------------------------------------------- /coverband.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "coverband/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "coverband" 9 | spec.version = Coverband::VERSION 10 | spec.authors = ["Dan Mayer", "Karl Baum"] 11 | spec.email = %w[dan@mayerdan.com] 12 | spec.description = 13 | "Rack middleware to measure production code usage (LOC runtime usage)" 14 | spec.summary = 15 | "Rack middleware to measure production code usage (LOC runtime usage)" 16 | spec.homepage = "https://github.com/danmayer/coverband" 17 | spec.license = "MIT" 18 | 19 | spec.files = `git ls-files`.split("\n").reject { |f| f.start_with?("docs") } 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.require_paths = %w[lib] 22 | 23 | spec.required_ruby_version = ">= 3.1" 24 | 25 | spec.metadata = { 26 | "homepage_uri" => "https://github.com/danmayer/coverband", 27 | "bug_tracker_uri" => "https://github.com/danmayer/coverband/issues", 28 | "documentation_uri" => "https://github.com/danmayer/coverband", 29 | "changelog_uri" => "https://github.com/danmayer/coverband/blob/main/changes.md", 30 | "source_code_uri" => "https://github.com/danmayer/coverband" 31 | } 32 | 33 | spec.add_development_dependency "benchmark-ips" 34 | spec.add_development_dependency "capybara" 35 | spec.add_development_dependency "m" 36 | spec.add_development_dependency "memory_profiler" 37 | # breaking change in minitest and mocha... 38 | # note: we are also adding 'spy' as mocha doesn't want us to spy on redis calls... 39 | spec.add_development_dependency "spy" 40 | # ^^^ probably need a large test cleanup refactor 41 | spec.add_development_dependency "minitest" 42 | spec.add_development_dependency "minitest-fork_executor" 43 | spec.add_development_dependency "minitest-stub-const" 44 | spec.add_development_dependency "mocha" 45 | spec.add_development_dependency "rack" 46 | spec.add_development_dependency "rack-test" 47 | spec.add_development_dependency "rake" 48 | spec.add_development_dependency "resque" 49 | spec.add_development_dependency "standard", ">= 1.35.1" 50 | # breaking changes in various rubocop versions 51 | spec.add_development_dependency "rubocop" 52 | 53 | spec.add_development_dependency "coveralls" 54 | # minitest-profile is not compatible with Rails 7.1.0 setup... dropping it for now 55 | # spec.add_development_dependency "minitest-profile" 56 | spec.add_development_dependency "webmock" 57 | spec.add_development_dependency "dalli" # Default memcached adapter 58 | 59 | # TODO: Remove when other production adapters exist 60 | # because the default configuration of redis store, we really do require 61 | # redis now. I was reluctant to add this, but until we offer another production 62 | # quality adapter, I think this is more honest about requirements and reduces confusion 63 | # without this there was a race condition on calling coverband configure before redis was loaded 64 | spec.add_runtime_dependency "redis", ">= 3.0" 65 | spec.add_runtime_dependency "base64" 66 | end 67 | -------------------------------------------------------------------------------- /docs/assets/logo/coverband_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/assets/logo/coverband_logo.png -------------------------------------------------------------------------------- /docs/assets/logo/green_head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/logo/head_liner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/assets/logo/head_liner.png -------------------------------------------------------------------------------- /docs/assets/logo/red_head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/logo/yellow_head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/coverband-install-resize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband-install-resize.gif -------------------------------------------------------------------------------- /docs/coverband_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_details.png -------------------------------------------------------------------------------- /docs/coverband_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_index.png -------------------------------------------------------------------------------- /docs/coverband_install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_install.gif -------------------------------------------------------------------------------- /docs/coverband_view_tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_view_tracker.png -------------------------------------------------------------------------------- /docs/coverband_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_web.png -------------------------------------------------------------------------------- /docs/coverband_web_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_web_ui.png -------------------------------------------------------------------------------- /docs/coverband_web_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/docs/coverband_web_update.png -------------------------------------------------------------------------------- /docs/internal_formats.md: -------------------------------------------------------------------------------- 1 | ### Internal Formats 2 | 3 | If you are doing development having some documented examples of various internal data formats can be helpful... 4 | 5 | The format we get from TracePoint, Coverage, Internal Representations, and Used by SimpleCov for reporting have traditionally varied a bit. We can document the differences in formats here. 6 | 7 | #### Coverage 8 | 9 | ``` 10 | >> require 'coverage' 11 | => true 12 | >> Coverage.start 13 | => nil 14 | >> require './test/unit/dog.rb' 15 | => true 16 | >> 5.times { Dog.new.bark } 17 | => 5 18 | >> Coverage.peek_result 19 | => {"/Users/danmayer/projects/coverband/test/unit/dog.rb"=>[nil, nil, 1, 1, 5, nil, nil]} 20 | ``` 21 | 22 | #### SimpleCov 23 | 24 | The same format, but relative paths. 25 | 26 | ``` 27 | {"test/unit/dog.rb"=>[1, 2, nil, nil, nil, nil, nil]} 28 | ``` 29 | 30 | #### Redis Store 31 | 32 | We store relative path in Redis, the Redis hash stores line numbers -> count (as strings). 33 | 34 | ``` 35 | # Array 36 | ["test/unit/dog.rb"] 37 | 38 | # Hash 39 | {"test/unit/dog.rb"=>{"1"=>"1", "2"=>"2"}} 40 | ``` 41 | 42 | #### File Store 43 | 44 | Similar format to redis store, but array with integer values 45 | 46 | ``` 47 | {"test/unit/dog.rb"=>{"1"=>1, "2"=>2}} 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | These notes of kind of for myself, but if anyone is seriously interested in contributing to the project, these resources might be helpful. I learned a lot looking at various existing projects and open source code. 4 | 5 | ##### Ruby Std-lib Coverage 6 | 7 | * [Ruby Coverage docs](https://ruby-doc.org/stdlib-2.5.0/libdoc/coverage/rdoc/Coverage.html) 8 | 9 | ##### Other 10 | 11 | * [erb code coverage](http://stackoverflow.com/questions/13030909/how-to-test-code-coverage-for-rails-erb-templates) 12 | * [more erb code coverage](https://github.com/colszowka/simplecov/issues/38) 13 | * [erb syntax](http://stackoverflow.com/questions/7996695/rails-erb-syntax) parse out and mark lines as important 14 | * [ruby 2 tracer](https://github.com/brightbox/deb-ruby2.0/blob/master/lib/tracer.rb) 15 | * [coveralls hosted code coverage tracking](https://coveralls.io/docs/ruby) currently for test coverage but might be a good partner for production coverage 16 | * [simplecov usage example](http://www.cakesolutions.net/teamblogs/brief-introduction-to-rspec-and-simplecov-for-ruby) copy some of the syntax sugar setup for cover band 17 | * [Jruby coverage bug](https://github.com/jruby/jruby/issues/1196) 18 | * [learn from oboe ruby code](https://github.com/appneta/oboe-ruby#writing-custom-instrumentation) 19 | * [learn from stackprof](https://github.com/tmm1/stackprof#readme) 20 | * I believe there are possible ways to get even better data using the new [Ruby2 TracePoint API](http://www.ruby-doc.org/core/TracePoint.html) 21 | -------------------------------------------------------------------------------- /lib/alternative_coverband_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | COVERBAND_ALTERNATE_PATCH = true 5 | end 6 | -------------------------------------------------------------------------------- /lib/coverband/adapters/file_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Adapters 5 | ### 6 | # FileStore store a merged coverage file to local disk 7 | # 8 | # Notes: Concurrency 9 | # * threadsafe as the caller to save_report uses @semaphore.synchronize 10 | # * file access process safe as each file written per process PID 11 | # 12 | # Usage: 13 | # config.store = Coverband::Adapters::FileStore.new('log/coverage.log') 14 | # 15 | # View Reports: 16 | # Using this assumes you are syncing the coverage files 17 | # to some shared storage that is accessible outside of the production server 18 | # download files to a system where you want to view the reports.. 19 | # When viewing coverage from the filestore adapter it merges all coverage 20 | # files matching the path pattern, in this case `log/coverage.log.*` 21 | # 22 | # run: `bundle exec rake coverband:coverage_server` 23 | # open http://localhost:9022/ 24 | # 25 | # one could also build a report via code, the output is suitable to feed into SimpleCov 26 | # 27 | # ``` 28 | # coverband.configuration.store.merge_mode = true 29 | # coverband.configuration.store.coverage 30 | # ``` 31 | ### 32 | class FileStore < Base 33 | attr_accessor :merge_mode 34 | def initialize(path, _opts = {}) 35 | super() 36 | @path = "#{path}.#{::Process.pid}" 37 | @merge_mode = false 38 | 39 | config_dir = File.dirname(@path) 40 | Dir.mkdir config_dir unless File.exist?(config_dir) 41 | end 42 | 43 | def clear! 44 | File.delete(path) if File.exist?(path) 45 | end 46 | 47 | def size 48 | File.size?(path).to_i 49 | end 50 | 51 | def coverage(_local_type = nil, opts = {}) 52 | if merge_mode 53 | data = {} 54 | Dir[path.sub(/\.\d+/, ".*")].each do |path| 55 | data = merge_reports(data, JSON.parse(File.read(path)), skip_expansion: true) 56 | end 57 | data 58 | elsif File.exist?(path) 59 | JSON.parse(File.read(path)) 60 | else 61 | {} 62 | end 63 | rescue Errno::ENOENT 64 | {} 65 | end 66 | 67 | def save_report(report) 68 | data = report.dup 69 | data = merge_reports(data, coverage) 70 | File.write(path, JSON.dump(data)) 71 | end 72 | 73 | def raw_store 74 | raise NotImplementedError, "FileStore doesn't support raw_store" 75 | end 76 | 77 | private 78 | 79 | attr_accessor :path 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/coverband/adapters/memcached_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Adapters 5 | class MemcachedStore < Base 6 | STORAGE_FORMAT_VERSION = "coverband_3_2" 7 | 8 | attr_reader :memcached_namespace 9 | 10 | def initialize(memcached, opts = {}) 11 | super() 12 | @memcached = memcached 13 | @memcached_namespace = opts[:memcached_namespace] 14 | @format_version = STORAGE_FORMAT_VERSION 15 | @keys = {} 16 | Coverband::TYPES.each do |type| 17 | @keys[type] = [@format_version, @memcached_namespace, type].compact.join(".") 18 | end 19 | end 20 | 21 | def clear! 22 | Coverband::TYPES.each do |type| 23 | @memcached.delete(type_base_key(type)) 24 | end 25 | end 26 | 27 | def clear_file!(filename) 28 | Coverband::TYPES.each do |type| 29 | data = coverage(type) 30 | data.delete(filename) 31 | save_coverage(data, type) 32 | end 33 | end 34 | 35 | def size 36 | @memcached.read(base_key) ? @memcached.read(base_key).bytesize : "N/A" 37 | end 38 | 39 | def type=(type) 40 | super 41 | reset_base_key 42 | end 43 | 44 | def coverage(local_type = nil, opts = {}) 45 | local_type ||= opts.key?(:override_type) ? opts[:override_type] : type 46 | data = memcached.read(type_base_key(local_type)) 47 | data = data ? JSON.parse(data) : {} 48 | data.delete_if { |file_path, file_data| file_hash(file_path) != file_data["file_hash"] } unless opts[:skip_hash_check] 49 | data 50 | end 51 | 52 | def save_report(report) 53 | data = report.dup 54 | data = merge_reports(data, coverage(nil, skip_hash_check: true)) 55 | save_coverage(data) 56 | end 57 | 58 | def raw_store 59 | raise NotImplementedError, "MemcachedStore doesn't support raw_store" 60 | end 61 | 62 | attr_reader :memcached 63 | 64 | private 65 | 66 | def reset_base_key 67 | @base_key = nil 68 | end 69 | 70 | def base_key 71 | @base_key ||= [@format_version, @memcached_namespace, type].compact.join(".") 72 | end 73 | 74 | def type_base_key(local_type) 75 | @keys[local_type] 76 | end 77 | 78 | def save_coverage(data, local_type = nil) 79 | local_type ||= type 80 | key = type_base_key(local_type) 81 | memcached.write(key, data.to_json) 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/coverband/adapters/null_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Adapters 5 | ### 6 | # NullStore is for benchmarking the impacts of calculating 7 | # and storing coverage data independent of Coverband/Coverage 8 | # 9 | # Usage: 10 | # config.store = Coverband::Adapters::NullStore.new 11 | ### 12 | class NullStore < Base 13 | def initialize(_opts = {}) 14 | super() 15 | end 16 | 17 | def clear! 18 | # NOOP 19 | end 20 | 21 | def size 22 | 0 23 | end 24 | 25 | def coverage(_local_type = nil, opts = {}) 26 | {} 27 | end 28 | 29 | def save_report(report) 30 | # NOOP 31 | end 32 | 33 | def raw_store 34 | raise NotImplementedError, "NullStore doesn't support raw_store" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/coverband/adapters/redis_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Adapters 5 | ### 6 | # RedisStore store a merged coverage file to redis 7 | ### 8 | class RedisStore < Base 9 | ### 10 | # This key isn't related to the coverband version, but to the internal format 11 | # used to store data to redis. It is changed only when breaking changes to our 12 | # redis format are required. 13 | ### 14 | REDIS_STORAGE_FORMAT_VERSION = "coverband_3_2" 15 | 16 | attr_reader :redis_namespace 17 | 18 | def initialize(redis, opts = {}) 19 | super() 20 | @redis = redis 21 | @ttl = opts[:ttl] 22 | @redis_namespace = opts[:redis_namespace] 23 | @format_version = REDIS_STORAGE_FORMAT_VERSION 24 | @keys = {} 25 | Coverband::TYPES.each do |type| 26 | @keys[type] = [@format_version, @redis_namespace, type].compact.join(".") 27 | end 28 | end 29 | 30 | def clear! 31 | Coverband::TYPES.each do |type| 32 | @redis.del(type_base_key(type)) 33 | end 34 | end 35 | 36 | def clear_file!(filename) 37 | Coverband::TYPES.each do |type| 38 | data = coverage(type) 39 | data.delete(filename) 40 | save_coverage(data, type) 41 | end 42 | end 43 | 44 | def size 45 | @redis.get(base_key) ? @redis.get(base_key).bytesize : "N/A" 46 | end 47 | 48 | def type=(type) 49 | super 50 | reset_base_key 51 | end 52 | 53 | def coverage(local_type = nil, opts = {}) 54 | local_type ||= opts.key?(:override_type) ? opts[:override_type] : type 55 | data = redis.get type_base_key(local_type) 56 | data = data ? JSON.parse(data) : {} 57 | data.delete_if { |file_path, file_data| file_hash(file_path) != file_data["file_hash"] } unless opts[:skip_hash_check] 58 | data 59 | end 60 | 61 | # Note: This could lead to slight race on redis 62 | # where multiple processes pull the old coverage and add to it then push 63 | # the Coverband 2 had the same issue, 64 | # and the tradeoff has always been acceptable 65 | def save_report(report) 66 | data = report.dup 67 | data = merge_reports(data, coverage(nil, skip_hash_check: true)) 68 | save_coverage(data) 69 | end 70 | 71 | def raw_store 72 | @redis 73 | end 74 | 75 | def file_count 76 | data = redis.get type_base_key(Coverband::RUNTIME_TYPE) 77 | JSON.parse(data).keys.length 78 | end 79 | 80 | def cached_file_count 81 | @cached_file_count ||= file_count 82 | end 83 | 84 | private 85 | 86 | attr_reader :redis 87 | 88 | def reset_base_key 89 | @base_key = nil 90 | end 91 | 92 | def base_key 93 | @base_key ||= [@format_version, @redis_namespace, type].compact.join(".") 94 | end 95 | 96 | def type_base_key(local_type) 97 | @keys[local_type] 98 | end 99 | 100 | def save_coverage(data, local_type = nil) 101 | local_type ||= type 102 | redis.set type_base_key(local_type), data.to_json 103 | redis.expire(type_base_key(local_type), @ttl) if @ttl 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/coverband/adapters/stdout_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Adapters 5 | ### 6 | # StdoutStore is for testing and development 7 | # 8 | # Usage: 9 | # config.store = Coverband::Adapters::StdoutStore.new 10 | ### 11 | class StdoutStore < Base 12 | def initialize(_opts = {}) 13 | super() 14 | end 15 | 16 | def clear! 17 | # NOOP 18 | end 19 | 20 | def size 21 | 0 22 | end 23 | 24 | def coverage(_local_type = nil, opts = {}) 25 | {} 26 | end 27 | 28 | def save_report(report) 29 | $stdout.puts(report.to_json) 30 | end 31 | 32 | def raw_store 33 | raise NotImplementedError, "StdoutStore doesn't support raw_store" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/coverband/at_exit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | class AtExit 5 | @semaphore = Mutex.new 6 | 7 | @at_exit_registered = nil 8 | def self.register 9 | return if ENV["COVERBAND_DISABLE_AT_EXIT"] 10 | return if @at_exit_registered 11 | 12 | @semaphore.synchronize do 13 | return if @at_exit_registered 14 | 15 | @at_exit_registered = true 16 | at_exit do 17 | ::Coverband::Background.stop 18 | 19 | if !Coverband.configuration.report_on_exit 20 | # skip reporting 21 | else 22 | Coverband.report_coverage 23 | # to ensure we track mailer views we now need to report views tracking 24 | # at exit as well for rake tasks and background tasks that can trigger email 25 | Coverband.configuration.view_tracker&.save_report 26 | Coverband.configuration.translations_tracker&.save_report 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/coverband/collectors/delta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Collectors 5 | class Delta 6 | @@previous_coverage = {} 7 | @@stubs = {} 8 | 9 | attr_reader :current_coverage 10 | 11 | def initialize(current_coverage) 12 | @current_coverage = current_coverage 13 | end 14 | 15 | class RubyCoverage 16 | def self.results 17 | if Coverband.configuration.use_oneshot_lines_coverage 18 | ::Coverage.result(clear: true, stop: false) 19 | else 20 | ::Coverage.peek_result 21 | end 22 | end 23 | end 24 | 25 | def self.results(process_coverage = RubyCoverage) 26 | coverage_results = process_coverage.results 27 | new(coverage_results).results 28 | end 29 | 30 | def results 31 | if Coverband.configuration.use_oneshot_lines_coverage 32 | transform_oneshot_lines_results(current_coverage) 33 | else 34 | new_results = generate 35 | @@previous_coverage = current_coverage 36 | new_results 37 | end 38 | end 39 | 40 | def self.reset 41 | @@previous_coverage = {} 42 | @@project_directory = File.expand_path(Coverband.configuration.root) 43 | @@ignore_patterns = Coverband.configuration.ignore 44 | end 45 | 46 | private 47 | 48 | def generate 49 | current_coverage.each_with_object({}) do |(file, line_counts), new_results| 50 | ### 51 | # Eager filter: 52 | # Normally I would break this out into additional methods 53 | # and improve the readability but this is in a tight loop 54 | # on the critical performance path, and any refactoring I come up with 55 | # would slow down the performance. 56 | ### 57 | next unless @@ignore_patterns.none? { |pattern| file.match(pattern) } && 58 | file.start_with?(@@project_directory) 59 | 60 | # This handles Coverage branch support, setup by default in 61 | # simplecov 0.18.x 62 | arr_line_counts = line_counts.is_a?(Hash) ? line_counts[:lines] : line_counts 63 | new_results[file] = if @@previous_coverage && @@previous_coverage[file] 64 | prev_line_counts = @@previous_coverage[file].is_a?(Hash) ? @@previous_coverage[file][:lines] : @@previous_coverage[file] 65 | array_diff(arr_line_counts, prev_line_counts) 66 | else 67 | arr_line_counts 68 | end 69 | end 70 | end 71 | 72 | def array_diff(latest, original) 73 | latest.map.with_index do |v, i| 74 | [0, v - original[i]].max if v && original[i] 75 | end 76 | end 77 | 78 | def transform_oneshot_lines_results(results) 79 | results.each_with_object({}) do |(file, coverage), new_results| 80 | ### 81 | # Eager filter: 82 | # Normally I would break this out into additional methods 83 | # and improve the readability but this is in a tight loop 84 | # on the critical performance path, and any refactoring I come up with 85 | # would slow down the performance. 86 | ### 87 | next unless @@ignore_patterns.none? { |pattern| file.match(pattern) } && 88 | file.start_with?(@@project_directory) 89 | 90 | @@stubs[file] ||= ::Coverage.line_stub(file) 91 | transformed_line_counts = coverage[:oneshot_lines].each_with_object(@@stubs[file].dup) { |line_number, line_counts| 92 | line_counts[line_number - 1] = 1 93 | } 94 | new_results[file] = transformed_line_counts 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/coverband/collectors/route_tracker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | require "singleton" 5 | 6 | module Coverband 7 | module Collectors 8 | ### 9 | # This class tracks route usage via ActiveSupport::Notifications 10 | ### 11 | class RouteTracker < AbstractTracker 12 | REPORT_ROUTE = "routes_tracker" 13 | TITLE = "Routes" 14 | 15 | def initialize(options = {}) 16 | if Rails&.respond_to?(:version) && Gem::Version.new(Rails.version) < Gem::Version.new("7.1.0") 17 | require_relative "../utils/rails6_ext" 18 | end 19 | 20 | super 21 | end 22 | 23 | ### 24 | # This method is called on every routing call, so we try to reduce method calls 25 | # and ensure high performance 26 | ### 27 | def track_key(payload) 28 | route = if payload.key?(:location) 29 | # For redirect.action_dispatch 30 | return unless Coverband.configuration.track_redirect_routes 31 | 32 | { 33 | controller: nil, 34 | action: nil, 35 | url_path: payload[:request].path, 36 | verb: payload[:request].method 37 | } 38 | else 39 | # For start_processing.action_controller 40 | { 41 | controller: payload[:params]["controller"], 42 | action: payload[:action], 43 | url_path: nil, 44 | verb: payload[:method] 45 | } 46 | end 47 | 48 | if newly_seen_key?(route) 49 | @logged_keys << route 50 | @keys_to_record << route if track_key?(route) 51 | end 52 | end 53 | 54 | def self.supported_version? 55 | defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 7 56 | end 57 | 58 | def unused_keys(used_keys = nil) 59 | recently_used_routes = (used_keys || self.used_keys).keys 60 | # NOTE: we match with or without path to handle paths with named params like `/user/:user_id` to used routes filling with all the variable named paths 61 | all_keys.reject { |r| recently_used_routes.include?(r.to_s) || recently_used_routes.include?(r.merge(url_path: nil).to_s) } 62 | end 63 | 64 | def railtie! 65 | ActiveSupport::Notifications.subscribe("start_processing.action_controller") do |name, start, finish, id, payload| 66 | Coverband.configuration.route_tracker.track_key(payload) 67 | end 68 | 69 | # NOTE: This event was instrumented in Aug 10th 2022, but didn't make the 7.0.4 release and should be in the next release 70 | # https://github.com/rails/rails/pull/43755 71 | # Automatic tracking of redirects isn't available before Rails 7.1.0 (currently tested against the 7.1.0.alpha) 72 | # We could consider back porting or patching a solution that works on previous Rails versions 73 | ActiveSupport::Notifications.subscribe("redirect.action_dispatch") do |name, start, finish, id, payload| 74 | Coverband.configuration.route_tracker.track_key(payload) 75 | end 76 | end 77 | 78 | private 79 | 80 | def concrete_target 81 | if defined?(Rails.application) 82 | if Rails.application.respond_to?(:reload_routes!) && Rails.application.routes.empty? 83 | # NOTE: depending on eager loading etc, routes may not be loaded 84 | # so load them if they aren't 85 | Rails.application.reload_routes! 86 | end 87 | Rails.application.routes.routes.map do |route| 88 | { 89 | controller: route.defaults[:controller], 90 | action: route.defaults[:action], 91 | url_path: route.path.spec.to_s.gsub("(.:format)", ""), 92 | verb: route.verb 93 | } 94 | end 95 | else 96 | [] 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/coverband/collectors/translation_tracker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | require "singleton" 5 | 6 | module Coverband 7 | module Collectors 8 | module I18n 9 | module KeyRegistry 10 | def lookup(locale, key, scope = [], options = {}) 11 | separator = options[:separator] || ::I18n.default_separator 12 | flat_key = ::I18n.normalize_keys(locale, key, scope, separator).join(separator) 13 | Coverband.configuration.translations_tracker.track_key(flat_key) 14 | 15 | super 16 | end 17 | end 18 | end 19 | 20 | ### 21 | # This class tracks translation usage via I18n::Backend 22 | ### 23 | class TranslationTracker < AbstractTracker 24 | REPORT_ROUTE = "translations_tracker" 25 | TITLE = "Translations" 26 | 27 | def railtie! 28 | # plugin to i18n 29 | ::I18n::Backend::Simple.send :include, ::Coverband::Collectors::I18n::KeyRegistry 30 | end 31 | 32 | private 33 | 34 | def concrete_target 35 | if defined?(Rails.application) 36 | app_translation_keys = [] 37 | app_translation_files = ::I18n.load_path.select { |f| f.match(/config\/locales/) } 38 | app_translation_files.each do |file| 39 | app_translation_keys += flatten_hash(YAML.load_file(file, aliases: true)).keys 40 | end 41 | app_translation_keys.uniq 42 | else 43 | [] 44 | end 45 | end 46 | 47 | def flatten_hash(hash) 48 | hash.each_with_object({}) do |(k, v), h| 49 | if v.is_a? Hash 50 | flatten_hash(v).map do |h_k, h_v| 51 | h[:"#{k}.#{h_k}"] = h_v 52 | end 53 | else 54 | h[k] = v 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/coverband/collectors/view_tracker_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Collectors 5 | ### 6 | # This class extends view tracker to support web service reporting 7 | ### 8 | class ViewTrackerService < ViewTracker 9 | def save_report 10 | reported_time = Time.now.to_i 11 | if @views_to_record.any? 12 | relative_views = @views_to_record.map! do |view| 13 | roots.each do |root| 14 | view = view.gsub(/#{root}/, "") 15 | end 16 | view 17 | end 18 | save_tracked_views(views: relative_views, reported_time: reported_time) 19 | end 20 | @views_to_record = [] 21 | rescue => e 22 | # we don't want to raise errors if Coverband can't reach the service 23 | logger&.error "Coverband: view_tracker failed to store, error #{e.class.name}" if Coverband.configuration.verbose || Coverband.configuration.service_dev_mode 24 | end 25 | 26 | def self.supported_version? 27 | defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 7 28 | end 29 | 30 | private 31 | 32 | def logger 33 | Coverband.configuration.logger 34 | end 35 | 36 | def save_tracked_views(views:, reported_time:) 37 | uri = URI("#{Coverband.configuration.service_url}/api/collector") 38 | req = Net::HTTP::Post.new(uri, "content-type" => "application/json", "Coverband-Token" => Coverband.configuration.api_key) 39 | data = { 40 | collection_type: "view_tracker_delta", 41 | collection_data: { 42 | tags: { 43 | runtime_env: Coverband.configuration.coverband_env 44 | }, 45 | collection_time: reported_time, 46 | tracked_views: views 47 | } 48 | } 49 | # puts "sending #{data}" 50 | req.body = {remote_uuid: SecureRandom.uuid, data: data}.to_json 51 | Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http| 52 | http.request(req) 53 | end 54 | rescue => e 55 | logger&.error "Coverband: Error while saving coverage #{e}" if Coverband.configuration.verbose || Coverband.configuration.service_dev_mode 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/coverband/integrations/background.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | class Background 5 | @semaphore = Mutex.new 6 | @thread = nil 7 | 8 | def self.stop 9 | return unless @thread 10 | 11 | @semaphore.synchronize do 12 | if @thread 13 | @thread.exit 14 | @thread = nil 15 | end 16 | end 17 | end 18 | 19 | def self.running? 20 | @thread&.alive? 21 | end 22 | 23 | def self.start 24 | return if running? 25 | 26 | logger = Coverband.configuration.logger 27 | @semaphore.synchronize do 28 | return if running? 29 | 30 | logger.debug("Coverband: Starting background reporting") if Coverband.configuration.verbose 31 | sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds.to_i 32 | @thread = Thread.new { 33 | Thread.current.name = "Coverband Background Reporter" 34 | 35 | loop do 36 | if Coverband.configuration.reporting_wiggle 37 | sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds.to_i + rand(Coverband.configuration.reporting_wiggle.to_i) 38 | end 39 | # NOTE: Normally as processes first start we immediately report, this causes a redis spike on deploys 40 | # if deferred is set also sleep frst to spread load 41 | sleep(sleep_seconds.to_i) if Coverband.configuration.defer_eager_loading_data? 42 | Coverband.report_coverage 43 | Coverband.configuration.trackers.each { |tracker| tracker.save_report } 44 | if Coverband.configuration.verbose 45 | logger.debug("Coverband: background reporting coverage (#{Coverband.configuration.store.type}). Sleeping #{sleep_seconds}s") 46 | end 47 | sleep(sleep_seconds.to_i) unless Coverband.configuration.defer_eager_loading_data? 48 | end 49 | } 50 | end 51 | rescue ThreadError 52 | stop 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/coverband/integrations/background_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | class BackgroundMiddleware 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | @app.call(env) 11 | ensure 12 | AtExit.register 13 | Background.start if Coverband.configuration.background_reporting_enabled 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/coverband/integrations/rack_server_check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | class RackServerCheck 5 | def self.running? 6 | new(Kernel.caller_locations).running? 7 | end 8 | 9 | def initialize(stack) 10 | @stack = stack 11 | end 12 | 13 | def running? 14 | rack_server? || rails_server? 15 | end 16 | 17 | def rack_server? 18 | @stack.any? { |line| line.path.include?("lib/rack/") } 19 | end 20 | 21 | def rails_server? 22 | @stack.any? do |location| 23 | location.path.include?("rails/commands/commands_tasks.rb") && location.label == "server" || 24 | location.path.include?("rails/commands/server/server_command.rb") && location.label == "perform" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/coverband/integrations/report_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | class ReportMiddleware 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | @app.call(env) 11 | ensure 12 | Collectors::Coverage.instance.report_coverage 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/coverband/integrations/resque.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Resque.after_fork do |_job| 4 | Coverband.start 5 | Coverband.runtime_coverage! 6 | end 7 | 8 | Resque.before_first_fork do 9 | Coverband.eager_loading_coverage! 10 | Coverband.configuration.background_reporting_enabled = false 11 | Coverband::Background.stop 12 | Coverband.report_coverage 13 | end 14 | 15 | module Coverband 16 | module ResqueWorker 17 | def perform 18 | super 19 | ensure 20 | Coverband.report_coverage 21 | end 22 | end 23 | end 24 | 25 | if defined?(Coverband::COVERBAND_ALTERNATE_PATCH) 26 | Resque::Job.class_eval do 27 | def perform_with_coverband 28 | perform_without_coverband 29 | ensure 30 | Coverband.report_coverage 31 | end 32 | alias perform_without_coverband perform 33 | alias perform perform_with_coverband 34 | end 35 | else 36 | Resque::Job.prepend(Coverband::ResqueWorker) 37 | end 38 | -------------------------------------------------------------------------------- /lib/coverband/integrations/sidekiq_swarm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sidekiq.configure_server do |config| 4 | config.on(:fork) do 5 | Coverband.start 6 | Coverband.runtime_coverage! 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/coverband/reporters/console_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Reporters 5 | ### 6 | # Console Report allows for simple reporting via the command line. 7 | ### 8 | class ConsoleReport < Base 9 | def self.report(store, options = {}) 10 | coverband_reports = Coverband::Reporters::Base.report(store, options) 11 | fix_reports(coverband_reports) 12 | result = Coverband::Utils::Results.new(coverband_reports) 13 | source_files = result.source_files 14 | 15 | Coverband.configuration.logger.info "total_files: #{source_files.length}" 16 | Coverband.configuration.logger.info "lines_of_code: #{source_files.lines_of_code}" 17 | Coverband.configuration.logger.info "lines_covered: #{source_files.covered_lines}" 18 | Coverband.configuration.logger.info "lines_missed: #{source_files.missed_lines}" 19 | Coverband.configuration.logger.info "covered_percent: #{source_files.covered_percent}" 20 | 21 | coverband_reports[:merged].each_pair do |file, usage| 22 | Coverband.configuration.logger.info "#{file}: #{usage["data"]}" 23 | end 24 | coverband_reports 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/coverband/reporters/html_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Reporters 5 | class HTMLReport < Base 6 | attr_accessor :filtered_report_files, :open_report, :notice, 7 | :base_path, :filename, :page 8 | 9 | def initialize(store, options = {}) 10 | self.page = options.fetch(:page) { nil } 11 | self.open_report = options.fetch(:open_report) { true } 12 | # TODO: refactor notice out to top level of web only 13 | self.notice = options.fetch(:notice) { nil } 14 | self.base_path = options.fetch(:base_path) { "./" } 15 | self.filename = options.fetch(:filename) { nil } 16 | 17 | coverband_reports = Coverband::Reporters::Base.report(store, options) 18 | # NOTE: at the moment the optimization around paging and filenames only works for hash redis store 19 | self.filtered_report_files = if (page || filename) && store.is_a?(Coverband::Adapters::HashRedisStore) 20 | coverband_reports 21 | else 22 | self.class.fix_reports(coverband_reports) 23 | end 24 | end 25 | 26 | def file_details 27 | Coverband::Utils::HTMLFormatter.new(filtered_report_files, 28 | base_path: base_path, 29 | notice: notice).format_source_file!(filename) 30 | end 31 | 32 | def report 33 | report_dynamic_html 34 | end 35 | 36 | def report_data 37 | report_dynamic_data 38 | end 39 | 40 | private 41 | 42 | def report_dynamic_html 43 | Coverband::Utils::HTMLFormatter.new(filtered_report_files, 44 | base_path: base_path, 45 | notice: notice, 46 | page: page).format_dynamic_html! 47 | end 48 | 49 | def report_dynamic_data 50 | Coverband::Utils::HTMLFormatter.new(filtered_report_files, 51 | base_path: base_path, 52 | page: page, 53 | notice: notice).format_dynamic_data! 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/coverband/utils/absolute_file_converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Utils 5 | class AbsoluteFileConverter 6 | def initialize(roots) 7 | @cache = {} 8 | @roots = roots.map { |root| "#{File.expand_path(root)}/" } 9 | end 10 | 11 | def self.instance 12 | @instance ||= new(Coverband.configuration.all_root_paths) 13 | end 14 | 15 | def self.reset 16 | @instance = nil 17 | end 18 | 19 | def self.convert(relative_path) 20 | instance.convert(relative_path) 21 | end 22 | 23 | def convert(relative_path) 24 | @cache[relative_path] ||= begin 25 | relative_filename = relative_path 26 | local_filename = relative_filename 27 | @roots.each do |root| 28 | relative_filename = relative_filename.sub(/^#{root}/, "./") 29 | # once we have a relative path break out of the loop 30 | break if relative_filename.start_with? "./" 31 | end 32 | # the filename for our reports is expected to be a full path. 33 | # roots.last should be roots << current_root}/ 34 | # a fully expanded path of config.root 35 | # filename = filename.gsub('./', roots.last) 36 | # above only works for app files 37 | # we need to rethink some of this logic 38 | # gems aren't at project root and can have multiple locations 39 | local_root = @roots.find { |root| 40 | File.exist?(relative_filename.gsub("./", root)) 41 | } 42 | local_root ? relative_filename.gsub("./", local_root) : local_filename 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/coverband/utils/configuration_template.rb: -------------------------------------------------------------------------------- 1 | #### 2 | # This is an example coverband configuration file. In a typical Rails app 3 | # it would be placed in config/coverband.rb 4 | # 5 | # Uncomment or adjust the code to your apps needs 6 | #### 7 | # Coverband.configure do |config| 8 | #### 9 | # set a redis URL and set it with with some reasonable timeouts 10 | #### 11 | # redis_url = ENV["COVERBAND_REDIS"] || ENV["REDIS_URL"] || "redis://localhost:6379" 12 | # config.store = Coverband::Adapters::RedisStore.new( 13 | # Redis.new( 14 | # url: redis_url, 15 | # timeout: ENV.fetch("REDIS_TIMEOUT", 1), 16 | # reconnect_attempts: ENV.fetch("REDIS_RECONNECT_ATTEMPTS", 1), 17 | # reconnect_delay: ENV.fetch("REDIS_RECONNECT_DELAY", 0.25), 18 | # reconnect_delay_max: ENV.fetch("REDIS_RECONNECT_DELAY_MAX", 2.5) 19 | # ) 20 | # ) 21 | 22 | # Allow folks to reset the coverband data via the web UI 23 | # config.web_enable_clear = true 24 | 25 | ### 26 | # Redis Performance Options. If you running hundreds of web server processes 27 | # you may want to have some controls on how often they are calling redis 28 | # This can help a relatively small Redis handle hundreds / thousands of reporting servers. 29 | ### 30 | # reduce the CPU and Redis overhead, we don't need reporting every 30s... This is how often each process will try to save reports 31 | # config.background_reporting_sleep_seconds = 400 32 | # add a wiggle to avoid cache stampede and flatten out Redis CPU... 33 | # This can help if you have many servers restart around a deploy and are all hitting redis at the same time. 34 | # config.reporting_wiggle = 90 35 | 36 | # ignore various files for whatever reason. I often ignore the below list as some are setup before coverband and not always 37 | # tracked correctly... Accepts regex or exact match strings. 38 | # config.ignore = %w[config/* 39 | # config/locales/* 40 | # config/environments/* 41 | # config/initializers/*] 42 | 43 | # config options false, true, or 'debug'. Always use false in production 44 | # true and debug can give helpful and interesting code usage information 45 | # they both increase the performance overhead of the gem a little. 46 | # they can also help with initially debugging the installation. 47 | # defaults to false 48 | # config.verbose = false 49 | 50 | # allow the web UI to display raw coverband data, generally only useful for coverband development 51 | # config.web_debug = true 52 | # end 53 | -------------------------------------------------------------------------------- /lib/coverband/utils/dead_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "coverband/utils/method_definition_scanner" 4 | 5 | module Coverband 6 | module Utils 7 | module ArrayToTableInConsole 8 | refine Array do 9 | def to_table 10 | column_sizes = 11 | reduce([]) { |lengths, row| 12 | row.each_with_index.map do |iterand, index| 13 | [lengths[index] || 0, iterand.to_s.length].max 14 | end 15 | } 16 | puts head = 17 | "-" * (column_sizes.inject(&:+) + (3 * column_sizes.count) + 1) 18 | each do |row| 19 | row = row.fill(nil, row.size..(column_sizes.size - 1)) 20 | row = 21 | row.each_with_index.map { |v, i| 22 | v.to_s + " " * (column_sizes[i] - v.to_s.length) 23 | } 24 | puts "| " + row.join(" | ") + " |" 25 | end 26 | puts head 27 | end 28 | end 29 | end 30 | 31 | class DeadMethods 32 | using ArrayToTableInConsole 33 | def self.scan(file_path:, coverage:) 34 | MethodDefinitionScanner.scan(file_path).reject do |method_definition| 35 | method_definition.body.coverage?(coverage) 36 | end 37 | end 38 | 39 | def self.scan_all 40 | # If the file was loaded during eager loading and then its code is never executed 41 | # during runtime, then it will not have any runtime coverage. When reporting 42 | # dead methods, we need to look at all the files discovered during the eager loading 43 | # and runtime phases. 44 | coverage = Coverband.configuration.store.get_coverage_report[Coverband::MERGED_TYPE] 45 | coverage.flat_map do |file_path, coverage| 46 | scan(file_path: file_path, coverage: coverage["data"]) 47 | end 48 | end 49 | 50 | def self.output_all 51 | rows = 52 | scan_all.each_with_object( 53 | [%w[file class method line_number]] 54 | ) { |dead_method, rows| 55 | rows << 56 | [ 57 | dead_method.file_path, 58 | dead_method.class_name, 59 | dead_method.name, 60 | dead_method.first_line_number 61 | ] 62 | } 63 | rows.to_table 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/coverband/utils/file_hasher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Utils 5 | class FileHasher 6 | @cache = {} 7 | 8 | def self.hash_file(file, path_converter: AbsoluteFileConverter.instance) 9 | @cache[file] ||= begin 10 | file = path_converter.convert(file) 11 | Digest::MD5.file(file).hexdigest if File.exist?(file) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/coverband/utils/file_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #### 4 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov 5 | # initial version pulled into Coverband from Simplecov 12/04/2018 6 | # 7 | # An array of SourceFile instances with additional collection helper 8 | # methods for calculating coverage across them etc. 9 | #### 10 | module Coverband 11 | module Utils 12 | class FileList < Array 13 | # Returns the count of lines that have coverage 14 | def covered_lines 15 | return 0.0 if empty? 16 | 17 | map { |f| f.covered_lines.count }.inject(:+) 18 | end 19 | 20 | # Returns the count of lines that have been missed 21 | def missed_lines 22 | return 0.0 if empty? 23 | 24 | map { |f| f.missed_lines.count }.inject(:+) 25 | end 26 | 27 | # Returns the count of lines that are not relevant for coverage 28 | def never_lines 29 | return 0.0 if empty? 30 | 31 | map { |f| f.never_lines.count }.inject(:+) 32 | end 33 | 34 | # Returns the count of skipped lines 35 | def skipped_lines 36 | return 0.0 if empty? 37 | 38 | map { |f| f.skipped_lines.count }.inject(:+) 39 | end 40 | 41 | # Computes the coverage based upon lines covered and lines missed for each file 42 | # Returns an array with all coverage percentages 43 | def covered_percentages 44 | map(&:covered_percent) 45 | end 46 | 47 | # Returns the overall amount of relevant lines of code across all files in this list 48 | def lines_of_code 49 | covered_lines + missed_lines 50 | end 51 | 52 | # Computes the coverage based upon lines covered and lines missed 53 | # @return [Float] 54 | def covered_percent 55 | return 100.0 if empty? || lines_of_code.zero? 56 | 57 | Float(covered_lines * 100.0 / lines_of_code) 58 | end 59 | 60 | # Computes the coverage based upon lines covered and lines missed, formatted 61 | # @return [Float] 62 | def formatted_covered_percent 63 | covered_percent.round(2) 64 | end 65 | 66 | # Computes the strength (hits / line) based upon lines covered and lines missed 67 | # @return [Float] 68 | def covered_strength 69 | return 0.0 if empty? || lines_of_code.zero? 70 | 71 | Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code) 72 | end 73 | 74 | def first_seen_at 75 | map(&:first_updated_at).reject { |el| el.is_a?(String) }.min 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/coverband/utils/jruby_ext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #### 4 | # This exists in CRuby, but not in JRuby, so add it 5 | # 6 | # Taken from: https://github.com/ruby/ruby/blob/c5eb24349a4535948514fe765c3ddb0628d81004/ext/coverage/lib/coverage.rb 7 | #### 8 | module Coverage 9 | def self.line_stub(file) 10 | lines = File.foreach(file).map { nil } 11 | iseqs = [RubyVM::InstructionSequence.compile_file(file)] 12 | until iseqs.empty? 13 | iseq = iseqs.pop 14 | iseq.trace_points.each { |n, type| lines[n - 1] = 0 if type == :line } 15 | iseq.each_child { |child| iseqs << child } 16 | end 17 | lines 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/coverband/utils/lines_classifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #### 4 | # 5 | # NOTE: with Ruby 2.6.0 and beyond we can replace this classifier with 6 | # ::Coverage.line_stub 7 | # https://ruby-doc.org/stdlib-2.6.1/libdoc/coverage/rdoc/Coverage.html#method-c-line_stub 8 | # 9 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov-html 10 | # initial version pulled into Coverband from Simplecov 12/04/2018 11 | # 12 | # Classifies whether lines are relevant for code coverage analysis. 13 | # Comments & whitespace lines, and :nocov: token blocks, are considered not relevant. 14 | #### 15 | module Coverband 16 | module Utils 17 | class LinesClassifier 18 | RELEVANT = 0 19 | NOT_RELEVANT = nil 20 | 21 | WHITESPACE_LINE = /^\s*$/ 22 | COMMENT_LINE = /^\s*#/ 23 | WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE) 24 | 25 | def self.no_cov_line 26 | /^(\s*)#(\s*)(\:nocov\:)/o 27 | end 28 | 29 | def self.no_cov_line?(line) 30 | line =~ no_cov_line 31 | rescue ArgumentError 32 | # E.g., line contains an invalid byte sequence in UTF-8 33 | false 34 | end 35 | 36 | def self.whitespace_line?(line) 37 | line =~ WHITESPACE_OR_COMMENT_LINE 38 | rescue ArgumentError 39 | # E.g., line contains an invalid byte sequence in UTF-8 40 | false 41 | end 42 | 43 | def classify(lines) 44 | skipping = false 45 | 46 | lines.map do |line| 47 | if self.class.no_cov_line?(line) 48 | skipping = !skipping 49 | NOT_RELEVANT 50 | elsif skipping || self.class.whitespace_line?(line) 51 | NOT_RELEVANT 52 | else 53 | RELEVANT 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/coverband/utils/method_definition_scanner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(RubyVM::AbstractSyntaxTree) 4 | module Coverband 5 | module Utils 6 | class MethodDefinitionScanner 7 | attr_reader :path 8 | 9 | def initialize(path) 10 | @path = path 11 | end 12 | 13 | def scan 14 | scan_node(RubyVM::AbstractSyntaxTree.parse_file(path), nil) 15 | end 16 | 17 | def self.scan(path) 18 | new(path).scan 19 | end 20 | 21 | class MethodBody 22 | def initialize(method_definition) 23 | @method_definition = method_definition 24 | end 25 | 26 | def coverage?(file_coverage) 27 | body_coverage = 28 | file_coverage[(first_line_number - 1)..(last_line_number - 1)] 29 | body_coverage.map(&:to_i).any?(&:positive?) 30 | end 31 | 32 | private 33 | 34 | def first_line_number 35 | if multiline? 36 | @method_definition.first_line_number + 1 37 | else 38 | @method_definition.first_line_number 39 | end 40 | end 41 | 42 | def last_line_number 43 | if multiline? 44 | @method_definition.last_line_number - 1 45 | else 46 | @method_definition.last_line_number 47 | end 48 | end 49 | 50 | def multiline? 51 | @method_definition.last_line_number - @method_definition.first_line_number > 1 52 | end 53 | end 54 | 55 | class MethodDefinition 56 | attr_reader :last_line_number, 57 | :first_line_number, 58 | :name, 59 | :class_name, 60 | :file_path 61 | 62 | def initialize( 63 | first_line_number:, 64 | last_line_number:, 65 | name:, 66 | class_name:, 67 | file_path: 68 | ) 69 | @first_line_number = first_line_number 70 | @last_line_number = last_line_number 71 | @name = name 72 | @class_name = class_name 73 | @file_path = file_path 74 | end 75 | 76 | def body 77 | MethodBody.new(self) 78 | end 79 | end 80 | 81 | private 82 | 83 | def scan_node(node, class_name) 84 | definitions = [] 85 | return definitions unless node.is_a?(RubyVM::AbstractSyntaxTree::Node) 86 | current_class = (node.type == :CLASS) ? node.children.first.children.last : class_name 87 | if node.type == :DEFN 88 | definitions << 89 | MethodDefinition.new( 90 | first_line_number: node.first_lineno, 91 | last_line_number: node.last_lineno, 92 | name: node.children.first, 93 | class_name: current_class, 94 | file_path: path 95 | ) 96 | end 97 | definitions + scan_children(node, current_class) 98 | end 99 | 100 | def scan_children(node, current_class) 101 | node.children.flatten.compact.map { |child| 102 | scan_node(child, current_class) 103 | }.flatten 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/coverband/utils/rails6_ext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ### 4 | # This backports routing redirect active notification events to Rails 6 5 | # 6 | # * reproducing this event: https://github.com/rails/rails/pull/43755/files 7 | # * and pulls in the later: https://github.com/rails/rails/commit/40dc22f715ede12ab9b7e06d59fae185da2c38c7 8 | # * using alias method although prepend might be an interesting alternative 9 | # * this doesn't backport the built in listener for the event (ActionDispatch::LogSubscriber) as logging isn't needed 10 | ### 11 | require "action_dispatch/routing/redirection" 12 | 13 | module ActionDispatch 14 | module Routing 15 | class Redirect < Endpoint 16 | def call(env) 17 | ActiveSupport::Notifications.instrument("redirect.action_dispatch") do |payload| 18 | request = Request.new(env) 19 | response = build_response(request) 20 | 21 | payload[:status] = @status 22 | payload[:location] = response.headers["Location"] 23 | payload[:request] = request 24 | 25 | response.to_a 26 | end 27 | end 28 | 29 | def build_response(req) 30 | uri = URI.parse(path(req.path_parameters, req)) 31 | 32 | unless uri.host 33 | if relative_path?(uri.path) 34 | uri.path = "#{req.script_name}/#{uri.path}" 35 | elsif uri.path.empty? 36 | uri.path = req.script_name.empty? ? "/" : req.script_name 37 | end 38 | end 39 | 40 | uri.scheme ||= req.scheme 41 | uri.host ||= req.host 42 | uri.port ||= req.port unless req.standard_port? 43 | 44 | req.commit_flash 45 | 46 | body = "" 47 | 48 | headers = { 49 | "location" => uri.to_s, 50 | "content-type" => "text/html", 51 | "content-length" => body.length.to_s 52 | } 53 | 54 | ActionDispatch::Response.new(status, headers, body) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/coverband/utils/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module RailsEagerLoad 5 | def eager_load! 6 | Coverband.eager_loading_coverage! 7 | super 8 | end 9 | end 10 | Rails::Engine.prepend(RailsEagerLoad) 11 | 12 | class Railtie < Rails::Railtie 13 | initializer "coverband.configure" do |app| 14 | app.middleware.use Coverband::BackgroundMiddleware 15 | rescue Redis::CannotConnectError => error 16 | Coverband.configuration.logger.info "Redis is not available (#{error}), Coverband not configured" 17 | Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore" 18 | end 19 | 20 | config.after_initialize do 21 | unless Coverband.tasks_to_ignore? 22 | Coverband.configure unless Coverband.configured? 23 | Coverband.eager_loading_coverage! 24 | Coverband.report_coverage 25 | Coverband.runtime_coverage! 26 | end 27 | 28 | Coverband.configuration.railtie! 29 | end 30 | 31 | config.before_configuration do 32 | unless ENV["COVERBAND_DISABLE_AUTO_START"] 33 | begin 34 | Coverband.configure unless Coverband.configured? 35 | Coverband.start 36 | rescue Redis::CannotConnectError => error 37 | Coverband.configuration.logger.info "Redis is not available (#{error}), Coverband not configured" 38 | Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore" 39 | end 40 | end 41 | end 42 | 43 | rake_tasks do 44 | load "coverband/utils/tasks.rb" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/coverband/utils/relative_file_converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coverband 4 | module Utils 5 | class RelativeFileConverter 6 | def self.instance 7 | @instance ||= new(Coverband.configuration.all_root_paths) 8 | end 9 | 10 | def self.reset 11 | @instance = nil 12 | end 13 | 14 | def self.convert(file) 15 | instance.convert(file) 16 | end 17 | 18 | def initialize(roots) 19 | @cache = {} 20 | @roots = normalize(roots) 21 | end 22 | 23 | def convert(file) 24 | @cache[file] ||= begin 25 | relative_file = file 26 | @roots.each do |root| 27 | relative_file = file.gsub(/^#{root}/, ".") 28 | break relative_file if relative_file.start_with?(".") 29 | end 30 | relative_file 31 | end 32 | end 33 | 34 | private 35 | 36 | def normalize(paths) 37 | paths.map { |root| File.expand_path(root) } 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/coverband/utils/result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha1" 4 | require "forwardable" 5 | 6 | #### 7 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov 8 | # initial version pulled into Coverband from Simplecov 12/04/2018 9 | # 10 | # A code coverage result, initialized from the Hash stdlib built-in coverage 11 | # library generates (Coverage.result). 12 | #### 13 | module Coverband 14 | module Utils 15 | class Result 16 | extend Forwardable 17 | # Returns the original Coverage.result used for this instance of Coverband::Result 18 | attr_reader :original_result 19 | # Returns all files that are applicable to this result (sans filters!) 20 | # as instances of Coverband::SourceFile. Aliased as :source_files 21 | attr_reader :files 22 | alias source_files files 23 | # Explicitly set the Time this result has been created 24 | attr_writer :created_at 25 | 26 | def_delegators :files, :covered_percent, :covered_percentages, :covered_strength, :covered_lines, :missed_lines 27 | def_delegator :files, :lines_of_code, :total_lines 28 | 29 | # Initialize a new Coverband::Result from given Coverage.result (a Hash of filenames each containing an array of 30 | # coverage data) 31 | def initialize(original_result) 32 | @original_result = (original_result || {}).freeze 33 | 34 | @files = Coverband::Utils::FileList.new(@original_result.map { |filename, coverage| 35 | Coverband::Utils::SourceFile.new(filename, coverage) if File.file?(filename) 36 | }.compact.sort_by(&:short_name)) 37 | end 38 | 39 | # Returns all filenames for source files contained in this result 40 | def filenames 41 | files.map(&:filename) 42 | end 43 | 44 | # Defines when this result has been created. Defaults to Time.now 45 | def created_at 46 | @created_at ||= Time.now 47 | end 48 | 49 | # Finds files that were to be tracked but were not loaded and initializes 50 | # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc). 51 | def self.add_not_loaded_files(result, tracked_files) 52 | if tracked_files 53 | # TODO: Can we get rid of this dup it wastes memory 54 | result = result.dup 55 | Dir[tracked_files].each do |file| 56 | absolute = File.expand_path(file) 57 | result[absolute] ||= { 58 | "data" => [], 59 | "never_loaded" => true 60 | } 61 | end 62 | end 63 | 64 | result 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/coverband/utils/results.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #### 4 | # A way to access the various coverage data breakdowns 5 | #### 6 | module Coverband 7 | module Utils 8 | class Results 9 | attr_accessor :type, :report 10 | 11 | def initialize(report) 12 | self.report = report 13 | self.type = Coverband::MERGED_TYPE 14 | @results = {} 15 | end 16 | 17 | def file_with_type(source_file, results_type) 18 | return unless get_results(results_type) 19 | 20 | @files_with_type ||= {} 21 | @files_with_type[results_type] ||= get_results(results_type).source_files.map do |source_file| 22 | [source_file.filename, source_file] 23 | end.to_h 24 | @files_with_type[results_type][source_file.filename] 25 | end 26 | 27 | def runtime_relevant_coverage(source_file) 28 | return unless eager_loading_coverage && runtime_coverage 29 | 30 | eager_file = get_eager_file(source_file) 31 | runtime_file = get_runtime_file(source_file) 32 | 33 | return 0.0 unless runtime_file 34 | 35 | return runtime_file.formatted_covered_percent unless eager_file 36 | 37 | runtime_relavant_lines = eager_file.relevant_lines - eager_file.covered_lines_count 38 | runtime_file.runtime_relavant_calculations(runtime_relavant_lines) { |file| file.formatted_covered_percent } 39 | end 40 | 41 | def runtime_relavent_lines(source_file) 42 | return 0 unless runtime_coverage 43 | 44 | eager_file = get_eager_file(source_file) 45 | runtime_file = get_runtime_file(source_file) 46 | 47 | return 0 unless runtime_file 48 | 49 | return runtime_file.covered_lines_count unless eager_file 50 | 51 | eager_file.relevant_lines - eager_file.covered_lines_count 52 | end 53 | 54 | def file_from_path_with_type(full_path, results_type = :merged) 55 | return unless get_results(results_type) 56 | 57 | @files_from_path_with_type ||= {} 58 | @files_from_path_with_type[results_type] ||= get_results(results_type).source_files.map do |source_file| 59 | [source_file.filename, source_file] 60 | end.to_h 61 | @files_from_path_with_type[results_type][full_path] 62 | end 63 | 64 | def method_missing(method, *args) 65 | if get_results(type).respond_to?(method) 66 | get_results(type).send(method, *args) 67 | else 68 | super 69 | end 70 | end 71 | 72 | def respond_to_missing?(method) 73 | get_results(type).respond_to?(method) 74 | end 75 | 76 | # Note: small set of hacks for static html simplecov report (groups, created_at, & command_name) 77 | def groups 78 | @groups ||= {} 79 | end 80 | 81 | def created_at 82 | @created_at ||= Time.now 83 | end 84 | 85 | def command_name 86 | @command_name ||= "Coverband" 87 | end 88 | 89 | private 90 | 91 | def get_eager_file(source_file) 92 | file_with_type(source_file, Coverband::EAGER_TYPE) 93 | end 94 | 95 | def get_runtime_file(source_file) 96 | file_with_type(source_file, Coverband::RUNTIME_TYPE) 97 | end 98 | 99 | def eager_loading_coverage 100 | get_results(Coverband::EAGER_TYPE) 101 | end 102 | 103 | def runtime_coverage 104 | get_results(Coverband::RUNTIME_TYPE) 105 | end 106 | 107 | ### 108 | # This is a first version of lazy loading the results 109 | # for the full advantage we need to push lazy loading to the file level 110 | # inside Coverband::Utils::Result 111 | ### 112 | def get_results(type) 113 | return nil unless Coverband::ALL_TYPES.include?(type) 114 | 115 | @results[type] ||= Coverband::Utils::Result.new(report[type]) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/coverband/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ### 4 | # ensure we properly do release candidate versioning; https://github.com/danmayer/coverband/issues/288 5 | # use format "4.2.1.rc.1" ~> 4.2.1.rc to prerelease versions like v4.2.1.rc.2 and v4.2.1.rc.3 6 | ### 7 | module Coverband 8 | VERSION = "6.1.5" 9 | end 10 | -------------------------------------------------------------------------------- /lua/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LUA_DIR="$HOME/lua51" 4 | LUA="$LUA_DIR/bin/lua" 5 | 6 | if [ ! -f $LUA ]; then 7 | echo "Installing lua" 8 | pip install hererocks 9 | hererocks $LUA_DIR -l5.1 -rlatest 10 | fi 11 | source $LUA_DIR/bin/activate 12 | lua -v 13 | for i in luacov busted redis-lua inspect lua-cjson; do 14 | luarocks install $i; 15 | done 16 | -------------------------------------------------------------------------------- /lua/lib/persist-coverage.lua: -------------------------------------------------------------------------------- 1 | local hmset = function (key, dict) 2 | if next(dict) == nil then return nil end 3 | local bulk = {} 4 | for k, v in pairs(dict) do 5 | table.insert(bulk, k) 6 | table.insert(bulk, v) 7 | end 8 | return redis.call('HMSET', key, unpack(bulk)) 9 | end 10 | local payload = cjson.decode(redis.call('get', (KEYS[1]))) 11 | local ttl = payload.ttl 12 | local files_data = payload.files_data 13 | redis.call('DEL', KEYS[1]) 14 | for _, file_data in ipairs(files_data) do 15 | 16 | local hash_key = file_data.hash_key 17 | local first_updated_at = file_data.meta.first_updated_at 18 | file_data.meta.first_updated_at = nil 19 | 20 | hmset(hash_key, file_data.meta) 21 | redis.call('HSETNX', hash_key, 'first_updated_at', first_updated_at) 22 | for line, coverage in pairs(file_data.coverage) do 23 | redis.call("HINCRBY", hash_key, line, coverage) 24 | if coverage > 0 then 25 | redis.call("HSET", hash_key, line .. "_last_posted", ARGV[1]) 26 | end 27 | end 28 | if ttl and ttl ~= cjson.null then 29 | redis.call("EXPIRE", hash_key, ttl) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lua/test/bootstrap.lua: -------------------------------------------------------------------------------- 1 | inspect = require "inspect" 2 | cjson = require 'cjson' 3 | require './lua/test/redis-call' 4 | 5 | 6 | -------------------------------------------------------------------------------- /lua/test/harness.lua: -------------------------------------------------------------------------------- 1 | require './lua/test/bootstrap' 2 | 3 | 4 | KEYS = {} 5 | ARGV = {} 6 | 7 | 8 | function call_redis_script(script, keys, argv) 9 | -- This may not be strictly necessary 10 | for k,v in pairs(ARGV) do ARGV[k] = nil end 11 | for k,v in pairs(KEYS) do KEYS[k] = nil end 12 | 13 | for k,v in pairs(keys) do table.insert(KEYS, v) end 14 | for k,v in pairs(argv) do table.insert(ARGV, v) end 15 | 16 | return dofile('./lua/lib/' .. script) 17 | end 18 | 19 | return call_redis_script; 20 | -------------------------------------------------------------------------------- /lua/test/redis-call.lua: -------------------------------------------------------------------------------- 1 | redis = require 'redis' 2 | 3 | -- If you have some different host/port change it here 4 | local host = "127.0.0.1" 5 | local port = 6379 6 | 7 | 8 | client = redis.connect(host, port) 9 | 10 | -- Workaround for absence of redis.call 11 | redis.call = function(cmd, ...) 12 | local arg={...} 13 | local args_string = '' 14 | for i,v in ipairs(arg) do 15 | args_string = args_string .. ' ' .. v 16 | end 17 | cmd = string.lower(cmd) 18 | print(cmd .. args_string) 19 | --local result = assert(load('return client:'.. cmd ..'(...)'))(...) 20 | local result = assert(loadstring('return client:'.. cmd ..'(...)'))(...) 21 | 22 | -- The redis-lua library returns some values differently to how `redis.call` works inside redis. 23 | -- this makes the responses look like those from the builtin redis 24 | local response_lookup = { 25 | type = function() return { ["ok"]= result } end, 26 | sadd = function() return tonumber(result) end, 27 | zrange = function() 28 | if type(result) == "table" and type(result[1]) == "table" then 29 | -- Deal with WITHSCORES... 30 | local new_result = {} 31 | for k,v in pairs(result) do 32 | table.insert(new_result, v[1]) 33 | table.insert(new_result, v[2]) 34 | end 35 | return new_result; 36 | end 37 | 38 | return result; 39 | end, 40 | hgetall = function() 41 | local new_result = {} 42 | for key, value in pairs(result) do 43 | table.insert(new_result, key) 44 | table.insert(new_result, value) 45 | end 46 | return new_result 47 | end 48 | } 49 | 50 | if response_lookup[cmd] then 51 | return response_lookup[cmd]() 52 | end 53 | 54 | return result; 55 | end 56 | -------------------------------------------------------------------------------- /public/colorbox/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/colorbox/border.png -------------------------------------------------------------------------------- /public/colorbox/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/colorbox/controls.png -------------------------------------------------------------------------------- /public/colorbox/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/colorbox/loading.gif -------------------------------------------------------------------------------- /public/colorbox/loading_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/colorbox/loading_background.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/favicon.png -------------------------------------------------------------------------------- /public/favicon_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/favicon_green.png -------------------------------------------------------------------------------- /public/favicon_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/favicon_red.png -------------------------------------------------------------------------------- /public/favicon_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/favicon_yellow.png -------------------------------------------------------------------------------- /public/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /public/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /public/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /public/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /public/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /public/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /public/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /public/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /public/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /public/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /public/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /public/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /public/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/loading.gif -------------------------------------------------------------------------------- /public/magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/public/magnify.png -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Future Roadmap 2 | 3 | ### Research Alternative Redis formats 4 | 5 | - Look at alternative storage formats for Redis 6 | - [redis bitmaps](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/) 7 | - [redis bitfield](https://stackoverflow.com/questions/47100606/optimal-way-to-store-array-of-integers-in-redis-database) 8 | - Add support for [zadd](http://redis.io/topics/data-types-intro) so one could determine single call versus multiple calls on a line, letting us determine the most executed code in production. 9 | - Changes and updates to Ruby Coverage Library that helps support templates 10 | - https://github.com/ioquatix/covered 11 | - https://github.com/simplecov-ruby/simplecov/pull/1037 12 | - Consider A Coverband Pro / Option to run coverband service locally 13 | - review how humperdink / e70 track translations, particularly how humperdink uses dirty sets with redis, for perf improvements for trackers 14 | - https://github.com/livingsocial/humperdink 15 | - https://github.com/sergioisidoro/e7o/blob/master/lib/e7o.rb 16 | - Possible Cross Application Support to track library usage? 17 | - Reducing differences between coverband local and coverband service 18 | 19 | ### Coverband Next... 20 | 21 | Will be the fully modern release that drops maintenance legacy support in favor of increased performance, ease of use, and maintainability. 22 | 23 | - look at adding a DB tracker 24 | - defaults to oneshot for coverage 25 | - possibly splits coverage and all other covered modules 26 | - drop middleware figure out a way to kick off background without middelware, possibly use similar process forking detection to humperdink 27 | - https://github.com/livingsocial/humperdink/blob/master/lib/humperdink/fork_savvy_redis.rb 28 | - options on reporting 29 | - background reporting 30 | - or middleware reporting 31 | - Support for file versions 32 | - md5 or release tags 33 | - add coverage timerange support 34 | - improved web reporting 35 | - lists current config options 36 | - eventually allow updating remote config 37 | - full theming 38 | - list redis data dump for debugging (refactor built in debug support) 39 | - additional adapters: Memcache, S3, and ActiveRecord 40 | - add articles / podcasts like prontos readme https://github.com/prontolabs/pronto 41 | - add meta data information first seen last recorded to the coverage report views (per file / per method?). 42 | - more details in this issue: https://github.com/danmayer/coverband/issues/118 43 | - See if we can add support for views / templates 44 | - using this technique https://github.com/ioquatix/covered 45 | - Better default grouping (could use groups features for gems for rails controllers, models, lib, etc) 46 | - Improved logging for easier debugging and development 47 | - drop the verbose mode and better support standard logger levels 48 | - redo the logger entirely 49 | - redo config system and allow live config updates via webui 50 | - move all code to work with relative paths leaving only stdlib Coverage working on full paths 51 | 52 | # Out of Scope 53 | 54 | It is important for a project to not only know what problems it is trying to solve, but what things are out of scope. We will start to try to document that here: 55 | 56 | * We have in the past tried to add coverage tracking for all gems, this added a lot of complexity and computation overhead and slowed things down to much. It also was of less value than we had hoped. There are alternative ways to instrument a shared library to track across multiple applications, and single application gem utilization is easier to handle in a one of basis. It is unlikely we will support that again. 57 | -------------------------------------------------------------------------------- /test/benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | classifier-reborn 2 | -------------------------------------------------------------------------------- /test/benchmarks/coverage_fork.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This is a small script to illustrate how previous results get wiped out by forking 4 | # this has implications of forking processes like Resque... 5 | # this in the end would cause coverage.array_diff 6 | # with previous results to add NEGATIVE code hits to the stored Coverage 7 | # which in turn causes all sorts of crazy issues. 8 | # 9 | # ruby test/benchmarks/coverage_fork.rb 10 | # in parent before fork 11 | # {"/Users/danmayer/projects/coverband/test/dog.rb"=>[nil, nil, 1, 1, 2, nil, nil]} 12 | # in child after fork 13 | # {"/Users/danmayer/projects/coverband/test/dog.rb"=>[nil, nil, 0, 0, 0, nil, nil]} 14 | # now triggering hits 15 | # {"/Users/danmayer/projects/coverband/test/dog.rb"=>[nil, nil, 0, 0, 3, nil, nil]} 16 | # 17 | # I believe this might be related to CoW and GC... not sure 18 | # http://patshaughnessy.net/2012/3/23/why-you-should-be-excited-about-garbage-collection-in-ruby-2-0 19 | # 20 | # NOTE: That the child now has 0 hits where previously method definitions had 1 21 | # this causes all sorts of bad things to happen. 22 | require 'coverage' 23 | Coverage.start 24 | load './test/dog.rb' 25 | Dog.new.bark 26 | Dog.new.bark 27 | puts 'in parent before fork' 28 | puts Coverage.peek_result 29 | fork do 30 | puts 'in child after fork' 31 | puts Coverage.peek_result 32 | puts 'now triggering hits' 33 | Dog.new.bark 34 | Dog.new.bark 35 | Dog.new.bark 36 | puts Coverage.peek_result 37 | end 38 | -------------------------------------------------------------------------------- /test/benchmarks/dog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Dog 4 | def bark 5 | "bark" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/benchmarks/graph_bench.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin//gnuplot 2 | 3 | # from http://www.bradlanders.com/2013/04/15/apache-bench-and-gnuplot-youre-probably-doing-it-wrong/ 4 | # Let's output to a jpeg file 5 | set terminal jpeg size 900,500 6 | # This sets the aspect ratio of the graph 7 | set size 1, 1 8 | # The file we'll write to 9 | set output "tmp/timeseries.jpg" 10 | # The graph title 11 | set title "Benchmark testing" 12 | # Where to place the legend/key 13 | set key left top 14 | # Draw gridlines oriented on the y axis 15 | set grid y 16 | # Specify that the x-series data is time data 17 | set xdata time 18 | # Specify the *input* format of the time data 19 | set timefmt "%s" 20 | # Specify the *output* format for the x-axis tick labels 21 | set format x "%S" 22 | # Label the x-axis 23 | set xlabel 'seconds' 24 | # Label the y-axis 25 | set ylabel "response time (ms)" 26 | # Tell gnuplot to use tabs as the delimiter instead of spaces (default) 27 | set datafile separator '\t' 28 | # Plot the data 29 | plot "tmp/ab_brench.tsv" every ::2 using 2:5 title 'response time' with points 30 | exit 31 | -------------------------------------------------------------------------------- /test/benchmarks/init_rails.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | require "coverband" 5 | 6 | desc "Initialize rails" 7 | task "init_rails" do 8 | Coverband.configure("./test/rails#{Rails::VERSION::MAJOR}_dummy/config/coverband.rb") 9 | require "./test/rails#{Rails::VERSION::MAJOR}_dummy/config/environment" 10 | end 11 | -------------------------------------------------------------------------------- /test/big_dog.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Dog<%=dog_number%> 4 | 5 | def self.bark 6 | new.bark 7 | end 8 | 9 | def bark 10 | 'woof' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/coverband/adapters/base_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class AdaptersBaseTest < Minitest::Test 6 | def test_abstract_methods 7 | abstract_methods = %w[clear! clear_file! size save_coverage coverage] 8 | abstract_methods.each do |method| 9 | assert_raises RuntimeError do 10 | Coverband::Adapters::Base.new.send(method.to_sym) 11 | end 12 | end 13 | end 14 | 15 | def test_size_in_mib 16 | base = Coverband::Adapters::Base.new 17 | def base.size 18 | 3.0 19 | end 20 | assert_equal "0.00", base.size_in_mib 21 | end 22 | 23 | def test_array_add 24 | original = [5, 7, nil, nil] 25 | latest = [3, 4, nil, 1] 26 | assert_equal [8, 11, nil, nil], Coverband::Adapters::Base.new.send(:array_add, latest, original) 27 | Coverband.configuration.stubs(:use_oneshot_lines_coverage).returns(true) 28 | assert_equal [1, 1, nil, nil], Coverband::Adapters::Base.new.send(:array_add, latest, original) 29 | end 30 | 31 | describe "Coverband::Adapters::Base using file" do 32 | def setup 33 | super 34 | @test_file_path = "/tmp/coverband_filestore_test_path.json" 35 | @store = Coverband::Adapters::FileStore.new(@test_file_path) 36 | mock_file_hash 37 | end 38 | 39 | def test_covered_merge 40 | old_time = 1541958097 41 | current_time = Time.now.to_i 42 | old_data = { 43 | "first_updated_at" => old_time, 44 | "last_updated_at" => current_time, 45 | "file_hash" => "abcd", 46 | "data" => [5, 7, nil] 47 | } 48 | old_report = {"/projects/coverband_demo/config/coverband.rb" => old_data, 49 | "/projects/coverband_demo/config/initializers/assets.rb" => old_data, 50 | "/projects/coverband_demo/config/initializers/cookies_serializer.rb" => old_data} 51 | new_report = {"/projects/coverband_demo/config/coverband.rb" => [5, 7, nil], 52 | "/projects/coverband_demo/config/initializers/filter_logging.rb" => [5, 7, nil], 53 | "/projects/coverband_demo/config/initializers/wrap_parameters.rb" => [5, 7, nil], 54 | "/projects/coverband_demo/app/controllers/application_controller.rb" => [5, 7, nil]} 55 | expected_merge = { 56 | "first_updated_at" => old_time, 57 | "last_updated_at" => current_time, 58 | "file_hash" => "abcd", 59 | "data" => [10, 14, nil] 60 | } 61 | new_data = { 62 | "first_updated_at" => current_time, 63 | "last_updated_at" => current_time, 64 | "file_hash" => "abcd", 65 | "data" => [5, 7, nil] 66 | } 67 | expected_result = { 68 | "/projects/coverband_demo/app/controllers/application_controller.rb" => new_data, 69 | "/projects/coverband_demo/config/coverband.rb" => expected_merge, 70 | "/projects/coverband_demo/config/initializers/assets.rb" => old_data, 71 | "/projects/coverband_demo/config/initializers/cookies_serializer.rb" => old_data, 72 | "/projects/coverband_demo/config/initializers/filter_logging.rb" => new_data, 73 | "/projects/coverband_demo/config/initializers/wrap_parameters.rb" => new_data 74 | } 75 | assert_equal expected_result, @store.send(:merge_reports, new_report, old_report) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/coverband/adapters/file_store_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class AdaptersFileStoreTest < Minitest::Test 6 | def test_covered_lines_when_no_file 7 | @store = Coverband::Adapters::FileStore.new("") 8 | expected = {} 9 | assert_equal expected, @store.coverage 10 | end 11 | 12 | describe "Coverband::Adapters::FileStore with file" do 13 | def setup 14 | super 15 | @test_file_path = "/tmp/coverband_filestore_test_path.json" 16 | previous_file_path = "#{@test_file_path}.#{::Process.pid}" 17 | `rm #{@test_file_path}` if File.exist?(@test_file_path) 18 | `rm #{previous_file_path}` if File.exist?(previous_file_path) 19 | File.write(previous_file_path, test_data.to_json) 20 | @store = Coverband::Adapters::FileStore.new(@test_file_path) 21 | end 22 | 23 | def test_coverage 24 | assert_equal @store.coverage["dog.rb"]["data"][0], 1 25 | assert_equal @store.coverage["dog.rb"]["data"][1], 2 26 | end 27 | 28 | def test_covered_lines_when_null 29 | assert_nil @store.coverage["none.rb"] 30 | end 31 | 32 | def test_covered_files 33 | assert @store.covered_files.include?("dog.rb") 34 | end 35 | 36 | def test_clear 37 | @store.clear! 38 | assert_equal false, File.exist?(@test_file_path) 39 | end 40 | 41 | def test_save_report 42 | mock_file_hash 43 | @store.send(:save_report, "cat.rb" => [0, 1]) 44 | assert_equal @store.coverage["cat.rb"]["data"][1], 1 45 | assert @store.size > 1 46 | end 47 | 48 | def test_data 49 | { 50 | "dog.rb" => {"data" => [1, 2, nil], 51 | "file_hash" => "abcd", 52 | "first_updated_at" => 1541968729, 53 | "last_updated_at" => 1541968729} 54 | } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/coverband/adapters/memecached_store_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | if ENV["COVERBAND_MEMCACHED"] 6 | require "active_support" 7 | require "dalli" 8 | 9 | class MemcachedTest < Minitest::Test 10 | def setup 11 | super 12 | @store = Coverband::Adapters::MemcachedStore.new(ActiveSupport::Cache::MemCacheStore.new) 13 | end 14 | 15 | def test_coverage 16 | @store.clear! 17 | mock_file_hash 18 | expected = basic_coverage 19 | @store.save_report(expected) 20 | assert_equal expected.keys, @store.coverage.keys 21 | @store.coverage.each_pair do |key, data| 22 | assert_equal expected[key], data["data"] 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/coverband/adapters/null_store_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class AdaptersNullStoreTest < Minitest::Test 6 | def test_covered_lines_when_no_file 7 | @store = Coverband::Adapters::NullStore.new("") 8 | expected = {} 9 | assert_equal expected, @store.coverage 10 | end 11 | 12 | describe "Coverband::Adapters::NullStore" do 13 | def setup 14 | super 15 | @store = Coverband::Adapters::NullStore.new(@test_file_path) 16 | end 17 | 18 | def test_coverage 19 | assert_equal @store.coverage, {} 20 | end 21 | 22 | def test_covered_lines_when_null 23 | assert_nil @store.coverage["none.rb"] 24 | end 25 | 26 | def test_covered_files 27 | assert_equal @store.covered_files.include?("dog.rb"), false 28 | end 29 | 30 | def test_clear 31 | assert_nil @store.clear! 32 | end 33 | 34 | def test_save_report 35 | @store.send(:save_report, "cat.rb" => [0, 1]) 36 | assert_equal @store.coverage, {} 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/coverband/adapters/web_service_store_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | require File.expand_path("../../../lib/coverband/adapters/web_service_store", File.dirname(__FILE__)) 5 | 6 | class WebServiceStoreTest < Minitest::Test 7 | COVERBAND_SERVICE_URL = "http://localhost:12345" 8 | FAKE_API_KEY = "12345" 9 | 10 | def setup 11 | WebMock.disable_net_connect! 12 | super 13 | @store = Coverband::Adapters::WebServiceStore.new(COVERBAND_SERVICE_URL) 14 | Coverband.configuration.store = @store 15 | end 16 | 17 | def test_coverage 18 | Coverband.configuration.api_key = FAKE_API_KEY 19 | stub_request(:post, "#{COVERBAND_SERVICE_URL}/api/collector").to_return(body: {status: "OK"}.to_json, status: 200) 20 | mock_file_hash 21 | @store.save_report(basic_coverage) 22 | end 23 | 24 | # TODO: sort out a retry test 25 | # def test_retries 26 | # Coverband.configuration.api_key = FAKE_API_KEY 27 | # stub_request(:post, "#{COVERBAND_SERVICE_URL}/api/collector").to_return(body: {status: "OK"}.to_json, status: 200) 28 | # mock_file_hash 29 | # @store.save_report(basic_coverage) 30 | # end 31 | 32 | def test_no_webservice_call_without_api_key 33 | Coverband.configuration.api_key = nil 34 | mock_file_hash 35 | @store.save_report(basic_coverage) 36 | end 37 | 38 | def test_clear 39 | assert_raises RuntimeError do 40 | @store.clear! 41 | end 42 | end 43 | 44 | def test_clear_file 45 | assert_raises RuntimeError do 46 | @store.clear_file!("app_path/dog.rb") 47 | end 48 | end 49 | 50 | def test_size 51 | mock_file_hash 52 | @store.type = :eager_loading 53 | @store.save_report("app_path/dog.rb" => [0, 1, 1]) 54 | assert @store.size, 1 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/coverband/at_exit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | 5 | class AtExitTest < Minitest::Test 6 | test "only registers once" do 7 | Coverband::AtExit.instance_eval { @at_exit_registered = nil } 8 | Coverband::AtExit.expects(:at_exit).yields.once.returns(true) 9 | 2.times { Coverband::AtExit.register } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/coverband/collectors/coverage_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class CollectorsCoverageTest < Minitest::Test 6 | attr_accessor :coverband 7 | 8 | def setup 9 | super 10 | @coverband = Coverband::Collectors::Coverage.instance 11 | # preload first coverage hit 12 | @coverband.report_coverage 13 | end 14 | 15 | def teardown 16 | Thread.current[:coverband_instance] = nil 17 | Coverband.configure do |config| 18 | end 19 | @coverband = Coverband::Collectors::Coverage.instance.reset_instance 20 | end 21 | 22 | test "Dog class coverage" do 23 | file = require_unique_file 24 | coverband.report_coverage 25 | coverage = Coverband.configuration.store.coverage 26 | assert_equal(coverage[file]["data"], [nil, nil, 1, 1, 0, nil, nil, 1, nil, 1, nil, nil]) 27 | end 28 | 29 | test "Dog method and class coverage" do 30 | load File.expand_path("../../dog.rb", File.dirname(__FILE__)) 31 | Dog.new.bark 32 | coverband.report_coverage 33 | coverage = Coverband.configuration.store.coverage 34 | assert_equal(coverage["./test/dog.rb"]["data"], [nil, nil, 1, 1, 1, nil, nil, 1, nil, 1, nil, nil]) 35 | end 36 | 37 | test "Dog eager load coverage" do 38 | store = Coverband.configuration.store 39 | assert_equal Coverband::RUNTIME_TYPE, store.type 40 | file = coverband.eager_loading { 41 | require_unique_file 42 | } 43 | coverage = Coverband.configuration.store.coverage[file] 44 | assert_nil coverage, "No runtime coverage" 45 | coverband.eager_loading! 46 | coverage = Coverband.configuration.store.coverage[file] 47 | refute_nil coverage, "Eager load coverage is present" 48 | assert_equal(coverage["data"], [nil, nil, 1, 1, 0, nil, nil, 1, nil, 1, nil, nil]) 49 | end 50 | 51 | test "gets coverage instance" do 52 | assert_equal Coverband::Collectors::Coverage, coverband.class 53 | end 54 | 55 | test "defaults to a redis store" do 56 | assert_equal Coverband::Adapters::RedisStore, coverband.instance_variable_get(:@store).class 57 | end 58 | 59 | test "report_coverage raises errors in tests" do 60 | Coverband::Adapters::RedisStore.any_instance.stubs(:save_report).raises("Oh no") 61 | @coverband.reset_instance 62 | assert_raises RuntimeError do 63 | @coverband.report_coverage 64 | end 65 | end 66 | 67 | test "report_coverage raises errors in tests with verbose enabled" do 68 | Coverband.configuration.verbose = true 69 | logger = mock 70 | Coverband.configuration.logger = logger 71 | @coverband.reset_instance 72 | Coverband::Adapters::RedisStore.any_instance.stubs(:save_report).raises("Oh no") 73 | logger.expects(:error).at_least(3) 74 | error = assert_raises RuntimeError do 75 | @coverband.report_coverage 76 | end 77 | assert_match %r{Oh no}, error.message 78 | end 79 | 80 | test "using coverage state idle with ruby >= 3.1.0" do 81 | ::Coverage.expects(:state).returns(:idle) 82 | ::Coverage.expects(:start).with(oneshot_lines: false) 83 | Coverband::Collectors::Coverage.send(:new) 84 | end 85 | 86 | test "using coverage state suspended with ruby >= 3.1.0" do 87 | ::Coverage.expects(:state).returns(:suspended).at_least_once 88 | ::Coverage.expects(:resume) 89 | Coverband::Collectors::Coverage.send(:new) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/coverband/collectors/translation_tracker_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class TranslationTrackerTest < Minitest::Test 6 | # NOTE: using struct vs open struct as open struct has a special keyword method that overshadows the method value on Ruby 2.x 7 | Payload = Struct.new(:path, :method) 8 | 9 | def tracker_key 10 | Coverband::Collectors::TranslationTracker.expects(:supported_version?).at_least_once.returns(true) 11 | Coverband::Collectors::TranslationTracker.new.send(:tracker_key) 12 | end 13 | 14 | def tracker_time_key 15 | Coverband::Collectors::TranslationTracker.expects(:supported_version?).at_least_once.returns(true) 16 | Coverband::Collectors::TranslationTracker.new.send(:tracker_time_key) 17 | end 18 | 19 | def setup 20 | super 21 | fake_store.raw_store.del(tracker_key) 22 | end 23 | 24 | test "init correctly" do 25 | Coverband::Collectors::TranslationTracker.expects(:supported_version?).returns(true) 26 | tracker = Coverband::Collectors::TranslationTracker.new(store: fake_store, roots: "dir") 27 | assert_nil tracker.target.first 28 | assert !tracker.store.nil? 29 | assert_equal [], tracker.target 30 | assert_equal [], tracker.logged_keys 31 | end 32 | 33 | test "track standard translation keys" do 34 | store = fake_store 35 | translation_key = "en.views.pagination.truncate" 36 | store.raw_store.expects(:hset).with(tracker_key, translation_key, anything) 37 | tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir") 38 | 39 | tracker.track_key(translation_key.to_sym) 40 | tracker.save_report 41 | assert_equal [translation_key.to_sym], tracker.logged_keys 42 | end 43 | 44 | test "report used_keys" do 45 | store = fake_store 46 | translation_key = "en.views.pagination.truncate" 47 | tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir") 48 | tracker.track_key(:"en.views.pagination.truncate") 49 | tracker.save_report 50 | assert_equal [translation_key], tracker.used_keys.keys 51 | end 52 | 53 | test "report unused_keys" do 54 | store = fake_store 55 | app_keys = [ 56 | "en.views.pagination.truncate", 57 | "en.views.pagination.next" 58 | ] 59 | tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir", target: app_keys) 60 | tracker.track_key(:"en.views.pagination.truncate") 61 | tracker.save_report 62 | assert_equal [app_keys.last], tracker.unused_keys 63 | end 64 | 65 | test "reset store" do 66 | store = fake_store 67 | store.raw_store.expects(:del).with(tracker_key) 68 | store.raw_store.expects(:del).with(tracker_time_key) 69 | tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir") 70 | tracker.track_key(:"en.views.pagination.truncate") 71 | tracker.reset_recordings 72 | end 73 | 74 | test "clear_key" do 75 | store = fake_store 76 | translation_key = "en.views.pagination.truncate" 77 | tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir") 78 | tracker.track_key(translation_key.to_sym) 79 | tracker.save_report 80 | assert_equal [translation_key.to_s], tracker.used_keys.keys 81 | tracker.clear_key!(translation_key.to_s) 82 | assert_equal [], tracker.store.raw_store.hgetall(tracker_key).keys 83 | end 84 | 85 | protected 86 | 87 | def fake_store 88 | @fake_store ||= Coverband::Adapters::RedisStore.new(Coverband::Test.redis, redis_namespace: "coverband_test") 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/coverband/coverband_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | 5 | class CoverbandTest < Minitest::Test 6 | test "Coverband#start kicks off background reporting if enabled and not in rack server" do 7 | Coverband.configuration.stubs(:background_reporting_enabled).returns(true) 8 | Coverband::RackServerCheck.expects(:running?).returns(false) 9 | Coverband::Background.expects(:start) 10 | Coverband.start 11 | end 12 | 13 | test "Coverband#start delays background reporting if enabled and running in a rack server" do 14 | Coverband.configuration.stubs(:background_reporting_enabled).returns(true) 15 | Coverband::RackServerCheck.expects(:running?).returns(true) 16 | Coverband::Background.expects(:start).never 17 | Coverband.start 18 | end 19 | 20 | test "Coverband#start does not kick off background reporting if not enabled" do 21 | Coverband.configuration.stubs(:background_reporting_enabled).returns(false) 22 | Coverband::Background.expects(:start).never 23 | ::Coverband.start 24 | end 25 | 26 | test "Coverband#configured? works" do 27 | Coverband.configure 28 | assert Coverband.configured? 29 | end 30 | 31 | test "Eager load coverage block" do 32 | Coverband.eager_loading_coverage do 33 | # some code 34 | 1 + 1 35 | end 36 | assert_equal :runtime, Coverband.configuration.store.type 37 | end 38 | 39 | test "Eager load coverage" do 40 | Coverband.eager_loading_coverage! 41 | assert_equal :eager_loading, Coverband.configuration.store.type 42 | Coverband.runtime_coverage! 43 | assert_equal :runtime, Coverband.configuration.store.type 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/coverband/integrations/background_middleware_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | require "rack" 5 | 6 | class BackgroundMiddlewareTest < Minitest::Test 7 | def setup 8 | super 9 | Coverband.configure do |config| 10 | config.background_reporting_enabled = false 11 | end 12 | end 13 | 14 | test "call app" do 15 | request = Rack::MockRequest.env_for("/anything.json") 16 | Coverband::Collectors::Coverage.instance.reset_instance 17 | middleware = Coverband::BackgroundMiddleware.new(fake_app) 18 | results = middleware.call(request) 19 | assert_equal "/anything.json", results.last 20 | end 21 | 22 | test "pass all rack lint checks" do 23 | Coverband::Collectors::Coverage.instance.reset_instance 24 | app = Rack::Lint.new(Coverband::BackgroundMiddleware.new(fake_app)) 25 | env = Rack::MockRequest.env_for("/hello") 26 | app.call(env) 27 | end 28 | 29 | test "starts background reporter when configured" do 30 | request = Rack::MockRequest.env_for("/anything.json") 31 | Coverband.configuration.stubs(:background_reporting_enabled).returns(true) 32 | Coverband::Background.expects(:start) 33 | middleware = Coverband::BackgroundMiddleware.new(fake_app) 34 | middleware.call(request) 35 | end 36 | 37 | private 38 | 39 | def fake_app 40 | @fake_app ||= lambda do |env| 41 | [200, {"content-type" => "text/plain"}, env["PATH_INFO"]] 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/coverband/integrations/background_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class BackgroundTest < Minitest::Test 6 | class ThreadDouble < Struct.new(:alive) 7 | def exit 8 | end 9 | 10 | def alive? 11 | alive 12 | end 13 | end 14 | 15 | def setup 16 | Coverband.configuration.reset 17 | super 18 | Coverband.configure do |config| 19 | config.background_reporting_sleep_seconds = 60 20 | Coverband.configuration.reporting_wiggle = 0 21 | end 22 | end 23 | 24 | def test_start 25 | sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds.to_i 26 | Thread.expects(:new).yields.returns(ThreadDouble.new(true)) 27 | Coverband::Background.expects(:loop).yields 28 | Coverband::Background.expects(:sleep).with(sleep_seconds) 29 | Coverband::Collectors::Coverage.instance.expects(:report_coverage).once 30 | 2.times { Coverband::Background.start } 31 | end 32 | 33 | def test_start_with_wiggle 34 | sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds.to_i 35 | Thread.expects(:new).yields.returns(ThreadDouble.new(true)) 36 | Coverband::Background.expects(:loop).yields 37 | Coverband::Background.expects(:sleep).with(sleep_seconds + 5) 38 | Coverband::Background.expects(:rand).with(10).returns(5) 39 | Coverband.configuration.reporting_wiggle = 10 40 | Coverband::Collectors::Coverage.instance.expects(:report_coverage).once 41 | 2.times { Coverband::Background.start } 42 | end 43 | 44 | def test_start_dead_thread 45 | Thread.expects(:new).yields.returns(ThreadDouble.new(false)).twice 46 | Coverband::Background.expects(:loop).yields.twice 47 | Coverband::Background.expects(:sleep).with(60).twice 48 | Coverband::Collectors::Coverage.instance.expects(:report_coverage).twice 49 | 2.times { Coverband::Background.start } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/coverband/integrations/rack_server_check_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class RackServerCheckTest < Minitest::Test 6 | # Create a Struct for the caller location at the class level 7 | LocationStruct = Struct.new(:path, :label) 8 | 9 | test "returns true when running in rack server" do 10 | caller_locations = ["blah/lib/rack/server.rb"].map { |path| LocationStruct.new(path, "foo") } 11 | Kernel.expects(:caller_locations).returns(caller_locations) 12 | assert(Coverband::RackServerCheck.running?) 13 | end 14 | 15 | test "returns false when not running in rack server" do 16 | caller_locations = ["blah/lib/sidekiq/worker.rb"].map { |path| LocationStruct.new(path, "foo") } 17 | Kernel.expects(:caller_locations).returns(caller_locations) 18 | refute(Coverband::RackServerCheck.running?) 19 | end 20 | 21 | test "returns true if running within a rails server" do 22 | caller_locations = [LocationStruct.new("/lib/rails/commands/commands_tasks.rb", "server")] 23 | Kernel.expects(:caller_locations).returns(caller_locations) 24 | assert(Coverband::RackServerCheck.running?) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/coverband/integrations/report_middleware_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | require "coverband/integrations/report_middleware" 5 | 6 | class ReportMiddlewareTest < Minitest::Test 7 | def setup 8 | super 9 | Coverband.configure do |config| 10 | config.background_reporting_enabled = false 11 | end 12 | end 13 | 14 | test "reports coverage" do 15 | request = Rack::MockRequest.env_for("/anything.json") 16 | Coverband::Collectors::Coverage.instance.expects(:report_coverage) 17 | middleware = Coverband::ReportMiddleware.new(fake_app) 18 | middleware.call(request) 19 | end 20 | 21 | test "reports coverage when an error is raised" do 22 | request = Rack::MockRequest.env_for("/anything.json") 23 | Coverband::Collectors::Coverage.instance.reset_instance 24 | Coverband::Collectors::Coverage.instance.expects(:report_coverage).once 25 | middleware = Coverband::ReportMiddleware.new(fake_app_raise_error) 26 | begin 27 | middleware.call(request) 28 | rescue 29 | nil 30 | end 31 | end 32 | 33 | private 34 | 35 | def fake_app 36 | @fake_app ||= lambda do |env| 37 | [200, {"content-type" => "text/plain"}, env["PATH_INFO"]] 38 | end 39 | end 40 | 41 | def fake_app_raise_error 42 | @fake_app_raise_error ||= -> { raise "hell" } 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/coverband/integrations/resque_worker_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class ResqueWorkerTest < Minitest::Test 6 | # NOTE: It appears there are some bugs in resque for JRUBY, not coverband, so excluding these tests on JRUBY 7 | # if folks hit issues with Coverband and resque this could be a resque issue, reach out with details. 8 | # I also highly recommend moving to sidekiq. 9 | unless RUBY_PLATFORM == "java" 10 | def enqueue_and_run_job 11 | Resque.enqueue(TestResqueJob) 12 | ENV["QUEUE"] = "resque_coverband" 13 | worker = Resque::Worker.new 14 | worker.startup 15 | worker.work_one_job 16 | end 17 | 18 | def setup 19 | super 20 | Coverband.configure do |config| 21 | config.background_reporting_enabled = false 22 | end 23 | Coverband.start 24 | redis = Coverband.configuration.store.instance_eval { @redis } 25 | Resque.redis = redis 26 | end 27 | 28 | test "resque job coverage" do 29 | relative_job_file = "./test/coverband/integrations/test_resque_job.rb" 30 | resque_job_file = File.expand_path("./test_resque_job.rb", File.dirname(__FILE__)) 31 | require resque_job_file 32 | 33 | enqueue_and_run_job 34 | 35 | assert !Coverband::Background.running? 36 | 37 | # TODO: There is a test only type issue where the test is looking at eager data 38 | # it merged eager and eager for merged and runtime is eager 39 | Coverband.runtime_coverage! 40 | report = Coverband.configuration.store.get_coverage_report 41 | 42 | if RUBY_PLATFORM == "java" 43 | # NOTE: the todo test only issue seems to be slightly different in JRuby 44 | # were nothing is showing up as runtime Coverage... This appears to be a test only issue 45 | assert_equal 1, report[Coverband::EAGER_TYPE][relative_job_file]["data"][6] 46 | else 47 | assert_equal 0, report[Coverband::EAGER_TYPE][relative_job_file]["data"][6] 48 | if report[Coverband::RUNTIME_TYPE] && report[Coverband::RUNTIME_TYPE][relative_job_file] 49 | assert_equal 1, report[Coverband::RUNTIME_TYPE][relative_job_file]["data"][6] 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/coverband/integrations/test_resque_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestResqueJob 4 | @queue = :resque_coverband 5 | 6 | def self.perform 7 | "resque job perform" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/coverband/reporters/console_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class HTMLReportTest < Minitest::Test 6 | REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION 7 | 8 | def setup 9 | super 10 | @store = Coverband.configuration.store 11 | end 12 | 13 | test "report data" do 14 | Coverband.configure do |config| 15 | config.reporter = "std_out" 16 | end 17 | Coverband.configuration.logger.stubs("info") 18 | mock_file_hash 19 | Coverband::Utils::RelativeFileConverter.expects(:convert).with("app_path/dog.rb").returns("./dog.rb") 20 | @store.send(:save_report, basic_coverage) 21 | 22 | report = Coverband::Reporters::ConsoleReport.report(@store)[:merged] 23 | expected = {"./dog.rb" => [0, 1, 2]} 24 | assert_equal(expected.keys, report.keys) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/coverband/reporters/html_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class ReportHTMLTest < Minitest::Test 6 | def setup 7 | super 8 | @store = Coverband.configuration.store 9 | Coverband.configure do |config| 10 | config.store = @store 11 | config.root = fixtures_root 12 | config.ignore = ["notsomething.rb", "lib/*"] 13 | end 14 | mock_file_hash 15 | end 16 | 17 | test "generate dynamic content hosted html report" do 18 | @store.send(:save_report, basic_coverage) 19 | 20 | html = Coverband::Reporters::HTMLReport.new(@store, 21 | open_report: false).report 22 | assert_match "Generated by", html 23 | end 24 | 25 | test "files with no Coverage but in project are shown in reports" do 26 | @store.send(:save_report, basic_source_fixture_coverage) 27 | 28 | html = Coverband::Reporters::HTMLReport.new(@store, 29 | open_report: false).report 30 | assert_match "sample.rb", html 31 | # in project, but not in coverage data 32 | assert_match "app/models/user.rb", html 33 | end 34 | 35 | test "files with no Coverage but in project details page list warning" do 36 | @store.send(:save_report, basic_coverage_full_path) 37 | 38 | basic_coverage_file_full_path 39 | base_path = Dir.pwd 40 | # in project, but not in coverage data 41 | html = Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, 42 | filename: "#{Dir.pwd}/test/fixtures/app/models/user.rb", 43 | base_path: base_path, 44 | open_report: false).file_details 45 | assert_match "This file was never loaded", html 46 | end 47 | 48 | test "generate dynamic content detailed file report" do 49 | @store.send(:save_report, basic_coverage_full_path) 50 | 51 | filename = basic_coverage_file_full_path 52 | base_path = "/coverage" 53 | html = Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, 54 | filename: filename, 55 | base_path: base_path, 56 | open_report: false).file_details 57 | assert_match "Coverage first seen", html 58 | end 59 | 60 | test "generate dynamic content detailed file report handles missing file" do 61 | @store.send(:save_report, basic_coverage_full_path) 62 | 63 | filename = "missing_path" 64 | base_path = "/coverage" 65 | html = Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, 66 | filename: filename, 67 | base_path: base_path, 68 | open_report: false).file_details 69 | assert_match "File No Longer Available", html 70 | end 71 | 72 | test "generate dynamic content detailed file report does not allow loading real non project files" do 73 | @store.send(:save_report, basic_coverage_full_path) 74 | 75 | filename = "#{test_root}/test_helper.rb" 76 | base_path = "/coverage" 77 | html = Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, 78 | filename: filename, 79 | base_path: base_path, 80 | open_report: false).file_details 81 | assert_match "File No Longer Available", html 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/coverband/reporters/json_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class ReportJSONTest < Minitest::Test 6 | def setup 7 | super 8 | @store = Coverband.configuration.store 9 | Coverband.configure do |config| 10 | config.store = @store 11 | config.root = fixtures_root 12 | config.ignore = ["notsomething.rb", "lib/*"] 13 | end 14 | mock_file_hash 15 | end 16 | 17 | test "includes totals" do 18 | @store.send(:save_report, basic_coverage) 19 | 20 | json = Coverband::Reporters::JSONReport.new(@store).report 21 | parsed = JSON.parse(json) 22 | expected_keys = ["total_files", "lines_of_code", "lines_covered", "lines_missed", "covered_strength", "covered_percent"] 23 | assert expected_keys - parsed.keys == [] 24 | end 25 | 26 | test "honors ignore list" do 27 | @store.send(:save_report, basic_coverage) 28 | 29 | json = Coverband::Reporters::JSONReport.new(@store).report 30 | parsed = JSON.parse(json) 31 | expected_files = ["app/controllers/sample_controller.rb", "app/models/user.rb"] 32 | assert_equal parsed["files"].keys, expected_files 33 | end 34 | 35 | test "includes metrics for files" do 36 | @store.send(:save_report, basic_coverage) 37 | 38 | json = Coverband::Reporters::JSONReport.new(@store).report 39 | parsed = JSON.parse(json) 40 | 41 | expected_keys = ["filename", "hash", "never_loaded", "runtime_percentage", "lines_of_code", "lines_covered", "lines_runtime", "lines_missed", "covered_percent", "covered_strength"] 42 | 43 | assert_equal parsed["files"].length, 2 44 | parsed["files"].keys.each do |file| 45 | assert_equal parsed["files"][file].keys, expected_keys 46 | end 47 | end 48 | 49 | test "supports merging" do 50 | @store.send(:save_report, basic_coverage) 51 | first_report = JSON.parse(Coverband::Reporters::JSONReport.new(@store, for_merged_report: true).report) 52 | 53 | @store.send(:save_report, increased_basic_coverage) 54 | second_report = JSON.parse(Coverband::Reporters::JSONReport.new(@store, for_merged_report: true).report) 55 | data = Coverband::Reporters::JSONReport.new(@store).merge_reports(first_report, second_report) 56 | assert_equal data[Coverband::RUNTIME_TYPE.to_s]["app_path/dog.rb"]["data"], [0, 4, 10] 57 | assert_equal data[Coverband::MERGED_TYPE.to_s]["app_path/dog.rb"]["data"], [0, 4, 10] 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/coverband/reporters/web_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | require File.expand_path("../../../lib/coverband/reporters/web", File.dirname(__FILE__)) 5 | require "rack/test" 6 | 7 | ENV["RACK_ENV"] = "test" 8 | 9 | module Coverband 10 | class WebTest < Minitest::Test 11 | include Rack::Test::Methods 12 | 13 | def app 14 | Coverband::Reporters::Web.new 15 | end 16 | 17 | def teardown 18 | super 19 | end 20 | 21 | test "renders index content" do 22 | get "/" 23 | assert last_response.ok? 24 | assert_match "Coverband Home", last_response.body 25 | end 26 | 27 | test "renders index content for empty path" do 28 | get "" 29 | assert last_response.ok? 30 | assert_match "Coverband Home", last_response.body 31 | end 32 | 33 | test "renders 404" do 34 | get "/show" 35 | assert last_response.not_found? 36 | assert_equal "404 error!", last_response.body 37 | end 38 | 39 | test "clears coverband" do 40 | post "/clear" 41 | assert_equal 302, last_response.status 42 | end 43 | end 44 | end 45 | 46 | module Coverband 47 | class AuthWebTest < Minitest::Test 48 | include Rack::Test::Methods 49 | 50 | def setup 51 | super 52 | @store = Coverband.configuration.store 53 | Coverband.configure do |config| 54 | config.password = "test_pass" 55 | end 56 | end 57 | 58 | def app 59 | Coverband::Reporters::Web.new 60 | end 61 | 62 | def teardown 63 | super 64 | end 65 | 66 | test "renders index with basic auth" do 67 | basic_authorize "anything", "test_pass" 68 | get "/" 69 | assert last_response.ok? 70 | assert_match "Coverband Home", last_response.body 71 | end 72 | 73 | test "renders 401 auth error when not provided" do 74 | get "/" 75 | assert_equal 401, last_response.status 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/coverband/track_key_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | 5 | class TrackKeyTest < Minitest::Test 6 | test "track_key accepts supported tracker types" do 7 | mock_view_tracker = mock 8 | mock_view_tracker.expects(:track_key).with("view/path/index.html.erb").returns(true) 9 | Coverband.configuration.expects(:view_tracker).returns(mock_view_tracker) 10 | 11 | assert Coverband.track_key(:view_tracker, "view/path/index.html.erb") 12 | end 13 | 14 | test "track_key raises ArgumentError for unsupported tracker type" do 15 | assert_raises ArgumentError do 16 | Coverband.track_key(:unsupported_tracker, "some_key") 17 | end 18 | end 19 | 20 | test "track_key returns false for nil key" do 21 | assert_equal false, Coverband.track_key(:view_tracker, nil) 22 | end 23 | 24 | test "track_key handles missing trackers" do 25 | Coverband.configuration.expects(:view_tracker).returns(nil) 26 | 27 | assert_equal false, Coverband.track_key(:view_tracker, "some_view") 28 | end 29 | 30 | test "track_key handles trackers without track_key method" do 31 | mock_invalid_tracker = mock 32 | mock_invalid_tracker.expects(:respond_to?).with(:track_key).returns(false) 33 | Coverband.configuration.expects(:view_tracker).returns(mock_invalid_tracker) 34 | 35 | assert_equal false, Coverband.track_key(:view_tracker, "some_view") 36 | end 37 | 38 | test "track_key with translations_tracker" do 39 | mock_translations_tracker = mock 40 | mock_translations_tracker.expects(:track_key).with("translation.key").returns(true) 41 | Coverband.configuration.expects(:translations_tracker).returns(mock_translations_tracker) 42 | 43 | assert Coverband.track_key(:translations_tracker, "translation.key") 44 | end 45 | 46 | test "track_key with routes_tracker" do 47 | mock_routes_tracker = mock 48 | mock_routes_tracker.expects(:track_key).with("index#show").returns(true) 49 | Coverband.configuration.expects(:routes_tracker).returns(mock_routes_tracker) 50 | 51 | assert Coverband.track_key(:routes_tracker, "index#show") 52 | end 53 | 54 | test "track_key logs error when tracking fails" do 55 | mock_logger = mock 56 | mock_logger.expects(:error).with(regexp_matches(/Failed to track key/)) 57 | 58 | mock_tracker = mock 59 | mock_tracker.expects(:track_key).with("test_key").raises(StandardError.new("Test error")) 60 | 61 | Coverband.configuration.expects(:translations_tracker).returns(mock_tracker) 62 | Coverband.configuration.expects(:logger).returns(mock_logger) 63 | 64 | assert_equal false, Coverband.track_key(:translations_tracker, "test_key") 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/coverband/utils/absolute_file_converter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | module Coverband 6 | module Utils 7 | class AbsoluteFileConverterTest < ::Minitest::Test 8 | def test_convert 9 | converter = AbsoluteFileConverter.new([FileUtils.pwd]) 10 | assert_equal("#{FileUtils.pwd}/lib/coverband.rb", converter.convert("./lib/coverband.rb")) 11 | end 12 | 13 | def test_convert_multiple_roots 14 | converter = AbsoluteFileConverter.new(["/foo/bar", FileUtils.pwd]) 15 | assert_equal("#{FileUtils.pwd}/Rakefile", converter.convert("./Rakefile")) 16 | end 17 | 18 | test "relative_path_to_full leave filename from a key with a local path" do 19 | converter = AbsoluteFileConverter.new(["/app/", "/full/remote_app/path/"]) 20 | assert_equal "/full/remote_app/path/is/a/path.rb", converter.convert("/full/remote_app/path/is/a/path.rb") 21 | end 22 | 23 | test "relative_path_to_full fix filename from a key with a swappable path" do 24 | key = "/app/is/a/path.rb" 25 | converter = AbsoluteFileConverter.new(["/app/", "/full/remote_app/path/"]) 26 | expected_path = "/full/remote_app/path/is/a/path.rb" 27 | File.expects(:exist?).with(key).returns(false) 28 | File.expects(:exist?).with(expected_path).returns(true) 29 | assert_equal expected_path, converter.convert(key) 30 | end 31 | 32 | test "relative_path_to_full fix filename a changing deploy path with quotes" do 33 | converter = AbsoluteFileConverter.new(['/box/apps/app_name/releases/\\d+/', "/full/remote_app/path/"]) 34 | expected_path = "/full/remote_app/path/app/models/user.rb" 35 | key = "/box/apps/app_name/releases/20140725203539/app/models/user.rb" 36 | File.expects(:exist?).with('/box/apps/app_name/releases/\\d+/app/models/user.rb').returns(false) 37 | File.expects(:exist?).with(expected_path).returns(true) 38 | assert_equal expected_path, converter.convert(key) 39 | assert_equal expected_path, converter.convert(key) 40 | end 41 | 42 | test "relative_path_to_full fix filename a changing deploy path real world examples" do 43 | current_app_root = "/var/local/company/company.d/79" 44 | converter = AbsoluteFileConverter.new(["/var/local/company/company.d/[0-9]*/", "#{current_app_root}/"]) 45 | 46 | expected_path = "/var/local/company/company.d/79/app/controllers/dashboard_controller.rb" 47 | key = "/var/local/company/company.d/78/app/controllers/dashboard_controller.rb" 48 | File.expects(:exist?).with("/var/local/company/company.d/[0-9]*/app/controllers/dashboard_controller.rb").returns(false) 49 | File.expects(:exist?).with(expected_path).returns(true) 50 | # roots = ["/var/local/company/company.d/[0-9]*/", "#{current_app_root}/"] 51 | assert_equal expected_path, converter.convert(key) 52 | assert_equal expected_path, converter.convert(key) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/coverband/utils/dead_methods_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | if defined?(RubyVM::AbstractSyntaxTree) 6 | require "coverband/utils/dead_methods" 7 | module Coverband 8 | module Utils 9 | class DeadMethodsTest < Minitest::Test 10 | attr_accessor :coverband 11 | 12 | def setup 13 | super 14 | @coverband = Coverband::Collectors::Coverage.instance 15 | end 16 | 17 | def test_dog_dead_methods 18 | file_path = require_unique_file 19 | coverage = [nil, nil, 1, 1, 0, nil, nil, 1, nil, 1, nil, nil] 20 | dead_methods = 21 | DeadMethods.scan(file_path: file_path, coverage: coverage) 22 | assert_equal(1, dead_methods.length) 23 | dead_method = dead_methods.first 24 | assert_equal(4, dead_method.first_line_number) 25 | assert_equal(6, dead_method.last_line_number) 26 | assert_equal(file_path, dead_method.file_path) 27 | end 28 | 29 | def test_all_dead_methods 30 | require_unique_file 31 | @coverband.report_coverage 32 | dead_methods = DeadMethods.scan_all 33 | dead_method = dead_methods.find { |method| method.class_name == :Dog } 34 | assert(dead_method) 35 | assert_equal(4, dead_method.first_line_number) 36 | assert_equal(6, dead_method.last_line_number) 37 | end 38 | 39 | def test_all_dead_methods_on_runtime 40 | @coverband.eager_loading! 41 | require_unique_file 42 | @coverband.report_coverage 43 | @coverband.runtime! 44 | dead_methods = DeadMethods.scan_all 45 | dead_method = dead_methods.find { |method| method.class_name == :Dog } 46 | assert(dead_method) 47 | assert_equal(4, dead_method.first_line_number) 48 | assert_equal(6, dead_method.last_line_number) 49 | end 50 | 51 | def test_output_all 52 | require_unique_file 53 | @coverband.report_coverage 54 | DeadMethods.output_all 55 | end 56 | 57 | def test_dog_methods_not_dead 58 | file = require_unique_file 59 | coverage = [nil, nil, 1, 1, 1, nil, nil, 1, nil, 1, nil, nil] 60 | assert_empty(DeadMethods.scan(file_path: file, coverage: coverage)) 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/coverband/utils/file_hasher_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | module Coverband 6 | module Utils 7 | class FileHasherTest < Minitest::Test 8 | def test_hash_same_file 9 | refute_nil FileHasher.hash_file("./test/dog.rb") 10 | assert_equal(FileHasher.hash_file("./test/dog.rb"), FileHasher.hash_file("./test/dog.rb")) 11 | assert_equal(FileHasher.hash_file(File.expand_path("./test/dog.rb")), FileHasher.hash_file("./test/dog.rb")) 12 | end 13 | 14 | def test_hash_different_files 15 | refute_equal(FileHasher.hash_file("./test/dog.rb"), FileHasher.hash_file("./lib/coverband.rb")) 16 | end 17 | 18 | def test_hash_file_not_exists 19 | assert_nil(FileHasher.hash_file("./made_up_file.py")) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/coverband/utils/file_list_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | #### 6 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov-html 7 | # initial version of test pulled into Coverband from Simplecov 12/19/2018 8 | #### 9 | describe Coverband::Utils::FileList do 10 | subject do 11 | original_result = { 12 | source_fixture("sample.rb") => {"first_updated_at" => Time.at(0), "data" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, 13 | source_fixture("app/models/user.rb") => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], 14 | source_fixture("app/controllers/sample_controller.rb") => [nil, 2, 2, 0, nil, nil, 0, nil, nil, nil] 15 | } 16 | Coverband::Utils::Result.new(original_result).files 17 | end 18 | 19 | it "has 11 covered lines" do 20 | assert_equal 11, subject.covered_lines 21 | end 22 | 23 | it "has 3 missed lines" do 24 | assert_equal 3, subject.missed_lines 25 | end 26 | 27 | it "has 17 never lines" do 28 | assert_equal 17, subject.never_lines 29 | end 30 | 31 | it "has 14 lines of code" do 32 | assert_equal 14, subject.lines_of_code 33 | end 34 | 35 | it "has 5 skipped lines" do 36 | assert_equal 5, subject.skipped_lines 37 | end 38 | 39 | it "has the correct covered percent" do 40 | assert_equal 78.57142857142857, subject.covered_percent 41 | end 42 | 43 | it "has the correct covered percentages" do 44 | assert_equal [50.0, 80.0, 100.0], subject.covered_percentages 45 | end 46 | 47 | it "has the correct covered strength" do 48 | assert_equal 0.9285714285714286, subject.covered_strength 49 | end 50 | 51 | it "has correct first_seen_at" do 52 | assert_equal Time.at(0), subject.first_seen_at 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/coverband/utils/html_formatter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | class HTMLFormatterTest < Minitest::Test 6 | def setup 7 | super 8 | @store = Coverband::Adapters::RedisStore.new(Coverband::Test.redis, redis_namespace: "coverband_test") 9 | end 10 | 11 | test "generate dynamic content hosted html report" do 12 | Coverband.configure do |config| 13 | config.store = @store 14 | config.ignore = ["notsomething.rb"] 15 | end 16 | mock_file_hash 17 | @store.send(:save_report, basic_coverage_full_path) 18 | 19 | notice = nil 20 | base_path = "/coverage" 21 | filtered_report_files = Coverband::Reporters::Base.report(@store, {}) 22 | html = Coverband::Utils::HTMLFormatter.new(filtered_report_files, 23 | base_path: base_path, 24 | notice: notice).format_dynamic_html! 25 | assert_match "loading source data", html 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/coverband/utils/lines_classifier_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | #### 6 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov-html 7 | # initial version of test pulled into Coverband from Simplecov 12/17/2018 8 | #### 9 | describe Coverband::Utils::LinesClassifier do 10 | describe "#classify" do 11 | def subject 12 | Coverband::Utils::LinesClassifier.new 13 | end 14 | 15 | describe "relevant lines" do 16 | it "determines code as relevant" do 17 | classified_lines = subject.classify [ 18 | "module Foo", 19 | " class Baz", 20 | " def Bar", 21 | " puts 'hi'", 22 | " end", 23 | " end", 24 | "end" 25 | ] 26 | 27 | assert_equal 7, classified_lines.length 28 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::RELEVANT }) 29 | end 30 | 31 | it "determines invalid UTF-8 byte sequences as relevant" do 32 | classified_lines = subject.classify [ 33 | "bytes = \"\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\"" 34 | ] 35 | 36 | assert_equal 1, classified_lines.length 37 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::RELEVANT }) 38 | end 39 | end 40 | 41 | describe "not-relevant lines" do 42 | it "determines whitespace is not-relevant" do 43 | classified_lines = subject.classify [ 44 | "", 45 | " ", 46 | "\t\t" 47 | ] 48 | 49 | assert_equal 3, classified_lines.length 50 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::NOT_RELEVANT }) 51 | end 52 | 53 | describe "comments" do 54 | it "determines comments are not-relevant" do 55 | classified_lines = subject.classify [ 56 | "#Comment", 57 | " # Leading space comment", 58 | "\t# Leading tab comment" 59 | ] 60 | 61 | assert_equal 3, classified_lines.length 62 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::NOT_RELEVANT }) 63 | end 64 | 65 | it "doesn't mistake interpolation as a comment" do 66 | classified_lines = subject.classify [ 67 | 'puts "#{var}"' 68 | ] 69 | 70 | assert_equal 1, classified_lines.length 71 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::RELEVANT }) 72 | end 73 | end 74 | 75 | describe ":nocov: blocks" do 76 | it "determines :nocov: blocks are not-relevant" do 77 | classified_lines = subject.classify [ 78 | "# :nocov:", 79 | "def hi", 80 | "end", 81 | "# :nocov:" 82 | ] 83 | 84 | assert_equal 4, classified_lines.length 85 | assert(classified_lines.all? { |line| line == Coverband::Utils::LinesClassifier::NOT_RELEVANT }) 86 | end 87 | 88 | it "determines all lines after a non-closing :nocov: as not-relevant" do 89 | classified_lines = subject.classify [ 90 | "# :nocov:", 91 | "puts 'Not relevant'", 92 | "# :nocov:", 93 | "puts 'Relevant again'", 94 | "puts 'Still relevant'", 95 | "# :nocov:", 96 | "puts 'Not relevant till the end'", 97 | "puts 'Ditto'" 98 | ] 99 | 100 | assert_equal 8, classified_lines.length 101 | 102 | assert(classified_lines[0..2].all? { |line| line == Coverband::Utils::LinesClassifier::NOT_RELEVANT }) 103 | assert(classified_lines[3..4].all? { |line| line == Coverband::Utils::LinesClassifier::RELEVANT }) 104 | assert(classified_lines[5..7].all? { |line| line == Coverband::Utils::LinesClassifier::NOT_RELEVANT }) 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/coverband/utils/method_definition_scanner_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | if defined?(RubyVM::AbstractSyntaxTree) 6 | require "coverband/utils/method_definition_scanner" 7 | module Coverband 8 | module Utils 9 | class MethodBodyTest < Minitest::Test 10 | def test_no_method_body_coverage 11 | method_body = 12 | MethodDefinitionScanner::MethodBody.new( 13 | MethodDefinitionScanner::MethodDefinition.new( 14 | first_line_number: 4, 15 | last_line_number: 6, 16 | name: :bark, 17 | class_name: :Dog, 18 | file_path: "./test/dog.rb" 19 | ) 20 | ) 21 | refute(method_body.coverage?([nil, nil, 1, 1, 0, nil, 1])) 22 | end 23 | 24 | def test_method_body_coverage 25 | method_body = 26 | MethodDefinitionScanner::MethodBody.new( 27 | MethodDefinitionScanner::MethodDefinition.new( 28 | first_line_number: 4, 29 | last_line_number: 6, 30 | name: :bark, 31 | class_name: :Dog, 32 | file_path: "./test/dog.rb" 33 | ) 34 | ) 35 | assert(method_body.coverage?([nil, nil, 1, 1, 1, nil, 1])) 36 | end 37 | end 38 | 39 | class MethodDefinitionScannerTest < Minitest::Test 40 | def test_scan 41 | method_definitions = MethodDefinitionScanner.scan("./test/dog.rb") 42 | assert(method_definitions) 43 | assert_equal(3, method_definitions.length) 44 | method_definition = method_definitions.first # assert_equal(4, method.first_line) 45 | assert_equal(4, method_definition.first_line_number) 46 | assert_equal(6, method_definition.last_line_number) 47 | assert_equal(:bark, method_definition.name) 48 | assert_equal(:Dog, method_definition.class_name) 49 | end 50 | 51 | def test_scan_large_class 52 | method_definitions = 53 | MethodDefinitionScanner.scan("./test/fixtures/casting_invitor.rb") 54 | method_first_line_numbers = 55 | method_definitions.map(&:first_line_number) 56 | assert_equal( 57 | [6, 13, 17, 35, 40, 44, 48, 52], 58 | method_first_line_numbers 59 | ) 60 | method_last_line_numbers = method_definitions.map(&:last_line_number) 61 | assert_equal( 62 | [11, 15, 31, 38, 42, 46, 50, 59], 63 | method_last_line_numbers 64 | ) 65 | method_names = method_definitions.map(&:name) 66 | assert_equal( 67 | %i[ 68 | initialize 69 | valid? 70 | deliver 71 | invalid_invitees 72 | invitee_list 73 | valid_message? 74 | valid_invitees? 75 | create_invitation 76 | ], 77 | method_names 78 | ) 79 | class_names = method_definitions.map(&:class_name) 80 | assert_equal(8.times.map { :CastingInviter }, class_names) 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/coverband/utils/relative_file_converter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | module Coverband 6 | module Utils 7 | class RelativeFileConverterTest < ::Minitest::Test 8 | def test_convert 9 | converter = RelativeFileConverter.new(["/bar/tmp/"]) 10 | assert_equal("./gracie.rb", converter.convert("/bar/tmp/gracie.rb")) 11 | end 12 | 13 | def test_convert_without_leading_forward_slash 14 | converter = RelativeFileConverter.new(["/foo/bar"]) 15 | assert_equal("./file.rb", converter.convert("/foo/bar/file.rb")) 16 | end 17 | 18 | def test_multiple_roots 19 | converter = RelativeFileConverter.new(["/bar/tmp/", "/foo/bar/"]) 20 | assert_equal("./josie.rb", converter.convert("/foo/bar/josie.rb")) 21 | end 22 | 23 | def test_no_match 24 | converter = RelativeFileConverter.new(["/bar/tmp/", "/foo/bar/"]) 25 | assert_equal("/foo/josie.rb", converter.convert("/foo/josie.rb")) 26 | end 27 | 28 | def test_middle_path_match 29 | converter = RelativeFileConverter.new(["/bar/tmp/", "/foo/bar/"]) 30 | assert_equal("/tmp/foo/bar/josie.rb", converter.convert("/tmp/foo/bar/josie.rb")) 31 | end 32 | 33 | def test_already_relative_file 34 | converter = RelativeFileConverter.new(["/bar/tmp/", "/foo/bar/"]) 35 | assert_equal("./foo/bar/josie.rb", converter.convert("./foo/bar/josie.rb")) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/coverband/utils/result_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | #### 6 | # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov-html 7 | # initial version of test pulled into Coverband from Simplecov 12/19/2018 8 | #### 9 | describe "result" do 10 | describe "with a (mocked) Coverage.result" do 11 | let(:original_result) do 12 | { 13 | source_fixture("sample.rb") => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], 14 | source_fixture("app/models/user.rb") => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], 15 | source_fixture("app/controllers/sample_controller.rb") => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil] 16 | } 17 | end 18 | 19 | describe "a simple cov result initialized from that" do 20 | subject { Coverband::Utils::Result.new(original_result) } 21 | 22 | it "has 3 filenames" do 23 | assert_equal 3, subject.filenames.count 24 | end 25 | 26 | it "has 3 source files" do 27 | assert_equal 3, subject.source_files.count 28 | subject.source_files.each do |source_file| 29 | assert source_file.is_a?(Coverband::Utils::SourceFile) 30 | end 31 | end 32 | 33 | it "returns an instance of Coverband::Utils::FileList for source_files and files" do 34 | assert subject.files.is_a?(Coverband::Utils::FileList) 35 | assert subject.source_files.is_a?(Coverband::Utils::FileList) 36 | end 37 | 38 | it "has files equal to source_files" do 39 | assert_equal subject.source_files, subject.files 40 | end 41 | 42 | it "has accurate covered percent" do 43 | # in our fixture, there are 13 covered line (result in 1) in all 15 relevant line (result in non-nil) 44 | assert_equal 86.66666666666667, subject.covered_percent 45 | end 46 | 47 | it "has accurate covered percentages" do 48 | assert_equal [80.0, 80.0, 100.0], subject.covered_percentages 49 | end 50 | 51 | %i[covered_percent 52 | covered_percentages 53 | covered_strength 54 | covered_lines 55 | missed_lines 56 | total_lines].each do |msg| 57 | it "responds to #{msg}" do 58 | assert(subject.respond_to?(msg)) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/coverband/utils/results_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../../test_helper", File.dirname(__FILE__)) 4 | 5 | describe "results" do 6 | describe "with a (mocked) Coverage.result" do 7 | let(:source_file) { Coverband::Utils::SourceFile.new(source_fixture("app/models/user.rb"), run_lines) } 8 | let(:eager_lines) { [nil, 1, 1, 0, nil, nil, 1, 0, nil, nil] } 9 | let(:run_lines) { [nil, nil, nil, 1, nil, nil, nil, nil, nil, nil] } 10 | let(:missing_run_coverage_file) { false } 11 | let(:original_result) do 12 | orig = { 13 | Coverband::MERGED_TYPE => {source_fixture("app/models/user.rb") => eager_lines} 14 | } 15 | orig[Coverband::EAGER_TYPE] = {source_fixture("app/models/user.rb") => eager_lines} if eager_lines 16 | orig[Coverband::RUNTIME_TYPE] = {source_fixture("app/models/user.rb") => run_lines} if run_lines 17 | orig[Coverband::RUNTIME_TYPE] = {"random.rb" => [nil, 1, nil]} if missing_run_coverage_file 18 | orig 19 | end 20 | subject { Coverband::Utils::Results.new(original_result) } 21 | 22 | describe "runtime relevant lines is supported" do 23 | it "has correct runtime relevant coverage" do 24 | assert_equal 50.0, subject.runtime_relevant_coverage(source_file) 25 | end 26 | 27 | it "has correct runtime relevant lines" do 28 | assert_equal 2, subject.runtime_relavent_lines(source_file) 29 | end 30 | end 31 | 32 | describe "runtime relevant lines when no runtime coverage file matches" do 33 | let(:run_lines) { nil } 34 | 35 | it "has correct runtime relevant coverage" do 36 | assert_equal 0, subject.runtime_relevant_coverage(source_file) 37 | end 38 | 39 | it "has correct runtime relevant lines" do 40 | assert_equal 0, subject.runtime_relavent_lines(source_file) 41 | end 42 | end 43 | 44 | describe "runtime relevant lines when no runtime coverage exists" do 45 | let(:run_lines) { nil } 46 | let(:missing_run_coverage_file) { true } 47 | 48 | it "has correct runtime relevant coverage" do 49 | assert_equal 0.0, subject.runtime_relevant_coverage(source_file) 50 | end 51 | 52 | it "has correct runtime relevant lines" do 53 | assert_equal 0, subject.runtime_relavent_lines(source_file) 54 | end 55 | end 56 | 57 | describe "runtime relevant lines when no eager coverage exists" do 58 | let(:eager_lines) { nil } 59 | 60 | it "has correct runtime relevant lines" do 61 | assert_equal 100.0, subject.runtime_relevant_coverage(source_file) 62 | end 63 | 64 | it "has correct runtime relevant lines" do 65 | assert_equal 1, subject.runtime_relavent_lines(source_file) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/dog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Dog 4 | def bark 5 | "woof" 6 | end 7 | 8 | def single_line; end # rubocop:disable Style 9 | 10 | def empty 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/dog.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Dog<%=dog_number%> 4 | def bark 5 | 'woof' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fake_app/basic_rack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rack" 4 | 5 | class HelloWorld 6 | def call(_env) 7 | [200, {"content-type" => "text/html"}, "Hello Rack!"] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/app/controllers/sample_controller.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = "baz" 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = "baz" 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/casting_invitor.rb: -------------------------------------------------------------------------------- 1 | class CastingInviter 2 | EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/ 3 | 4 | attr_reader :message, :invitees, :casting 5 | 6 | def initialize(attributes = {}) 7 | @message = attributes[:message] || "" 8 | @invitees = attributes[:invitees] || "" 9 | @sender = attributes[:sender] 10 | @casting = attributes[:casting] 11 | end 12 | 13 | def valid? 14 | valid_message? && valid_invitees? 15 | end 16 | 17 | def deliver 18 | if valid? 19 | invitee_list.each do |email| 20 | invitation = create_invitation(email) 21 | Mailer.invitation_notification(invitation, @message) 22 | end 23 | else 24 | failure_message = 25 | "Your #{ 26 | @casting 27 | } message couldn’t be sent. Invitees emails or message are invalid" 28 | invitation = create_invitation(@sender) 29 | Mailer.invitation_notification(invitation, failure_message) 30 | end 31 | end 32 | 33 | private 34 | 35 | def invalid_invitees 36 | @invalid_invitees ||= 37 | invitee_list.reject { |item| item.match(EMAIL_REGEX) } 38 | end 39 | 40 | def invitee_list 41 | @invitee_list ||= @invitees.gsub(/\s+/, "").split(/[\n,;]+/) 42 | end 43 | 44 | def valid_message? 45 | @message.present? 46 | end 47 | 48 | def valid_invitees? 49 | invalid_invitees.empty? 50 | end 51 | 52 | def create_invitation(email) 53 | Invitation.create( 54 | casting: @casting, 55 | sender: @sender, 56 | invitee_email: email, 57 | status: "pending" 58 | ) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/fixtures/never.rb: -------------------------------------------------------------------------------- 1 | # This class is purely some 2 | # comments 3 | -------------------------------------------------------------------------------- /test/fixtures/sample.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = "baz" 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | 11 | # :nocov: 12 | def skipped 13 | @foo * 2 14 | end 15 | # :nocov: 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/skipped.rb: -------------------------------------------------------------------------------- 1 | # Not relevant 2 | # :nocov: 3 | # Hash.new 4 | # :nocov: 5 | -------------------------------------------------------------------------------- /test/fixtures/skipped_and_executed.rb: -------------------------------------------------------------------------------- 1 | # So much skippping 2 | # :nocov: 3 | class Foo 4 | def bar 5 | 0 6 | end 7 | end 8 | # :nocov: 9 | -------------------------------------------------------------------------------- /test/fixtures/utf-8.rb: -------------------------------------------------------------------------------- 1 | puts "135°C" 2 | -------------------------------------------------------------------------------- /test/forked/rails_full_stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../rails_test_helper", File.dirname(__FILE__)) 4 | 5 | class RailsFullStackTest < Minitest::Test 6 | include Capybara::DSL 7 | include Capybara::Minitest::Assertions 8 | 9 | def setup 10 | super 11 | rails_setup 12 | # preload first coverage hit 13 | Coverband.report_coverage 14 | require "rainbow" 15 | Rainbow("this text is red").red 16 | end 17 | 18 | def teardown 19 | super 20 | Capybara.reset_sessions! 21 | Capybara.use_default_driver 22 | end 23 | 24 | # We have to combine everything in one test 25 | # because we can only initialize rails once per test 26 | # run. Possibly fork test runs to avoid this problem in future? 27 | unless ENV["COVERBAND_MEMORY_TEST"] 28 | test "this is how we do it" do 29 | visit "/dummy/show" 30 | Coverband.report_coverage 31 | assert_content("I am no dummy") 32 | visit "/coverage" 33 | within page.find("a", text: /dummy_controller.rb/).find(:xpath, "../..") do 34 | assert_selector("td", text: "100.0 %") 35 | end 36 | 37 | # Test eager load data stored separately 38 | dummy_controller = "./app/controllers/dummy_controller.rb" 39 | store.type = :eager_loading 40 | eager_expected = [1, 1, 0, nil, nil] 41 | results = store.coverage[dummy_controller]["data"] 42 | assert_equal(eager_expected, results) 43 | 44 | store.type = Coverband::RUNTIME_TYPE 45 | runtime_expected = [0, 0, 1, nil, nil] 46 | results = store.coverage[dummy_controller]["data"] 47 | assert_equal(runtime_expected, results) 48 | end 49 | end 50 | 51 | ### 52 | # as we run it in single test mode via the benchmarks. 53 | # Add new tests below this test 54 | ### 55 | if ENV["COVERBAND_MEMORY_TEST"] 56 | test "memory usage" do 57 | return unless ENV["COVERBAND_MEMORY_TEST"] 58 | 59 | # we don't want this to run during our standard test suite 60 | # as the below profiler changes the runtime 61 | # and should only be included for isolated processes 62 | begin 63 | require "memory_profiler" 64 | 65 | # warmup 66 | 3.times do 67 | visit "/dummy/show" 68 | assert_content("I am no dummy") 69 | Coverband.report_coverage 70 | end 71 | 72 | previous_out = $stdout 73 | capture = StringIO.new 74 | $stdout = capture 75 | 76 | MemoryProfiler.report { 77 | 15.times do 78 | visit "/dummy/show" 79 | assert_content("I am no dummy") 80 | Coverband.report_coverage 81 | ### 82 | # Set to nil not {} as it is easier to verify that no memory is retained when nil gets released 83 | # don't use Coverband::Collectors::Delta.reset which sets to {} 84 | # 85 | # we clear this as this one variable is expected to retain memory and is a false positive 86 | ### 87 | Coverband::Collectors::Delta.class_variable_set(:@@previous_coverage, nil) 88 | # needed to test older versions to discover when we had the regression 89 | # Coverband::Collectors::Coverage.instance.send(:add_previous_results, nil) 90 | end 91 | }.pretty_print 92 | data = $stdout.string 93 | $stdout = previous_out 94 | if data.match(/retained objects by gem(.*)retained objects by file/m)[0]&.match(/coverband/) 95 | puts data 96 | raise "leaking memory!!!" 97 | end 98 | ensure 99 | $stdout = previous_out 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/forked/rails_full_stack_views_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../rails_test_helper", File.dirname(__FILE__)) 4 | 5 | class RailsFullStackTest < Minitest::Test 6 | include Capybara::DSL 7 | include Capybara::Minitest::Assertions 8 | 9 | def setup 10 | super 11 | rails_setup 12 | Coverband.report_coverage 13 | end 14 | 15 | def teardown 16 | super 17 | Capybara.reset_sessions! 18 | Capybara.use_default_driver 19 | end 20 | 21 | test "verify erb haml slim support" do 22 | visit "/dummy_view/show" 23 | assert_content("I am no dummy view tracker text") 24 | Coverband.report_coverage 25 | Coverband.configuration.view_tracker&.save_report 26 | Coverband.configuration.route_tracker&.save_report 27 | visit "/coverage/views_tracker" 28 | assert_content("Used Views: (1)") 29 | assert_content("Unused Views: (2)") 30 | assert_selector("li.used-keys", text: "dummy_view/show.html.erb") 31 | assert_selector("li.unused-keys", text: "dummy_view/show_haml.html.haml") 32 | assert_selector("li.unused-keys", text: "dummy_view/show_slim.html.slim") 33 | 34 | visit "/coverage/routes_tracker" 35 | assert_content("Used Routes: (1)") 36 | assert_content("Unused Routes: (4)") 37 | 38 | visit "/dummy_view/show_haml" 39 | assert_content("I am haml text") 40 | Coverband.report_coverage 41 | Coverband.configuration.view_tracker&.save_report 42 | visit "/coverage/views_tracker" 43 | assert_content("Used Views: (2)") 44 | assert_content("Unused Views: (1)") 45 | assert_selector("li.used-keys", text: "dummy_view/show_haml.html.haml") 46 | 47 | visit "/dummy_view/show_slim" 48 | assert_content("I am slim text") 49 | Coverband.report_coverage 50 | Coverband.configuration.view_tracker&.save_report 51 | visit "/coverage/views_tracker" 52 | assert_content("Used Views: (3)") 53 | assert_content("Unused Views: (0)") 54 | assert_selector("li.used-keys", text: "dummy_view/show_slim.html.slim") 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/forked/rails_rake_full_stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../rails_test_helper", File.dirname(__FILE__)) 4 | require "rails" 5 | 6 | class RailsRakeFullStackTest < Minitest::Test 7 | def setup 8 | super 9 | Coverband.configuration.reset 10 | Coverband.configure("./test/rails#{Rails::VERSION::MAJOR}_dummy/config/coverband.rb") 11 | end 12 | 13 | # test 'rake tasks shows coverage properly within eager_loading' do 14 | # this was testing gem data, which we no longer support and I dont know if this makes sense anymre 15 | # end 16 | 17 | test "ignored rake tasks don't add coverage" do 18 | store.clear! 19 | store.instance_variable_set(:@redis_namespace, "coverband_test") 20 | store.send(:save_report, basic_coverage_full_path) 21 | output = `COVERBAND_CONFIG=./test/rails#{Rails::VERSION::MAJOR}_dummy/config/coverband.rb bundle exec rake -f test/rails#{Rails::VERSION::MAJOR}_dummy/Rakefile coverband:clear` 22 | assert_nil output.match(/Coverband: Reported coverage via thread/) 23 | coverage_report = store.get_coverage_report 24 | empty_hash = {} 25 | assert_equal empty_hash, coverage_report[Coverband::RUNTIME_TYPE] 26 | assert_equal empty_hash, coverage_report[:eager_loading] 27 | assert_equal empty_hash, coverage_report[:merged] 28 | end 29 | 30 | test "doesn't exit non-zero with error on missing redis" do 31 | output = `COVERBAND_CONFIG=./test/rails#{Rails::VERSION::MAJOR}_dummy/config/coverband_missing_redis.rb bundle exec rake -f test/rails#{Rails::VERSION::MAJOR}_dummy/Rakefile -T` 32 | assert_equal 0, $?.to_i 33 | if ENV["COVERBAND_HASH_REDIS_STORE"] 34 | assert output.match(/Redis is not available/) 35 | else 36 | assert output.match(/coverage failed to store/) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/forked/rails_route_tracker_stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../rails_test_helper", File.dirname(__FILE__)) 4 | 5 | class RailsRoutesWithoutConfigStackTest < Minitest::Test 6 | def setup 7 | super 8 | setup_server 9 | end 10 | 11 | def teardown 12 | super 13 | shutdown_server 14 | end 15 | 16 | test "check route tracker" do 17 | output = `sleep 7 && curl http://localhost:9999/dummy_view/show` 18 | assert output.match(/rendered view/) 19 | assert output.match(/I am no dummy view tracker text/) 20 | output = `sleep 2 && curl http://localhost:9999/coverage/routes_tracker` 21 | assert output.match(/Used Routes: \(1\)/) 22 | assert output.match(/dummy_view\/show/) 23 | assert output.match(/GET/) 24 | end 25 | 26 | private 27 | 28 | # NOTE: We aren't leveraging Capybara because it loads all of our other test helpers and such, 29 | # which in turn Configures coverband making it impossible to test the configuration error 30 | def setup_server 31 | ENV["RAILS_ENV"] = "test" 32 | require "rails" 33 | fork do 34 | exec "cd test/rails#{Rails::VERSION::MAJOR}_dummy && COVERBAND_TEST=test bundle exec rackup config.ru -p 9999 --pid /tmp/testrack.pid" 35 | end 36 | end 37 | 38 | def shutdown_server 39 | if File.exist?("/tmp/testrack.pid") 40 | pid = `cat /tmp/testrack.pid`&.strip&.to_i 41 | Process.kill("HUP", pid) 42 | sleep 1 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/forked/rails_view_tracker_stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../rails_test_helper", File.dirname(__FILE__)) 4 | 5 | class RailsWithoutConfigStackTest < Minitest::Test 6 | def setup 7 | super 8 | setup_server 9 | end 10 | 11 | def teardown 12 | super 13 | shutdown_server 14 | end 15 | 16 | test "check view tracker" do 17 | output = `sleep 7 && curl http://localhost:9999/dummy_view/show` 18 | assert output.match(/rendered view/) 19 | assert output.match(/I am no dummy view tracker text/) 20 | output = `sleep 2 && curl http://localhost:9999/coverage/views_tracker` 21 | assert output.match(/Used Views: \(1\)/) 22 | assert output.match(/dummy_view\/show/) 23 | end 24 | 25 | private 26 | 27 | # NOTE: We aren't leveraging Capybara because it loads all of our other test helpers and such, 28 | # which in turn Configures coverband making it impossible to test the configuration error 29 | def setup_server 30 | ENV["RAILS_ENV"] = "test" 31 | require "rails" 32 | fork do 33 | exec "cd test/rails#{Rails::VERSION::MAJOR}_dummy && COVERBAND_TEST=test bundle exec rackup config.ru -p 9999 --pid /tmp/testrack.pid" 34 | end 35 | end 36 | 37 | def shutdown_server 38 | if File.exist?("/tmp/testrack.pid") 39 | pid = `cat /tmp/testrack.pid`&.strip&.to_i 40 | Process.kill("HUP", pid) 41 | sleep 1 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/integration/full_stack_deferred_eager_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | require "rack" 5 | 6 | class FullStackDeferredEagerTest < Minitest::Test 7 | REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION 8 | TEST_RACK_APP = "../fake_app/basic_rack.rb" 9 | 10 | def setup 11 | super 12 | Coverband::Collectors::Coverage.instance.reset_instance 13 | Coverband.configure do |config| 14 | config.background_reporting_enabled = false 15 | config.track_gems = true 16 | config.defer_eager_loading_data = true 17 | end 18 | Coverband.start 19 | Coverband::Collectors::Coverage.instance.eager_loading! 20 | @rack_file = require_unique_file "fake_app/basic_rack.rb" 21 | Coverband.report_coverage 22 | Coverband::Collectors::Coverage.instance.runtime! 23 | end 24 | 25 | test "call app" do 26 | # eager loaded class coverage starts empty 27 | Coverband.eager_loading_coverage! 28 | expected = {} 29 | assert_equal expected, Coverband.configuration.store.coverage 30 | 31 | Coverband::Collectors::Coverage.instance.runtime! 32 | request = Rack::MockRequest.env_for("/anything.json") 33 | middleware = Coverband::BackgroundMiddleware.new(fake_app_with_lines) 34 | results = middleware.call(request) 35 | assert_equal "Hello Rack!", results.last 36 | Coverband.report_coverage 37 | expected = [nil, nil, 0, nil, 0, 0, 1, nil, nil] 38 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 39 | 40 | # eager loaded class coverage is saved at first normal coverage report 41 | Coverband.eager_loading_coverage! 42 | expected = [nil, nil, 1, nil, 1, 1, 0, nil, nil] 43 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 44 | end 45 | 46 | private 47 | 48 | def fake_app_with_lines 49 | @fake_app_with_lines ||= ::HelloWorld.new 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/integration/full_stack_send_deferred_eager_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | require "rack" 5 | 6 | class FullStackSendDeferredEagerTest < Minitest::Test 7 | REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION 8 | TEST_RACK_APP = "../fake_app/basic_rack.rb" 9 | 10 | def setup 11 | super 12 | Coverband::Collectors::Coverage.instance.reset_instance 13 | Coverband.configure do |config| 14 | config.background_reporting_enabled = false 15 | config.track_gems = true 16 | config.defer_eager_loading_data = true 17 | config.send_deferred_eager_loading_data = false 18 | end 19 | Coverband.start 20 | Coverband::Collectors::Coverage.instance.eager_loading! 21 | @rack_file = require_unique_file "fake_app/basic_rack.rb" 22 | Coverband.report_coverage 23 | Coverband::Collectors::Coverage.instance.runtime! 24 | end 25 | 26 | test "call app" do 27 | # eager loaded class coverage starts empty 28 | Coverband.eager_loading_coverage! 29 | expected = {} 30 | assert_equal expected, Coverband.configuration.store.coverage 31 | 32 | Coverband::Collectors::Coverage.instance.runtime! 33 | request = Rack::MockRequest.env_for("/anything.json") 34 | middleware = Coverband::BackgroundMiddleware.new(fake_app_with_lines) 35 | results = middleware.call(request) 36 | assert_equal "Hello Rack!", results.last 37 | Coverband.report_coverage 38 | expected = [nil, nil, 0, nil, 0, 0, 1, nil, nil] 39 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 40 | 41 | # eager loaded class coverage is skipped at first normal coverage report 42 | Coverband.eager_loading_coverage! 43 | expected = {} 44 | assert_equal expected, Coverband.configuration.store.coverage 45 | end 46 | 47 | private 48 | 49 | def fake_app_with_lines 50 | @fake_app_with_lines ||= ::HelloWorld.new 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/integration/full_stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("../test_helper", File.dirname(__FILE__)) 4 | require "rack" 5 | 6 | class FullStackTest < Minitest::Test 7 | REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION 8 | TEST_RACK_APP = "../fake_app/basic_rack.rb" 9 | 10 | def setup 11 | super 12 | Coverband::Collectors::Coverage.instance.reset_instance 13 | Coverband.configure do |config| 14 | config.background_reporting_enabled = false 15 | config.track_gems = true 16 | end 17 | Coverband.start 18 | Coverband::Collectors::Coverage.instance.eager_loading! 19 | @rack_file = require_unique_file "fake_app/basic_rack.rb" 20 | Coverband.report_coverage 21 | Coverband::Collectors::Coverage.instance.runtime! 22 | end 23 | 24 | test "call app" do 25 | request = Rack::MockRequest.env_for("/anything.json") 26 | middleware = Coverband::BackgroundMiddleware.new(fake_app_with_lines) 27 | results = middleware.call(request) 28 | assert_equal "Hello Rack!", results.last 29 | Coverband.report_coverage 30 | expected = [nil, nil, 0, nil, 0, 0, 1, nil, nil] 31 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 32 | 33 | # additional calls increase count by 1 34 | middleware.call(request) 35 | Coverband.report_coverage 36 | expected = [nil, nil, 0, nil, 0, 0, 2, nil, nil] 37 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 38 | 39 | # class coverage 40 | Coverband.eager_loading_coverage! 41 | Coverband.configuration.store.coverage[@rack_file]["data"] 42 | expected = [nil, nil, 1, nil, 1, 1, 0, nil, nil] 43 | assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"] 44 | end 45 | 46 | private 47 | 48 | def fake_app_with_lines 49 | @fake_app_with_lines ||= ::HelloWorld.new 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/jruby_check.rb: -------------------------------------------------------------------------------- 1 | require "coverage" 2 | 3 | Coverage.start 4 | 5 | require "./test/dog" 6 | 7 | puts Coverage.peek_result 8 | 9 | puts Dog.new.bark 10 | 11 | puts Coverage.peek_result 12 | 13 | puts "done" 14 | -------------------------------------------------------------------------------- /test/rails7_dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/rails7_dummy/app/controllers/dummy_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyController < ActionController::Base 2 | def show 3 | render plain: "I am no dummy" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/rails7_dummy/app/controllers/dummy_view_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyViewController < ActionController::Base 2 | def show 3 | @text = "I am no dummy view tracker text" 4 | render layout: false 5 | end 6 | 7 | def show_haml 8 | @text = "I am haml text" 9 | render layout: false 10 | end 11 | 12 | def show_slim 13 | @text = "I am slim text" 14 | render layout: false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/rails7_dummy/app/views/dummy_view/show.html.erb: -------------------------------------------------------------------------------- 1 | rendered view 2 | 3 |
4 | <%= @text %> 5 |
6 | -------------------------------------------------------------------------------- /test/rails7_dummy/app/views/dummy_view/show_haml.html.haml: -------------------------------------------------------------------------------- 1 | rendered haml view 2 | 3 | #foo 4 | = @text 5 | -------------------------------------------------------------------------------- /test/rails7_dummy/app/views/dummy_view/show_slim.html.slim: -------------------------------------------------------------------------------- 1 | rendered slim view 2 | 3 | #content 4 | = @text 5 | -------------------------------------------------------------------------------- /test/rails7_dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path("../config/environment", __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("boot", __dir__) 4 | 5 | require "rails" 6 | require "action_controller/railtie" 7 | require "coverband" 8 | Bundler.require(*Rails.groups) 9 | 10 | module Rails6Dummy 11 | class Application < Rails::Application 12 | config.eager_load = true 13 | config.consider_all_requests_local = true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/coverband.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Coverband.configure do |config| 4 | # NOTE: we reuse this config in each of the fake rails projects 5 | # the below ensures the root is set to the correct fake project 6 | config.root = ::File.expand_path("../../../", __FILE__).to_s + "/rails#{Rails::VERSION::MAJOR}_dummy" 7 | config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2, url: ENV["REDIS_URL"]), redis_namespace: "coverband_test") if defined? Redis 8 | config.ignore = %w[.erb$ .slim$] 9 | config.root_paths = [] 10 | config.logger = Rails.logger 11 | config.verbose = true 12 | config.background_reporting_enabled = true 13 | config.track_routes = true 14 | config.use_oneshot_lines_coverage = true if ENV["ONESHOT"] 15 | end 16 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/coverband_missing_redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Coverband.configure do |config| 4 | config.root = Dir.pwd 5 | config.store = Coverband::Adapters::RedisStore.new(Redis.new(db: 2, url: "redis://127.0.0.1:123"), redis_namespace: "coverband_test") if defined? Redis 6 | config.ignore = %w[vendor .erb$ .slim$] 7 | config.root_paths = [] 8 | config.logger = Rails.logger 9 | config.verbose = true 10 | config.background_reporting_enabled = true 11 | config.track_gems = true 12 | config.gem_details = true 13 | config.use_oneshot_lines_coverage = true if ENV["ONESHOT"] 14 | end 15 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path("../application", __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get "dummy/show" 3 | get "dummy_view/show", to: "dummy_view#show" 4 | get "dummy_view/show_haml", to: "dummy_view#show_haml" 5 | get "dummy_view/show_slim", to: "dummy_view#show_slim" 6 | mount Coverband::Reporters::Web.new, at: "/coverage" 7 | end 8 | -------------------------------------------------------------------------------- /test/rails7_dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | test: 2 | secret_key_base: 8080f2894307a3dcf72127c0a279a729c58c7c10c11a15de761c8e16017e0e478647d1a7ac11bf143730cac7b6901fa000428c6b4873d9298250f8ca4657b5c6 3 | 4 | -------------------------------------------------------------------------------- /test/rails7_dummy/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/test/rails7_dummy/tmp/.keep -------------------------------------------------------------------------------- /test/rails8_dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/rails8_dummy/app/controllers/dummy_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyController < ActionController::Base 2 | def show 3 | render plain: "I am no dummy" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/rails8_dummy/app/controllers/dummy_view_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyViewController < ActionController::Base 2 | def show 3 | @text = "I am no dummy view tracker text" 4 | render layout: false 5 | end 6 | 7 | def show_haml 8 | @text = "I am haml text" 9 | render layout: false 10 | end 11 | 12 | def show_slim 13 | @text = "I am slim text" 14 | render layout: false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/rails8_dummy/app/views/dummy_view/show.html.erb: -------------------------------------------------------------------------------- 1 | rendered view 2 | 3 |
4 | <%= @text %> 5 |
6 | -------------------------------------------------------------------------------- /test/rails8_dummy/app/views/dummy_view/show_haml.html.haml: -------------------------------------------------------------------------------- 1 | rendered haml view 2 | 3 | #foo 4 | = @text 5 | -------------------------------------------------------------------------------- /test/rails8_dummy/app/views/dummy_view/show_slim.html.slim: -------------------------------------------------------------------------------- 1 | rendered slim view 2 | 3 | #content 4 | = @text 5 | -------------------------------------------------------------------------------- /test/rails8_dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path("../config/environment", __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("boot", __dir__) 4 | 5 | require "rails" 6 | require "action_controller/railtie" 7 | require "coverband" 8 | Bundler.require(*Rails.groups) 9 | 10 | module Rails6Dummy 11 | class Application < Rails::Application 12 | config.eager_load = true 13 | config.consider_all_requests_local = true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/coverband.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../rails7_dummy/config/coverband" 4 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/coverband_missing_redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../rails7_dummy/config/coverband_missing_redis" 4 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path("../application", __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get "dummy/show" 3 | get "dummy_view/show", to: "dummy_view#show" 4 | get "dummy_view/show_haml", to: "dummy_view#show_haml" 5 | get "dummy_view/show_slim", to: "dummy_view#show_slim" 6 | mount Coverband::Reporters::Web.new, at: "/coverage" 7 | end 8 | -------------------------------------------------------------------------------- /test/rails8_dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | test: 2 | secret_key_base: 8080f2894307a3dcf72127c0a279a729c58c7c10c11a15de761c8e16017e0e478647d1a7ac11bf143730cac7b6901fa000428c6b4873d9298250f8ca4657b5c6 3 | 4 | -------------------------------------------------------------------------------- /test/rails8_dummy/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmayer/coverband/d9db09b4e1e94cbcd15b392cf8c23454c9bd4853/test/rails8_dummy/tmp/.keep -------------------------------------------------------------------------------- /test/rails8_dummy/tmp/local_secret.txt: -------------------------------------------------------------------------------- 1 | c5de6abbe8f20a9a3ed005913ceb885dfc18906657583137cb54a4ac99f44be4104653f3d8b3385ae2e4099b84bb5910691d984f10e45228abaada5d7368da78 -------------------------------------------------------------------------------- /test/rails_test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest" 4 | require "minitest/fork_executor" 5 | 6 | # Forked executor includes autorun which does not work with qrush/m 7 | # https://github.com/qrush/m/issues/26 8 | # https://github.com/seattlerb/minitest/blob/master/lib/minitest/autorun.rb 9 | if defined?(M) 10 | Minitest.class_eval do 11 | def self.autorun 12 | puts "No autorunning" 13 | end 14 | end 15 | end 16 | 17 | Minitest.parallel_executor = Minitest::ForkExecutor.new 18 | require File.expand_path("./test_helper", File.dirname(__FILE__)) 19 | require "capybara" 20 | require "capybara/minitest" 21 | def rails_setup 22 | ENV["RAILS_ENV"] = "test" 23 | require "rails" 24 | # coverband must be required after rails 25 | Coverband.configure("./test/rails#{Rails::VERSION::MAJOR}_dummy/config/coverband.rb") 26 | load "coverband/utils/railtie.rb" 27 | 28 | require_relative "../test/rails#{Rails::VERSION::MAJOR}_dummy/config/environment" 29 | require "capybara/rails" 30 | # Our coverage report is wrapped in display:none as of now 31 | Capybara.ignore_hidden_elements = false 32 | require "mocha/minitest" 33 | end 34 | -------------------------------------------------------------------------------- /test/unique_files.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "securerandom" 4 | require "fileutils" 5 | require "erb" 6 | 7 | UNIQUE_FILES_DIR = "./test/unique_files" 8 | 9 | def require_unique_file(file = "dog.rb", variables = {}) 10 | uuid = SecureRandom.uuid 11 | dir = "#{UNIQUE_FILES_DIR}/#{uuid}" 12 | file_name = file.sub(".erb", "") 13 | temp_file = "#{dir}/#{file_name}" 14 | FileUtils.mkdir_p(Pathname.new(temp_file).dirname.to_s) 15 | file_contents = File.read("./test/#{file}") 16 | if variables.any? 17 | # Create a binding with the variables defined 18 | b = binding 19 | variables.each { |key, value| b.local_variable_set(key, value) } 20 | file_contents = ERB.new(file_contents).result(b) 21 | end 22 | File.write(temp_file, file_contents) 23 | require temp_file 24 | Coverband::Utils::RelativeFileConverter.convert(File.expand_path(temp_file)) 25 | end 26 | 27 | def require_class_unique_file 28 | @dogs ||= 0 29 | @dogs += 1 30 | require_unique_file("dog.rb.erb", dog_number: @dogs) 31 | end 32 | 33 | def remove_unique_files 34 | FileUtils.rm_r(UNIQUE_FILES_DIR) if File.exist?(UNIQUE_FILES_DIR) 35 | end 36 | 37 | if defined?(Minitest) 38 | Minitest.after_run do 39 | remove_unique_files 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /views/abstract_tracker.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coverband Info: <%= Coverband::VERSION %> 5 | 6 | 7 | 8 | ' media='screen, projection, print' rel='stylesheet' type='text/css'> 9 | 10 | 11 | 12 | 13 |
14 | <%= display_nav(active_link: tracker.route) %> 15 |
16 |

17 | <% if Coverband.configuration.web_enable_clear %> 18 | <%= button("#{base_path}clear_#{tracker.route}", "reset #{tracker.title} tracker", delete: true) %> 19 | <% end %> 20 |

21 |

Unused <%= tracker.title %>: (<%= tracker.unused_keys.length %>)

22 |

These <%= tracker.title %> have not been rendered since recording started at <%= tracker.tracking_since %>

23 | 28 | 29 |

Used <%= tracker.title %>: (<%= tracker.used_keys.length %>)

30 |

These <%= tracker.title %> have been rendered at least once

31 | 42 |
43 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /views/data.erb: -------------------------------------------------------------------------------- 1 | <%= result.source_files.to_json %> 2 | -------------------------------------------------------------------------------- /views/file_list.erb: -------------------------------------------------------------------------------- 1 |
2 | <% unless options[:skip_nav] %> 3 |

4 | <%= title %> 5 | <% unless page %> 6 | (<%= source_files.covered_percent.round(2) %>% 7 | covered at 8 | 9 | 10 | <%= source_files.covered_strength.round(2) %> 11 | 12 | hits/line) 13 | <% end %> 14 |

15 | <% end %> 16 | 17 |
18 | <% unless page %> 19 | <%= source_files.length %> files in total. 20 | <%= source_files.lines_of_code %> relevant lines. 21 | <%= source_files.covered_lines %> lines covered and 22 | <%= source_files.missed_lines %> lines missed 23 | <% end %> 24 |
25 | " data-coverageurl="<%= base_path %>"> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <% unless page %> 41 | <% source_files.each do |source_file| %> 42 | 43 | <% source_class = source_file.never_loaded ? 'strong red' : 'strong'%> 44 | 47 | 48 | <% runtime_percentage = result.runtime_relevant_coverage(source_file) %> 49 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | <% end %> 62 | <% end %> 63 | 64 |
File% covered% runtimeLinesRelevant LinesLines coveredLines runtimeLines missedAvg. Hits / Line
45 | <%= link_to_source_file(source_file) %> 46 | <%= source_file.covered_percent.round(2).to_s %> % strong"> 50 | <%= "#{runtime_percentage || '0'} %" %> 51 | <%= source_file.lines.count %><%= source_file.covered_lines.count + source_file.missed_lines.count %><%= source_file.covered_lines.count %> 56 | <%= result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.covered_lines_count || 0 %> 57 | <%= source_file.missed_lines.count %><%= source_file.covered_strength %>
65 |
66 | -------------------------------------------------------------------------------- /views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coverband: <%= Coverband::VERSION %> 5 | 6 | 7 | 8 | ' media='screen, projection, print' rel='stylesheet' type='text/css'> 9 | " /> 10 | 11 | 12 | 13 | 14 |
15 | loading 16 |
17 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /views/nav.erb: -------------------------------------------------------------------------------- 1 | 2 | 13 | <% if notice.to_s.length > 0 %> 14 |
<%= notice %>
15 | <% end %> 16 |
Generated <%= timeago(Time.now) %>
17 |
18 | <% if defined?(result) %> 19 |
20 | Coverage Recording Started: <%= timeago(result.source_files.first_seen_at) %> 21 |
22 | <% end %> 23 | 24 | 26 | 27 | 47 | -------------------------------------------------------------------------------- /views/settings.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coverband Info: <%= Coverband::VERSION %> 5 | 6 | 7 | ' media='screen, projection, print' rel='stylesheet' type='text/css'> 8 | 9 | 10 | 11 | 12 |
13 | 17 |
18 |
19 | <% Coverband.configuration.to_h.each_pair do |key,value| %> 20 |
<%= key %>
21 |
<%= value %>
22 | <% end %> 23 | 24 |
Size (in bytes)
25 |
<%= Coverband.configuration.store.size %>
26 |
Size (in MiB)
27 |
<%= Coverband.configuration.store.size_in_mib %>
28 |
29 |
30 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /views/source_file.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= shortened_filename source_file %>

4 |

5 | 6 | <%= source_file.covered_percent.round(2).to_s %> % 7 | 8 | covered, 9 | 10 | 11 | <%= result.runtime_relevant_coverage(source_file) %> % 12 | 13 | runtime covered 14 | 15 | <% if Coverband.configuration.web_enable_clear %> 16 | <%= button("#{base_path}clear_file?filename=#{source_file.relative_path}", 'clear file coverage') %>   17 | <% end %> 18 | 19 | <% if source_file.never_loaded %> 20 |
21 | 22 | This file was never loaded during app runtime or loading (or was loaded before Coverband loaded)! 23 | 24 | <% end %> 25 |

26 |
27 | <%= source_file.lines_of_code %> relevant lines. 28 | <%= result.runtime_relavent_lines(source_file) %> runtime relevant lines. 29 | <%= source_file.covered_lines.count %> lines covered and 30 | <%= source_file.missed_lines.count %> lines missed. 31 |
32 |
33 | Coverage first seen: <%= source_file.first_updated_at %>, last activity recorded: 34 | <%= source_file.last_updated_at %> 35 |
36 |
37 | 38 |
39 |     
    40 | <% source_file.lines.each_with_index do |line, index| %> 41 |
  1. 42 | <% if line.covered? %> 43 | load: 44 | <%= result.file_with_type(source_file, Coverband::EAGER_TYPE)&.line_coverage(index) || 0 %>, 45 | runtime: 46 | <%= result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.line_coverage(index) || 0 %> 47 | all: <%= line.coverage %> 48 | last posted: <%= timeago(result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.line_coverage_posted(index), "-") %> 49 | <% end %> 50 | <% if line.skipped? %>skipped<% end %> 51 | <%= CGI.escapeHTML(line.src.chomp) %> 52 |
  2. 53 | <% end %> 54 |
55 |
56 |
57 | --------------------------------------------------------------------------------