Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hk issue 9 #14

Merged
merged 33 commits into from Dec 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c3b4ea1
Move to an array of reporters in anticipation of there being multiple
hanneskaeufler Dec 5, 2018
9f7c2c1
Conform io reporter to imaginary interface
hanneskaeufler Dec 5, 2018
bac5cfa
First passing test for stryker dashboard reporter
hanneskaeufler Dec 5, 2018
de6bc8e
Create reporter namespace and move http client into it
hanneskaeufler Dec 5, 2018
2bca6eb
Explain stryker reporter
hanneskaeufler Dec 5, 2018
e185a7a
Use env vars to configure the http call
hanneskaeufler Dec 5, 2018
4d18dcd
Fix formatting
hanneskaeufler Dec 5, 2018
5333d03
Conform io reporter to reporter interface
hanneskaeufler Dec 5, 2018
3e03846
Conform badge reporter as well
hanneskaeufler Dec 5, 2018
2e21beb
Use the correct protocol to use both ENV and a hash
hanneskaeufler Dec 5, 2018
78c563f
Push broken code to see failure on CI
hanneskaeufler Dec 5, 2018
ab0cdd8
Comment for now
hanneskaeufler Dec 5, 2018
566e27d
Add mutation score badge
hanneskaeufler Dec 5, 2018
7cbf48d
Fully describe reporter interface
hanneskaeufler Dec 5, 2018
0b58072
Remove empty method overload, be stricter with types
hanneskaeufler Dec 5, 2018
28da7d4
Unclutter the fake process runner
hanneskaeufler Dec 6, 2018
ef1a6f3
Allow stryker reporter being used
hanneskaeufler Dec 6, 2018
80bdacf
Allow to inject the generator to be able to test the msi reporting
hanneskaeufler Dec 6, 2018
5961bdf
Rename files according to classnames
hanneskaeufler Dec 6, 2018
8ee155d
Fix formatting
hanneskaeufler Dec 6, 2018
5aac21d
Bit of cleanup
hanneskaeufler Dec 6, 2018
50ad573
Move io reporter into the reporter folder
hanneskaeufler Dec 6, 2018
402c873
Move stryker reporter into proper folder
hanneskaeufler Dec 6, 2018
3afd715
Fix faulty import
hanneskaeufler Dec 6, 2018
87ba680
Determine branch by env var as well
hanneskaeufler Dec 6, 2018
e401093
Duh, need to pass the reporters to ctor. Thats what happens if you do…
hanneskaeufler Dec 6, 2018
77b4449
Add debug stuff
hanneskaeufler Dec 6, 2018
ce124a3
Am I not setting the env var correctly?
hanneskaeufler Dec 6, 2018
2b94662
Explain mutation badge. Closes #9
hanneskaeufler Dec 6, 2018
11b31bb
Improve readme for badge
hanneskaeufler Dec 6, 2018
49514e2
Run mutations for io reporter
hanneskaeufler Dec 6, 2018
14d3388
Run mutations on CI
hanneskaeufler Dec 6, 2018
3c7fc9b
Remove debug of response
hanneskaeufler Dec 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Expand Up @@ -11,6 +11,7 @@ jobs:
- run: ./bin/code-style
- run: SPEC_VERBOSE=1 ./bin/test
- run: SPEC_VERBOSE=1 ./bin/test-coverage
- run: ./bin/test-mutations
test-crystal-0.27.0:
<<: *test-template
docker:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- `--min-msi` cli argument to allow passing the suite (exiting with 0) even when there are mutants that survived. Pass as float like `--min-msi=75.0`.
- Post MSI score to [stryker dashboard](https://dashboard.stryker-mutator.io) if env vars are set.

### Changed
- NumberLiteralChange mutant now outputs 0 (for everything != 0) and 1 (for 0)
Expand Down
15 changes: 14 additions & 1 deletion README.md
@@ -1,4 +1,4 @@
[![CircleCI](https://circleci.com/gh/hanneskaeufler/crytic/tree/master.svg?style=svg)](https://circleci.com/gh/hanneskaeufler/crytic/tree/master)
[![CircleCI](https://circleci.com/gh/hanneskaeufler/crytic/tree/master.svg?style=svg)](https://circleci.com/gh/hanneskaeufler/crytic/tree/master) ![Mutation Score](https://badge.stryker-mutator.io/github.com/hanneskaeufler/crytic/master)
hanneskaeufler marked this conversation as resolved.
Show resolved Hide resolved

# crytic

Expand Down Expand Up @@ -76,6 +76,19 @@ Each following occurance of `✅` shows that a mutant has been killed, ergo that

`❌ NumberLiteralSignFlip` is signaling that indeed a mutation was not detected. The diff below shows the change that was made which was not caught by the test suite.

### Mutation Badge

To show a badge about your mutation testing efforts like at the top of this readme you can make use of the [dashboard](https://dashboard.stryker-mutator.io) of stryker by letting crytic post the msi score to the stryker api. To do that, make sure to have the following env vars set:

```
CIRCLE_BRANCH => "master",
CIRCLE_PROJECT_REPONAME => "crytic",
CIRCLE_PROJECT_USERNAME => "hanneskaeufler",
STRYKER_DASHBOARD_API_KEY => "apikey",
```

It is currently limited to work with Circle CI and assumes your project is hosted on GitHub.

### Available mutants

There are many ways a code-base can be modified to introduce arbitrary failures. Crytic only provides mutators which keep the code compiling (at least in theory).
Expand Down
2 changes: 1 addition & 1 deletion bin/test-mutations
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

./bin/crytic --subject src/crytic/io_reporter.cr spec/io_reporter_spec.cr
crystal ./src/crytic.cr -- --min-msi=20.0 --subject src/crytic/reporter/io_reporter.cr spec/reporter/io_reporter_spec.cr
15 changes: 4 additions & 11 deletions spec/mutation/fake_process_runner.cr
Expand Up @@ -2,28 +2,21 @@ require "../../src/crytic/process_runner"

module Crytic
class FakeProcessRunner < ProcessRunner
private getter cmd
private getter args
property exit_code
property timeout
@timeout = [] of Time::Span
property exit_code : Int32 = 0
property timeout = [] of Time::Span
@cmd = [] of String
@args = [] of String
@output_io = IO::Memory.new

def cmd_with_args
cmd.zip(args).map { |c, a| "#{c} #{a}".strip }
end

def initialize
@exit_code = 0
@cmd.zip(@args).map { |c, a| "#{c} #{a}".strip }
end

def run(cmd : String, args : Array(String), output, error)
@cmd << cmd
@args << args.join(" ")
output << @output_io.to_s
@exit_code
exit_code
end

def run(cmd : String, args : Array(String), output, error, timeout)
Expand Down
19 changes: 14 additions & 5 deletions spec/io_reporter_spec.cr → spec/reporter/io_reporter_spec.cr
@@ -1,7 +1,7 @@
require "../src/crytic/io_reporter"
require "../src/crytic/mutant/number_literal_change"
require "../src/crytic/mutation/original_result"
require "./spec_helper"
require "../../src/crytic/mutant/number_literal_change"
require "../../src/crytic/mutation/original_result"
require "../../src/crytic/reporter/io_reporter"
require "../spec_helper"

private def fake_mutant
Crytic::Mutant::NumberLiteralChange.at(Crystal::Location.new(filename: nil, line_number: 0, column_number: 0))
Expand All @@ -11,7 +11,7 @@ private def original(exit_code = 0, output = "output")
Crytic::Mutation::OriginalResult.new(exit_code: exit_code, output: output)
end

module Crytic
module Crytic::Reporter
describe IoReporter do
describe "#report_original_result" do
it "prints the original passing suites status" do
Expand Down Expand Up @@ -89,5 +89,14 @@ module Crytic
io.to_s.should contain "Mutation Score Indicator (MSI): N/A"
end
end

describe "#report_msi" do
it "is a noop" do
io = IO::Memory.new
results = [] of Mutation::Result
IoReporter.new(io).report_msi(results).should eq nil
io.to_s.should eq ""
end
end
end
end
58 changes: 58 additions & 0 deletions spec/reporter/stryker_badge_reporter_spec.cr
@@ -0,0 +1,58 @@
require "../../src/crytic/mutant/number_literal_change"
require "../../src/crytic/mutation/result"
require "../../src/crytic/reporter/http_client"
require "../../src/crytic/reporter/stryker_badge_reporter"
require "../spec_helper"
require "json"

module Crytic::Reporter
describe StrykerBadgeReporter do
describe "#report_msi" do
it "posts to the stryker dashboard" do
client = FakeClient.new

StrykerBadgeReporter.new(client, {
"CIRCLE_BRANCH" => "master",
"CIRCLE_PROJECT_REPONAME" => "crytic",
"CIRCLE_PROJECT_USERNAME" => "hanneskaeufler",
"STRYKER_DASHBOARD_API_KEY" => "apikey",
}).report_msi(results)

client.path.should eq "https://dashboard.stryker-mutator.io/api/reports"
client.body.should eq({
"apiKey" => "apikey",
"branch" => "master",
"mutationScore" => 100.0,
"repositorySlug" => "github.com/hanneskaeufler/crytic",
})
end
end
end
end

private class FakeClient < Crytic::Reporter::HttpClient
def post(url : String, bbody : Hash(String, String | Float64))
@path = url
@body = bbody
end

def path
@path
end

def body
@body
end
end

private def fake_mutant
Crytic::Mutant::NumberLiteralChange.at(
Crystal::Location.new(filename: nil, line_number: 0, column_number: 0))
end

private def results
[Crytic::Mutation::Result.new(
status: Crytic::Mutation::Status::Covered,
mutant: fake_mutant,
diff: "")]
end
38 changes: 38 additions & 0 deletions spec/runner_spec.cr
Expand Up @@ -17,5 +17,43 @@ describe Crytic::Runner do
Crytic::Runner.new.run("./fixtures/simple/bar.cr", ["./nope_spec.cr"])
end
end

it "reports events in order" do
reporter = FakeReporter.new
runner = Crytic::Runner.new(
generator: FakeGenerator.new,
reporters: [reporter] of Crytic::Reporter::Reporter)

runner.run("./fixtures/simple/bar.cr", ["./fixtures/simple/bar_spec.cr"])

reporter.events.should eq ["report_original_result", "report_summary", "report_msi"]
end
end
end

private class FakeReporter < Crytic::Reporter::Reporter
getter events
@events = [] of String

def report_original_result(original_result)
@events << "report_original_result"
end

def report_result(result)
@events << "report_result"
end

def report_summary(results)
@events << "report_summary"
end

def report_msi(results)
@events << "report_msi"
end
end

private class FakeGenerator < Crytic::Generator
def mutations_for(source : String, specs : Array(String))
[] of Crytic::Mutation::Mutation
end
end
21 changes: 19 additions & 2 deletions src/crytic.cr
@@ -1,5 +1,8 @@
require "option_parser"
require "./crytic/reporter/http_client"
require "./crytic/reporter/io_reporter"
require "./crytic/reporter/stryker_badge_reporter"
require "./crytic/runner"
require "option_parser"

subject_source = ""
msi_threshold = 100.0
Expand All @@ -22,8 +25,22 @@ OptionParser.parse! do |parser|
end
end

reporters = [Crytic::Reporter::IoReporter.new(STDOUT)] of Crytic::Reporter::Reporter

if ENV["STRYKER_DASHBOARD_API_KEY"]?
client = Crytic::Reporter::DefaultHttpClient.new
reporters << Crytic::Reporter::StrykerBadgeReporter.new(client, {
# manually map from ENV to a Hash because I am unable to conform ENV
# to anything that I can replace with a stub in the tests
"CIRCLE_BRANCH" => ENV["CIRCLE_BRANCH"],
"CIRCLE_PROJECT_REPONAME" => ENV["CIRCLE_PROJECT_REPONAME"],
"CIRCLE_PROJECT_USERNAME" => ENV["CIRCLE_PROJECT_USERNAME"],
"STRYKER_DASHBOARD_API_KEY" => ENV["STRYKER_DASHBOARD_API_KEY"],
})
end

success = Crytic::Runner
.new(threshold: msi_threshold)
.new(threshold: msi_threshold, reporters: reporters)
.run(subject_source, spec_files)

exit(success ? 0 : 1)
5 changes: 5 additions & 0 deletions src/crytic/generator/generator.cr
@@ -0,0 +1,5 @@
module Crytic
abstract class Generator
abstract def mutations_for(source : String, specs : Array(String))
end
end
@@ -1,5 +1,7 @@
require "./generator"

module Crytic
class Generator
class InMemoryMutationsGenerator < Generator
MUTANT_POSSIBILITIES = [
Mutant::AndOrSwapPossibilities.new,
Mutant::BoolLiteralFlipPossibilities.new,
Expand Down
17 changes: 17 additions & 0 deletions src/crytic/reporter/http_client.cr
@@ -0,0 +1,17 @@
require "http/client"
require "json"

module Crytic::Reporter
abstract class HttpClient
abstract def post(url : String, body : Hash(String, String | Float64))
end

class DefaultHttpClient < HttpClient
def post(url : String, body : Hash(String, String | Float64))
HTTP::Client.post(
url,
headers: HTTP::Headers{"Content-Type" => "application/json"},
body: body.to_json)
end
end
end
13 changes: 9 additions & 4 deletions src/crytic/io_reporter.cr → src/crytic/reporter/io_reporter.cr
@@ -1,10 +1,11 @@
require "./msi_calculator"
require "./mutation/mutation"
require "../msi_calculator"
require "../mutation/mutation"
require "./reporter"
require "spec/dsl"

module Crytic
module Crytic::Reporter
# Reports crytics output into an IO. Useful for e.g. the console output
class IoReporter
class IoReporter < Reporter
INDENT = " "

def initialize(@io : IO, @start_time = Time.now)
Expand Down Expand Up @@ -59,6 +60,10 @@ module Crytic
@io << summary.colorize(results.map(&.status.covered?).all? ? :green : :red).to_s
end

# intentional noop
def report_msi(results)
end

private def score_in_percent(results)
return "N/A" if results.empty?
"#{MsiCalculator.new(results).msi}%"
Expand Down
8 changes: 8 additions & 0 deletions src/crytic/reporter/reporter.cr
@@ -0,0 +1,8 @@
module Crytic::Reporter
abstract class Reporter
abstract def report_original_result(original_result)
abstract def report_result(result)
abstract def report_summary(results)
abstract def report_msi(results)
end
end
40 changes: 40 additions & 0 deletions src/crytic/reporter/stryker_badge_reporter.cr
@@ -0,0 +1,40 @@
require "../msi_calculator"
require "./http_client"
require "./reporter"

module Crytic::Reporter
# Sends a MSI score to the stryker dashboard
# See also https://infection.github.io/guide/mutation-badge.html
class StrykerBadgeReporter < Reporter
private DASHBOARD_URL = "https://dashboard.stryker-mutator.io/api/reports"

def initialize(@client : HttpClient, @env : Hash(String, String))
end

def report_msi(results)
@client.post(DASHBOARD_URL, {
"apiKey" => @env["STRYKER_DASHBOARD_API_KEY"],
"repositorySlug" => slug,
"branch" => @env["CIRCLE_BRANCH"],
"mutationScore" => score(results),
})
end

def report_original_result(original_result)
end

def report_result(result)
end

def report_summary(results)
end

private def slug
"github.com/#{@env["CIRCLE_PROJECT_USERNAME"]}/#{@env["CIRCLE_PROJECT_REPONAME"]}"
end

private def score(results)
MsiCalculator.new(results).msi
end
end
end