Commit 632e91a1 authored by Maxime Orefice's avatar Maxime Orefice Committed by Shinya Maeda

Add codequality reports

This MR adds the necessary logic to parse codequality report from
a job artifact and read it a the pipeline level.
parent 945cc9c5
......@@ -923,6 +923,14 @@ module Ci
coverage_report
end
def collect_codequality_reports!(codequality_report)
each_report(Ci::JobArtifact::CODEQUALITY_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
end
codequality_report
end
def collect_terraform_reports!(terraform_reports)
each_report(::Ci::JobArtifact::TERRAFORM_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, terraform_reports, artifact: report_artifact)
......
......@@ -16,6 +16,7 @@ module Ci
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
CODEQUALITY_REPORT_FILE_TYPES = %w[codequality].freeze
ACCESSIBILITY_REPORT_FILE_TYPES = %w[accessibility].freeze
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze
......@@ -157,6 +158,10 @@ module Ci
with_file_types(COVERAGE_REPORT_FILE_TYPES)
end
scope :codequality_reports, -> do
with_file_types(CODEQUALITY_REPORT_FILE_TYPES)
end
scope :terraform_reports, -> do
with_file_types(TERRAFORM_REPORT_FILE_TYPES)
end
......
......@@ -956,6 +956,14 @@ module Ci
end
end
def codequality_reports
Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build|
build.collect_codequality_reports!(codequality_reports)
end
end
end
def terraform_reports
::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build|
......
{
"description": "Codequality used by codeclimate parser",
"type": "object",
"required": ["description", "fingerprint", "severity", "location"],
"properties": {
"description": { "type": "string" },
"fingerprint": { "type": "string" },
"severity": { "type": "string" },
"location": {
"type": "object",
"properties": {
"path": { "type": "string" },
"lines": {
"type": "object",
"properties": {
"begin": { "type": "integer" }
}
},
"positions": {
"type": "object",
"properties": {
"begin": {
"type": "object",
"properties": {
"line": { "type": "integer" }
}
}
}
}
}
}
},
"additionalProperties": true
}
......@@ -198,8 +198,8 @@ RSpec.describe 'Pipeline', :js do
it 'shows code quality issue with link to file' do
wait_for_requests
expect(page).to have_content('Function `simulateEvent` has 28 lines of code (exceeds 25 allowed). Consider refactoring.')
expect(find_link('app/assets/javascripts/test_utils/simulate_drag.js:1')[:href]).to end_with(project_blob_path(project, File.join(pipeline.commit.id, 'app/assets/javascripts/test_utils/simulate_drag.js')) + '#L1')
expect(page).to have_content('Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.')
expect(find_link('foo.rb:10')[:href]).to end_with(project_blob_path(project, File.join(pipeline.commit.id, 'foo.rb')) + '#L10')
end
it 'contains events for data tracking', :aggregate_failures do
......
......@@ -10,7 +10,8 @@ module Gitlab
junit: ::Gitlab::Ci::Parsers::Test::Junit,
cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura,
terraform: ::Gitlab::Ci::Parsers::Terraform::Tfplan,
accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y
accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y,
codequality: ::Gitlab::Ci::Parsers::Codequality::CodeClimate
}
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Codequality
class CodeClimate
def parse!(json_data, codequality_report)
root = Gitlab::Json.parse(json_data)
parse_all(root, codequality_report)
rescue JSON::ParserError => e
codequality_report.set_error_message("JSON parsing failed: #{e}")
end
private
def parse_all(root, codequality_report)
return unless root.present?
root.each do |degradation|
break unless codequality_report.add_degradation(degradation)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
class CodequalityReports
attr_reader :degradations, :error_message
CODECLIMATE_SCHEMA_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'codeclimate.json').to_s
def initialize
@degradations = {}.with_indifferent_access
@error_message = nil
end
def add_degradation(degradation)
valid_degradation?(degradation) && @degradations[degradation.dig('fingerprint')] = degradation
end
def set_error_message(error)
@error_message = error
end
def degradations_count
@degradations.size
end
def all_degradations
@degradations.values
end
private
def valid_degradation?(degradation)
JSON::Validator.validate!(CODECLIMATE_SCHEMA_PATH, degradation)
rescue JSON::Schema::ValidationError => e
set_error_message("Invalid degradation format: #{e.message}")
false
end
end
end
end
end
......@@ -356,6 +356,12 @@ FactoryBot.define do
end
end
trait :codequality_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :codequality, job: build)
end
end
trait :terraform_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :terraform, job: build)
......
......@@ -245,7 +245,17 @@ FactoryBot.define do
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/codequality/codequality.json'), 'application/json')
Rails.root.join('spec/fixtures/codequality/codeclimate.json'), 'application/json')
end
end
trait :codequality_without_errors do
file_type { :codequality }
file_format { :raw }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/codequality/codeclimate_without_errors.json'), 'application/json')
end
end
......
......@@ -145,6 +145,14 @@ FactoryBot.define do
end
end
trait :with_codequality_reports do
status { :success }
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ci_build, :codequality_reports, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_coverage_report_artifact do
after(:build) do |pipeline, evaluator|
pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, pipeline: pipeline, project: pipeline.project)
......
[
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
},
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_backwards_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "f3bdc1e8c102ba5fbd9e7f6cda51c95e",
"location": {
"path": "foo.rb",
"lines": {
"begin": 14,
"end": 14
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
},
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"categories": [
"Complexity"
],
"remediation_points": 550000,
"location": {
"path": "foo.rb",
"positions": {
"begin": {
"column": 24,
"line": 14
},
"end": {
"column": 49,
"line": 14
}
}
},
"content": {
"body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
},
"engine_name": "rubocop",
"fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
"severity": "minor"
}
]
This diff is collapsed.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
describe '#parse!' do
subject(:parse) { described_class.new.parse!(code_climate, codequality_report) }
let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:code_climate) do
[
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
}
].to_json
end
context "when data is code_climate style JSON" do
context "when there are no degradations" do
let(:code_climate) { [].to_json }
it "returns a codequality report" do
expect { parse }.not_to raise_error
expect(codequality_report.degradations_count).to eq(0)
end
end
context "when there are degradations" do
it "returns a codequality report" do
expect { parse }.not_to raise_error
expect(codequality_report.degradations_count).to eq(1)
end
end
end
context "when data is not a valid JSON string" do
let(:code_climate) do
[
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
}
]
end
it "sets error_message" do
expect { parse }.not_to raise_error
expect(codequality_report.error_message).to include('JSON parsing failed')
end
end
context 'when degradations contain an invalid one' do
let(:code_climate) do
[
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
"severity": "minor"
},
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
}
].to_json
end
it 'stops parsing the report' do
expect { parse }.not_to raise_error
expect(codequality_report.degradations_count).to eq(0)
expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'")
end
end
end
end
......@@ -30,6 +30,14 @@ RSpec.describe Gitlab::Ci::Parsers do
end
end
context 'when file_type is codequality' do
let(:file_type) { 'codequality' }
it 'fabricates the class' do
is_expected.to be_a(described_class::Codequality::CodeClimate)
end
end
context 'when file_type is terraform' do
let(:file_type) { 'terraform' }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
let(:codequality_report) { described_class.new }
let(:degradation_1) do
{
"categories": [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
}.with_indifferent_access
end
let(:degradation_2) do
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"categories": [
"Complexity"
],
"remediation_points": 550000,
"location": {
"path": "foo.rb",
"positions": {
"begin": {
"column": 14,
"line": 10
},
"end": {
"column": 39,
"line": 10
}
}
},
"content": {
"body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
},
"engine_name": "rubocop",
"fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
"severity": "minor"
}.with_indifferent_access
end
it { expect(codequality_report.degradations).to eq({}) }
describe '#add_degradation' do
context 'when there is a degradation' do
before do
codequality_report.add_degradation(degradation_1)
end
it 'adds degradation to codequality report' do
expect(codequality_report.degradations.keys).to eq([degradation_1[:fingerprint]])
expect(codequality_report.degradations.values.size).to eq(1)
end
end
context 'when a required property is missing in the degradation' do
let(:invalid_degradation) do
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
"severity": "minor"
}.with_indifferent_access
end
it 'sets location as an error' do
codequality_report.add_degradation(invalid_degradation)
expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'")
end
end
end
describe '#set_error_message' do
context 'when there is an error' do
it 'sets errors' do
codequality_report.set_error_message("error")
expect(codequality_report.error_message).to eq("error")
end
end
end
describe '#degradations_count' do
subject(:degradations_count) { codequality_report.degradations_count }
context 'when there are many degradations' do
before do
codequality_report.add_degradation(degradation_1)
codequality_report.add_degradation(degradation_2)
end
it 'returns the number of degradations' do
expect(degradations_count).to eq(2)
end
end
end
describe '#all_degradations' do
subject(:all_degradations) { codequality_report.all_degradations }
context 'when there are many degradations' do
before do
codequality_report.add_degradation(degradation_1)
codequality_report.add_degradation(degradation_2)
end
it 'returns all degradations' do
expect(all_degradations).to contain_exactly(degradation_1, degradation_2)
end
end
end
end
......@@ -4071,6 +4071,38 @@ RSpec.describe Ci::Build do
end
end
describe '#collect_codequality_reports!' do
subject(:codequality_report) { build.collect_codequality_reports!(Gitlab::Ci::Reports::CodequalityReports.new) }
it { expect(codequality_report.degradations).to eq({}) }
context 'when build has a codequality report' do
context 'when there is a codequality report' do
before do
create(:ci_job_artifact, :codequality, job: build, project: build.project)
end
it 'parses blobs and add the results to the codequality report' do
expect { codequality_report }.not_to raise_error
expect(codequality_report.degradations_count).to eq(3)
end
end
context 'when there is an codequality report without errors' do
before do
create(:ci_job_artifact, :codequality_without_errors, job: build, project: build.project)
end
it 'parses blobs and add the results to the codequality report' do
expect { codequality_report }.not_to raise_error
expect(codequality_report.degradations_count).to eq(0)
end
end
end
end
describe '#collect_terraform_reports!' do
let(:terraform_reports) { Gitlab::Ci::Reports::TerraformReports.new }
......
......@@ -96,6 +96,22 @@ RSpec.describe Ci::JobArtifact do
end
end
describe '.codequality_reports' do
subject { described_class.codequality_reports }
context 'when there is a codequality report' do
let!(:artifact) { create(:ci_job_artifact, :codequality) }
it { is_expected.to eq([artifact]) }
end
context 'when there are no codequality reports' do
let!(:artifact) { create(:ci_job_artifact, :archive) }
it { is_expected.to be_empty }
end
end
describe '.terraform_reports' do
context 'when there is a terraform report' do
it 'return the job artifact' do
......
......@@ -498,6 +498,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
context 'when pipeline has a codequality report' do
subject { described_class.with_reports(Ci::JobArtifact.codequality_reports) }
let(:pipeline_with_report) { create(:ci_pipeline, :with_codequality_reports) }
it 'selects the pipeline' do
is_expected.to eq([pipeline_with_report])
end
end
context 'when pipeline has a terraform report' do
it 'selects the pipeline' do
pipeline_with_report = create(:ci_pipeline, :with_terraform_reports)
......@@ -3360,6 +3370,39 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
describe '#codequality_reports' do
subject(:codequality_reports) { pipeline.codequality_reports }
context 'when pipeline has multiple builds with codequality reports' do
let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) }
let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) }
before do
create(:ci_job_artifact, :codequality, job: build_rspec, project: project)
create(:ci_job_artifact, :codequality_without_errors, job: build_golang, project: project)
end
it 'returns codequality report with collected data' do
expect(codequality_reports.degradations_count).to eq(3)
end
context 'when builds are retried' do
let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) }
let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) }
it 'returns a codequality reports without degradations' do
expect(codequality_reports.degradations).to be_empty
end
end
end
context 'when pipeline does not have any builds with codequality reports' do
it 'returns codequality reports without degradations' do
expect(codequality_reports.degradations).to be_empty
end
end
end
describe '#total_size' do
let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment