Skip to content

Commit

Permalink
Merge pull request #98 from DataDog/anmarchenko/codeowners
Browse files Browse the repository at this point in the history
[CIVIS-3035] CODEOWNERS support for test visibility
  • Loading branch information
anmarchenko authored Jan 9, 2024
2 parents f3be528 + 8e59911 commit dc38a5b
Show file tree
Hide file tree
Showing 27 changed files with 631 additions and 2 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @DataDog/ruby-guild @DataDog/ci-app-libraries
102 changes: 102 additions & 0 deletions lib/datadog/ci/codeowners/matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# frozen_string_literal: true

require_relative "rule"

module Datadog
module CI
module Codeowners
# Responsible for matching a test source file path to a list of owners
class Matcher
def initialize(codeowners_file_path)
@rules = parse(codeowners_file_path)
@rules.reverse!
end

def list_owners(file_path)
# treat all file paths that we check as absolute from the repository root
file_path = "/#{file_path}" unless file_path.start_with?("/")

Datadog.logger.debug { "Matching file path #{file_path} to CODEOWNERS rules" }

@rules.each do |rule|
if rule.match?(file_path)
Datadog.logger.debug { "Matched rule [#{rule.pattern}] with owners #{rule.owners}" }
return rule.owners
end
end

Datadog.logger.debug { "CODEOWNERS rule not matched" }
nil
end

private

def parse(codeowners_file_path)
unless File.exist?(codeowners_file_path)
Datadog.logger.debug { "CODEOWNERS file not found at #{codeowners_file_path}" }
return []
end

result = []
section_default_owners = []

File.open(codeowners_file_path, "r") do |f|
f.each_line do |line|
line.strip!

next if line.empty?
next if comment?(line)

pattern, *line_owners = line.strip.split(/\s+/)
next if pattern.nil? || pattern.empty?

# if the current line starts with section record the default owners for this section
if section?(pattern)
section_default_owners = line_owners
next
end

pattern = expand_pattern(pattern)
# if the current line doesn't have any owners then use the default owners for this section
if line_owners.empty? && !section_default_owners.empty?
line_owners = section_default_owners
end

result << Rule.new(pattern, line_owners)
end
end

result
rescue => e
Datadog.logger.warn(
"Failed to parse codeowners file at #{codeowners_file_path}: " \
"#{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
)
[]
end

def comment?(line)
line.start_with?("#")
end

def section?(line)
line.start_with?("[", "^[") && line.end_with?("]")
end

def expand_pattern(pattern)
return pattern if pattern == "*"

# if pattern ends with a slash then it matches everything deeply nested in this directory
pattern += "**" if pattern.end_with?(::File::SEPARATOR)

# if pattern doesn't start with a slash then it matches anywhere in the repository
if !pattern.start_with?(::File::SEPARATOR, "**#{::File::SEPARATOR}", "*#{::File::SEPARATOR}")
pattern = "**#{::File::SEPARATOR}#{pattern}"
end

pattern
end
end
end
end
end
42 changes: 42 additions & 0 deletions lib/datadog/ci/codeowners/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require_relative "matcher"

module Datadog
module CI
module Codeowners
# Responsible for parsing a CODEOWNERS file
class Parser
DEFAULT_LOCATION = "CODEOWNERS"
POSSIBLE_CODEOWNERS_LOCATIONS = [
"CODEOWNERS",
".github/CODEOWNERS",
".gitlab/CODEOWNERS",
"docs/CODEOWNERS"
].freeze

def initialize(root_file_path)
@root_file_path = root_file_path || Dir.pwd
end

def parse
default_path = File.join(@root_file_path, DEFAULT_LOCATION)
# We are using the first codeowners file that we find or
# default location if nothing is found
#
# Matcher handles it internally and creates a class with
# an empty list of rules if the file is not found
codeowners_file_path = POSSIBLE_CODEOWNERS_LOCATIONS.map do |codeowners_location|
File.join(@root_file_path, codeowners_location)
end.find do |path|
File.exist?(path)
end || default_path

::Datadog.logger.debug { "Using CODEOWNERS file from: #{codeowners_file_path}" }

Matcher.new(codeowners_file_path)
end
end
end
end
end
33 changes: 33 additions & 0 deletions lib/datadog/ci/codeowners/rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Datadog
module CI
module Codeowners
class Rule
attr_reader :pattern, :owners

def initialize(pattern, owners)
@pattern = pattern
@owners = owners
end

def match?(file_path)
res = false
# if pattern does not end with a separator or a wildcard, it could be either a directory or a file
if !pattern.end_with?(::File::SEPARATOR, "*")
directory_pattern = "#{pattern}#{::File::SEPARATOR}*"
res ||= File.fnmatch?(directory_pattern, file_path, flags)
end

res ||= File.fnmatch?(pattern, file_path, flags)
res
end

private

def flags
return ::File::FNM_PATHNAME if pattern.end_with?("#{::File::SEPARATOR}*")
0
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/datadog/ci/ext/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Test
TAG_COMMAND = "test.command"
TAG_SOURCE_FILE = "test.source.file"
TAG_SOURCE_START = "test.source.start"
TAG_CODEOWNERS = "test.codeowners"

TEST_TYPE = "test"

Expand Down
7 changes: 7 additions & 0 deletions lib/datadog/ci/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def test_module_id
def test_session_id
get_tag(Ext::Test::TAG_TEST_SESSION_ID)
end

# Source file path of the test relative to git repository root.
# @return [String] the source file path of the test
# @return [nil] if the source file path is not found
def source_file
get_tag(Ext::Test::TAG_SOURCE_FILE)
end
end
end
end
16 changes: 15 additions & 1 deletion lib/datadog/ci/test_visibility/recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
require_relative "context/global"
require_relative "context/local"

require_relative "../codeowners/parser"
require_relative "../ext/app_types"
require_relative "../ext/test"
require_relative "../ext/environment"
require_relative "../utils/git"

require_relative "../span"
require_relative "../null_span"
Expand All @@ -27,12 +29,16 @@ module TestVisibility
class Recorder
attr_reader :environment_tags, :test_suite_level_visibility_enabled

def initialize(test_suite_level_visibility_enabled: false)
def initialize(
test_suite_level_visibility_enabled: false,
codeowners: Codeowners::Parser.new(Utils::Git.root).parse
)
@test_suite_level_visibility_enabled = test_suite_level_visibility_enabled

@environment_tags = Ext::Environment.tags(ENV).freeze
@local_context = Context::Local.new
@global_context = Context::Global.new
@codeowners = codeowners
end

def start_test_session(service: nil, tags: {})
Expand Down Expand Up @@ -206,6 +212,8 @@ def build_test(tracer_span, tags)
test = Test.new(tracer_span)
set_initial_tags(test, tags)
validate_test_suite_level_visibility_correctness(test)
set_codeowners(test)

test
end

Expand Down Expand Up @@ -251,6 +259,12 @@ def set_module_context(tags, test_module = nil)
end
end

def set_codeowners(test)
source = test.source_file
owners = @codeowners.list_owners(source) if source
test.set_tag(Ext::Test::TAG_CODEOWNERS, owners) unless owners.nil?
end

def set_suite_context(tags, span: nil, name: nil)
return if span.nil? && name.nil?

Expand Down
23 changes: 23 additions & 0 deletions sig/datadog/ci/codeowners/matcher.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Datadog
module CI
module Codeowners
class Matcher
@rules: Array[Rule]

def initialize: (String codeowners_file_path) -> void

def list_owners: (String file_path) -> Array[String]?

private

def parse: (String file_path) -> Array[Rule]

def comment?: (String line) -> bool

def section?: (String line) -> bool

def expand_pattern: (String pattern) -> String
end
end
end
end
17 changes: 17 additions & 0 deletions sig/datadog/ci/codeowners/parser.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Datadog
module CI
module Codeowners
class Parser
@root_file_path: String

DEFAULT_LOCATION: "CODEOWNERS"

POSSIBLE_CODEOWNERS_LOCATIONS: ::Array["CODEOWNERS" | ".github/CODEOWNERS" | ".gitlab/CODEOWNERS" | "docs/CODEOWNERS"]

def initialize: (String? root_file_path) -> void

def parse: () -> Datadog::CI::Codeowners::Matcher
end
end
end
end
23 changes: 23 additions & 0 deletions sig/datadog/ci/codeowners/rule.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Datadog
module CI
module Codeowners
class Rule
@pattern: String

@owners: Array[String]

attr_reader pattern: String

attr_reader owners: Array[String]

def initialize: (String pattern, Array[String] owners) -> void

def match?: (String file_path) -> untyped

private

def flags: () -> Integer
end
end
end
end
2 changes: 2 additions & 0 deletions sig/datadog/ci/ext/test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ module Datadog

TAG_SOURCE_START: String

TAG_CODEOWNERS: String

TAG_TEST_SESSION_ID: String

TAG_TEST_MODULE_ID: String
Expand Down
1 change: 1 addition & 0 deletions sig/datadog/ci/test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Datadog
def test_suite_name: () -> String?
def test_module_id: () -> String?
def test_session_id: () -> String?
def source_file: () -> String?
end
end
end
5 changes: 4 additions & 1 deletion sig/datadog/ci/test_visibility/recorder.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ module Datadog
@environment_tags: Hash[String, String]
@local_context: Datadog::CI::TestVisibility::Context::Local
@global_context: Datadog::CI::TestVisibility::Context::Global
@codeowners: Datadog::CI::Codeowners::Matcher

@null_span: Datadog::CI::NullSpan

attr_reader environment_tags: Hash[String, String]
attr_reader test_suite_level_visibility_enabled: bool

def initialize: (?test_suite_level_visibility_enabled: bool) -> void
def initialize: (?test_suite_level_visibility_enabled: bool, ?codeowners: Datadog::CI::Codeowners::Matcher) -> void

def trace_test: (String span_name, String test_suite_name, ?service: String?, ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped

Expand Down Expand Up @@ -70,6 +71,8 @@ module Datadog

def set_module_context: (Hash[untyped, untyped] tags, ?Datadog::CI::TestModule | Datadog::Tracing::SpanOperation? test_module) -> void

def set_codeowners: (Datadog::CI::Test test) -> void

def null_span: () -> Datadog::CI::Span

def skip_tracing: (?untyped block) -> untyped
Expand Down
Loading

0 comments on commit dc38a5b

Please sign in to comment.