Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ require 'rake/testtask'

Rake::TestTask.new do |t|
t.libs.push "lib"
t.test_files = FileList['test/*_test.rb']
t.libs.push "test"
t.test_files = FileList['test/**/*_test.rb']
t.verbose = true
end

Expand Down
101 changes: 101 additions & 0 deletions lib/cc/formatters/snapshot_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module CC::Formatters
module SnapshotFormatter
# Simple Comparator for rating letters.
class Rating
include Comparable

def initialize(letter)
@letter = letter
end

def <=>(other)
other.to_s <=> to_s
end

def hash
@letter.hash
end

def eql?(other)
to_s == other.to_s
end

def inspect
"<Rating:#{to_s}>"
end

def to_s
@letter.to_s
end
end

C = Rating.new("C")
D = Rating.new("D")

# SnapshotFormatter::Base takes the quality information from the payload and divides it
# between alerts and improvements.
#
# The information in the payload must be a comparison in time between two quality reports, aka snapshot.
# This information is in the payload when the service receive a `receive_snapshot` and also
# when it receives a `receive_test`. In this latest case, the comparison is between today and seven days ago.
class Base
attr_reader :alert_constants_payload, :improved_constants_payload, :details_url, :compare_url

def initialize(payload)
new_constants = Array(payload["new_constants"])
changed_constants = Array(payload["changed_constants"])

alert_constants = new_constants.select(&new_constants_selector)
alert_constants += changed_constants.select(&decreased_constants_selector)

improved_constants = changed_constants.select(&improved_constants_selector)

data = {
"from" => { "commit_sha" => payload["previous_commit_sha"] },
"to" => { "commit_sha" => payload["commit_sha"] }
}

@alert_constants_payload = data.merge("constants" => alert_constants) if alert_constants.any?
@improved_constants_payload = data.merge("constants" => improved_constants) if improved_constants.any?
end

private

def new_constants_selector
Proc.new { |constant| to_rating(constant) < C }
end

def decreased_constants_selector
Proc.new { |constant| from_rating(constant) > D && to_rating(constant) < C }
end

def improved_constants_selector
Proc.new { |constant| from_rating(constant) < C && to_rating(constant) > from_rating(constant) }
end

def to_rating(constant)
Rating.new(constant["to"]["rating"])
end

def from_rating(constant)
Rating.new(constant["from"]["rating"])
end
end

# Override the base snapshot formatter for be more lax grouping information.
# This is useful to show more information for testing the service.
class Sample < Base
def new_constants_selector
Proc.new { |_| true }
end

def decreased_constants_selector
Proc.new { |constant| to_rating(constant) < from_rating(constant) }
end

def improved_constants_selector
Proc.new { |constant| to_rating(constant) > from_rating(constant) }
end
end
end
end
17 changes: 13 additions & 4 deletions lib/cc/helpers/quality_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ def previous_remediation_cost
payload.fetch("previous_remediation_cost", 0)
end

def with_article(letter)
def with_article(letter, bold = false)
letter ||= '?'

if %w( A F ).include?(letter)
"an #{letter}"
text = bold ? "*#{letter}*" : letter
if %w( A F ).include?(letter.to_s)
"an #{text}"
else
"a #{letter}"
"a #{text}"
end
end

def constant_basename(name)
if name.include?(".")
File.basename(name)
else
name
end
end
end
6 changes: 4 additions & 2 deletions lib/cc/service/helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module CC::Service::Helper
GREEN_HEX = "#38ae6f"
RED_HEX = "#ed2f00"

def repo_name
payload["repo_name"]
Expand Down Expand Up @@ -30,9 +32,9 @@ def color

def hex_color
if improved?
"#38ae6f"
GREEN_HEX
else
"#ed2f00"
RED_HEX
end
end

Expand Down
72 changes: 68 additions & 4 deletions lib/cc/services/slack.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# encoding: UTF-8

class CC::Service::Slack < CC::Service
include CC::Service::QualityHelper

class Config < CC::Service::Config
attribute :webhook_url, String,
label: "Webhook URL",
Expand All @@ -13,17 +17,20 @@ class Config < CC::Service::Config
def receive_test
speak(formatter.format_test)

# payloads for test receivers include the weekly quality report.
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Sample.new(payload))

{ ok: true, message: "Test message sent" }
rescue => ex
{ ok: false, message: ex.message }
end

def receive_coverage
speak(formatter.format_coverage, hex_color)
def receive_snapshot
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Base.new(payload))
end

def receive_quality
speak(formatter.format_quality, hex_color)
def receive_coverage
speak(formatter.format_coverage, hex_color)
end

def receive_vulnerability
Expand Down Expand Up @@ -51,4 +58,61 @@ def speak(message, color = nil)
http.headers['Content-Type'] = 'application/json'
http_post(config.webhook_url, body.to_json)
end

def send_snapshot_to_slack(snapshot)
if snapshot.alert_constants_payload
speak(alerts_message(snapshot.alert_constants_payload), RED_HEX)
end

if snapshot.improved_constants_payload
speak(improvements_message(snapshot.improved_constants_payload), GREEN_HEX)
end
end

def alerts_message(constants_payload)
constants = constants_payload["constants"]
message = ["Quality alert triggered for *#{repo_name}* (<#{compare_url}|Compare>)\n"]

constants[0..2].each do |constant|
object_identifier = constant_basename(constant["name"])

if constant["from"]
from_rating = constant["from"]["rating"]
to_rating = constant["to"]["rating"]

message << "• _#{object_identifier}_ just declined from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
else
rating = constant["to"]["rating"]

message << "• _#{object_identifier}_ was just created and is #{with_article(rating, :bold)}"
end
end

if constants.size > 3
remaining = constants.size - 3
message << "\nAnd <#{details_url}|#{remaining} other #{"change".pluralize(remaining)}>"
end

message.join("\n")
end

def improvements_message(constants_payload)
constants = constants_payload["constants"]
message = ["Quality improvements in *#{repo_name}* (<#{compare_url}|Compare>)\n"]

constants[0..2].each do |constant|
object_identifier = constant_basename(constant["name"])
from_rating = constant["from"]["rating"]
to_rating = constant["to"]["rating"]

message << "• _#{object_identifier}_ just improved from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
end

if constants.size > 3
remaining = constants.size - 3
message << "\nAnd <#{details_url}|#{remaining} other #{"improvement".pluralize(remaining)}>"
end

message.join("\n")
end
end
47 changes: 47 additions & 0 deletions test/formatters/snapshot_formatter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "helper"

class TestSnapshotFormatter < Test::Unit::TestCase
def described_class
CC::Formatters::SnapshotFormatter::Base
end

def test_quality_alert_with_new_constants
f = described_class.new({"new_constants" => [{"to" => {"rating" => "D"}}], "changed_constants" => []})
refute_nil f.alert_constants_payload
end

def test_quality_alert_with_decreased_constants
f = described_class.new({"new_constants" => [],
"changed_constants" => [{"to" => {"rating" => "D"}, "from" => {"rating" => "A"}}]
})
refute_nil f.alert_constants_payload
end

def test_quality_improvements_with_better_ratings
f = described_class.new({"new_constants" => [],
"changed_constants" => [{"to" => {"rating" => "A"}, "from" => {"rating" => "D"}}]
})
refute_nil f.improved_constants_payload
end

def test_nothing_set_without_changes
f = described_class.new({"new_constants" => [], "changed_constants" => []})
assert_nil f.alert_constants_payload
assert_nil f.improved_constants_payload
end

def test_snapshot_formatter_test_with_relaxed_constraints
f = CC::Formatters::SnapshotFormatter::Sample.new({
"new_constants" => [{"name" => "foo", "to" => {"rating" => "A"}}, {"name" => "bar", "to" => {"rating" => "A"}}],
"changed_constants" => [
{"from" => {"rating" => "B"}, "to" => {"rating" => "C"}},
{"from" => {"rating" => "D"}, "to" => {"rating" => "D"}},
{"from" => {"rating" => "D"}, "to" => {"rating" => "D"}},
{"from" => {"rating" => "A"}, "to" => {"rating" => "B"}},
{"from" => {"rating" => "C"}, "to" => {"rating" => "B"}}
]})

refute_nil f.alert_constants_payload
refute_nil f.improved_constants_payload
end
end
Loading