├── .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 |
24 | <% tracker.unused_keys.each do |key| %>
25 | - <%= key %>
26 | <% end %>
27 |
28 |
29 |
Used <%= tracker.title %>: (<%= tracker.used_keys.length %>)
30 |
These <%= tracker.title %> have been rendered at least once
31 |
32 | <% tracker.used_keys.each_pair do |key, time_at| %>
33 | -
34 | <%= key %>
35 | last activity recorded <%= Time.at(time_at.to_i)%>
36 | <% if Coverband.configuration.web_enable_clear %>
37 | <%= button("#{base_path}clear_#{tracker.route}_key?key=#{key}", 'reset tracked key', delete: true) %>
38 | <% end %>
39 |
40 | <% end %>
41 |
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 | File |
29 | % covered |
30 | % runtime |
31 | Lines |
32 | Relevant Lines |
33 | Lines covered |
34 | Lines runtime |
35 | Lines missed |
36 | Avg. Hits / Line |
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 |
45 | <%= link_to_source_file(source_file) %>
46 | |
47 | <%= source_file.covered_percent.round(2).to_s %> % |
48 | <% runtime_percentage = result.runtime_relevant_coverage(source_file) %>
49 | strong">
50 | <%= "#{runtime_percentage || '0'} %" %>
51 | |
52 | <%= source_file.lines.count %> |
53 | <%= source_file.covered_lines.count + source_file.missed_lines.count %> |
54 | <%= source_file.covered_lines.count %> |
55 |
56 | <%= result.file_with_type(source_file, Coverband::RUNTIME_TYPE)&.covered_lines_count || 0 %>
57 | |
58 | <%= source_file.missed_lines.count %> |
59 | <%= source_file.covered_strength %> |
60 |
61 | <% end %>
62 | <% end %>
63 |
64 |
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 |
 %>)
16 |
17 |
18 | <%= display_nav %>
19 |
20 |
21 | <%= formatted_file_list("Coverage", result, result.source_files) %>
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 | loading source data...
32 |
33 |
 %>)
34 |
35 |
36 |
37 |
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 |
37 |
38 |
39 |
40 | <% source_file.lines.each_with_index do |line, index| %>
41 | -
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 |
53 | <% end %>
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------