diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/reports/store/actions.js | 9 | ||||
-rw-r--r-- | app/controllers/projects/merge_requests_controller.rb | 17 | ||||
-rw-r--r-- | app/models/ci/build.rb | 28 | ||||
-rw-r--r-- | app/models/ci/job_artifact.rb | 20 | ||||
-rw-r--r-- | app/models/ci/pipeline.rb | 12 | ||||
-rw-r--r-- | app/models/merge_request.rb | 42 | ||||
-rw-r--r-- | app/serializers/merge_request_widget_entity.rb | 6 | ||||
-rw-r--r-- | app/serializers/test_case_entity.rb | 7 | ||||
-rw-r--r-- | app/serializers/test_reports_comparer_entity.rb | 11 | ||||
-rw-r--r-- | app/serializers/test_reports_comparer_serializer.rb | 3 | ||||
-rw-r--r-- | app/serializers/test_suite_comparer_entity.rb | 14 |
11 files changed, 169 insertions, 0 deletions
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js index 15c077b0fd8..edbf860ecc6 100644 --- a/app/assets/javascripts/reports/store/actions.js +++ b/app/assets/javascripts/reports/store/actions.js @@ -1,4 +1,5 @@ import Visibility from 'visibilityjs'; +import $ from 'jquery'; import axios from '../../lib/utils/axios_utils'; import Poll from '../../lib/utils/poll'; import * as types from './mutation_types'; @@ -63,5 +64,13 @@ export const receiveReportsSuccess = ({ commit }, response) => export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR); +export const openModal = ({ dispatch }, payload) => { + dispatch('setModalData', payload); + + $('#modal-mrwidget-reports').modal('show'); +}; + +export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dc6551fc761..410fb92eb7c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -99,6 +99,23 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo } end + def test_reports + result = @merge_request.compare_test_reports + + Gitlab::PollingInterval.set_header(response, interval: 10_000) + + case result[:status] + when :parsing + render json: '', status: :no_content + when :parsed + render json: result[:data], status: :ok + when :error + render json: { status_reason: result[:status_reason] }, status: :bad_request + else + render json: { status_reason: 'Unknown error' }, status: :internal_server_error + end + end + def edit define_edit_vars end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 93bbee49c09..bf931a04592 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -69,6 +69,11 @@ module Ci where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) end + scope :with_test_reports, ->() do + includes(:job_artifacts_junit) # Prevent N+1 problem when iterating each ci_job_artifact row + .where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').test_reports) + end + scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } @@ -627,8 +632,31 @@ module Ci running? && runner_session_url.present? end + def collect_test_reports!(test_reports) + raise ArgumentError, 'build does not have test reports' unless has_test_reports? + + test_reports.get_suite(group_name).tap do |test_suite| + each_test_report do |file_type, blob| + parse_test_report!(test_suite, file_type, blob) + end + end + end + private + def each_test_report + Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type| + public_send("job_artifacts_#{file_type}").each_blob do |blob| # rubocop:disable GitlabSecurity/PublicSend + yield file_type, blob + end + end + end + + def parse_test_report!(test_suite, file_type, blob) + "Gitlab::Ci::Parsers::#{file_type.capitalize}Parser".constantize + .new(blob).parse!(test_suite) + end + def update_artifacts_size self.artifacts_size = legacy_artifacts_file&.size end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 054b714f8ac..50f375f0804 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -4,6 +4,8 @@ module Ci include ObjectStorage::BackgroundMove extend Gitlab::Ci::Model + NotSupportedAdapterError = Class.new(StandardError) + TEST_REPORT_FILE_TYPES = %w[junit].freeze DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze @@ -44,6 +46,10 @@ module Ci gzip: 3 } + FILE_FORMAT_ADAPTERS = { + gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter + }.freeze + def valid_file_format? unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym errors.add(:file_format, 'Invalid file format with specified file type') @@ -75,8 +81,22 @@ module Ci end end + def each_blob(&blk) + unless file_format_adapter_class + raise NotSupportedAdapterError, 'This file format requires a dedicated adapter' + end + + file.open do |stream| + file_format_adapter_class.new(stream).each_blob(&blk) + end + end + private + def file_format_adapter_class + FILE_FORMAT_ADAPTERS[file_format.to_sym] + end + def set_size self.size = file.size end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e5caa3ffa41..f06a6416307 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -603,6 +603,18 @@ module Ci @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a end + def has_test_reports? + complete? && builds.with_test_reports.any? + end + + def test_reports + Gitlab::Ci::Reports::TestReports.new.tap do |test_reports| + builds.with_test_reports.each do |build| + build.collect_test_reports!(test_reports) + end + end + end + private def ci_yaml_from_repo diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 06642c15585..9f8ebd9b4ca 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -13,6 +13,11 @@ class MergeRequest < ActiveRecord::Base include ThrottledTouch include Gitlab::Utils::StrongMemoize include LabelEventable + include ReactiveCaching + + self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } + self.reactive_cache_refresh_interval = 1.hour + self.reactive_cache_lifetime = 1.hour ignore_column :locked_at, :ref_fetched, @@ -1012,6 +1017,37 @@ class MergeRequest < ActiveRecord::Base .order(id: :desc) end + def has_test_reports? + actual_head_pipeline&.has_test_reports? + end + + def compare_test_reports + unless actual_head_pipeline && actual_head_pipeline.has_test_reports? + return { status: :error, status_reason: 'head pipeline does not have test reports' } + end + + with_reactive_cache(base_pipeline&.iid, actual_head_pipeline.iid) { |data| data } || { status: :parsing } + end + + def calculate_reactive_cache(base_pipeline_iid, head_pipeline_iid) + begin + base_pipeline = project.pipelines.find_by_iid(base_pipeline_iid) + head_pipeline = project.pipelines.find_by_iid(head_pipeline_iid) + + comparer = Gitlab::Ci::Reports::TestReportsComparer + .new(base_pipeline&.test_reports, head_pipeline.test_reports) + + { + status: :parsed, + data: TestReportsComparerSerializer + .new(project: project) + .represent(comparer).to_json + } + rescue => e + { status: :error, status_reason: e.message } + end + end + def all_commits # MySQL doesn't support LIMIT in a subquery. diffs_relation = if Gitlab::Database.postgresql? @@ -1124,6 +1160,12 @@ class MergeRequest < ActiveRecord::Base true end + def base_pipeline + @base_pipeline ||= project.pipelines + .order(id: :desc) + .find_by(sha: diff_base_sha) + end + def discussions_rendered_on_frontend? true end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 63fd9d63ec4..f55d448235a 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -231,6 +231,12 @@ class MergeRequestWidgetEntity < IssuableEntity end end + expose :test_reports_path do |merge_request| + if merge_request.has_test_reports? + test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json) + end + end + private delegate :current_user, to: :request diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb new file mode 100644 index 00000000000..5c1cbf37182 --- /dev/null +++ b/app/serializers/test_case_entity.rb @@ -0,0 +1,7 @@ +class TestCaseEntity < Grape::Entity + expose :status + expose :name + expose :execution_time + expose :system_output + expose :stack_trace +end diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb new file mode 100644 index 00000000000..b95d820d093 --- /dev/null +++ b/app/serializers/test_reports_comparer_entity.rb @@ -0,0 +1,11 @@ +class TestReportsComparerEntity < Grape::Entity + expose :total_status, as: :status + + expose :summary do + expose :total_count, as: :total + expose :resolved_count, as: :resolved + expose :failed_count, as: :failed + end + + expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity +end diff --git a/app/serializers/test_reports_comparer_serializer.rb b/app/serializers/test_reports_comparer_serializer.rb new file mode 100644 index 00000000000..a739858efb2 --- /dev/null +++ b/app/serializers/test_reports_comparer_serializer.rb @@ -0,0 +1,3 @@ +class TestReportsComparerSerializer < BaseSerializer + entity TestReportsComparerEntity +end diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb new file mode 100644 index 00000000000..a3965ba3930 --- /dev/null +++ b/app/serializers/test_suite_comparer_entity.rb @@ -0,0 +1,14 @@ +class TestSuiteComparerEntity < Grape::Entity + expose :name + expose :total_status, as: :status + + expose :summary do + expose :total_count, as: :total + expose :resolved_count, as: :resolved + expose :failed_count, as: :failed + end + + expose :new_failures, using: TestCaseEntity + expose :resolved_failures, using: TestCaseEntity + expose :existing_failures, using: TestCaseEntity +end |