gitlab_quality-test_tooling 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -4
  3. data/Gemfile.lock +1 -1
  4. data/Guardfile +0 -22
  5. data/README.md +150 -9
  6. data/exe/generate-test-session +50 -0
  7. data/exe/post-to-slack +58 -0
  8. data/exe/prepare-stage-reports +38 -0
  9. data/exe/relate-failure-issue +59 -0
  10. data/exe/report-results +56 -0
  11. data/exe/update-screenshot-paths +38 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +194 -0
  13. data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +26 -0
  14. data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +51 -0
  15. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +75 -0
  16. data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +49 -0
  17. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +275 -0
  18. data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +78 -0
  19. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +377 -0
  20. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +134 -0
  21. data/lib/gitlab_quality/test_tooling/report/report_results.rb +83 -0
  22. data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +130 -0
  23. data/lib/gitlab_quality/test_tooling/report/results_in_testcases.rb +113 -0
  24. data/lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb +81 -0
  25. data/lib/gitlab_quality/test_tooling/runtime/env.rb +113 -0
  26. data/lib/gitlab_quality/test_tooling/runtime/logger.rb +92 -0
  27. data/lib/gitlab_quality/test_tooling/runtime/token_finder.rb +44 -0
  28. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +36 -0
  29. data/lib/gitlab_quality/test_tooling/summary_table.rb +41 -0
  30. data/lib/gitlab_quality/test_tooling/support/http_request.rb +34 -0
  31. data/lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb +65 -0
  32. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +21 -0
  33. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +21 -0
  34. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +21 -0
  35. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +21 -0
  36. data/lib/gitlab_quality/test_tooling/system_logs/log_types/log.rb +38 -0
  37. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/api_log.rb +34 -0
  38. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/application_log.rb +27 -0
  39. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/exception_log.rb +23 -0
  40. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb +30 -0
  41. data/lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb +29 -0
  42. data/lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb +65 -0
  43. data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +39 -0
  44. data/lib/gitlab_quality/test_tooling/test_results/builder.rb +35 -0
  45. data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +27 -0
  46. data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +29 -0
  47. data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +184 -0
  48. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  49. data/lib/gitlab_quality/test_tooling.rb +11 -2
  50. metadata +51 -3
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ # Uses the API to create or update GitLab test result issues with the results of tests from RSpec report files.
7
+ class ResultsInIssues < ReportAsIssue
8
+ include Concerns::ResultsReporter
9
+
10
+ def initialize(**kwargs)
11
+ super
12
+ @issue_type = 'issue'
13
+ end
14
+
15
+ def get_related_issue(testcase, test)
16
+ issue = find_linked_results_issue_by_iid(testcase, test)
17
+ is_new = false
18
+
19
+ if issue
20
+ issue = update_issue_title(issue, test) if issue_title_needs_updating?(issue, test)
21
+ else
22
+ puts "No valid issue link found"
23
+ issue = find_or_create_results_issue(test)
24
+ is_new = true
25
+ end
26
+
27
+ [issue, is_new]
28
+ end
29
+
30
+ def update_issue(issue, test)
31
+ new_labels = issue_labels(issue)
32
+ new_labels |= ['Testcase Linked']
33
+
34
+ labels_updated = update_labels(issue, test, new_labels)
35
+ note_posted = note_status(issue, test)
36
+
37
+ if labels_updated || note_posted
38
+ puts "Issue updated: #{issue.web_url}"
39
+ else
40
+ puts "Test passed, no results issue update needed."
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def find_linked_results_issue_by_iid(testcase, _test)
47
+ iid = issue_iid_from_testcase(testcase)
48
+
49
+ return unless iid
50
+
51
+ find_issue_by_iid(iid)
52
+ end
53
+
54
+ def find_or_create_results_issue(test)
55
+ find_issue(test) || create_issue(test)
56
+ end
57
+
58
+ def issue_iid_from_testcase(testcase)
59
+ results = testcase.description.partition(TEST_CASE_RESULTS_SECTION_TEMPLATE).last if testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE)
60
+
61
+ return puts "No issue link found" unless results
62
+
63
+ issue_iid = results.split('/').last
64
+
65
+ issue_iid&.to_i
66
+ end
67
+
68
+ def note_status(issue, test)
69
+ return false if test.skipped
70
+ return false if test.failures.empty?
71
+
72
+ note = note_content(test)
73
+
74
+ gitlab.find_issue_discussions(iid: issue.iid).each do |discussion|
75
+ if new_note_matches_discussion?(
76
+ note, discussion)
77
+ return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id,
78
+ body: failure_summary)
79
+ end
80
+ end
81
+
82
+ gitlab.create_issue_note(iid: issue.iid, note: note)
83
+
84
+ true
85
+ end
86
+
87
+ def note_content(test)
88
+ errors = test.failures.each_with_object([]) do |failure, text|
89
+ text << <<~TEXT
90
+ Error:
91
+ ```
92
+ #{failure['message']}
93
+ ```
94
+
95
+ Stacktrace:
96
+ ```
97
+ #{failure['stacktrace']}
98
+ ```
99
+ TEXT
100
+ end.join("\n\n")
101
+
102
+ "#{failure_summary}\n\n#{errors}"
103
+ end
104
+
105
+ def failure_summary
106
+ summary = [":x: ~\"#{pipeline}::failed\""]
107
+ summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"
108
+ summary.join(' ')
109
+ end
110
+
111
+ def new_note_matches_discussion?(note, discussion)
112
+ note_error = error_and_stack_trace(note)
113
+ discussion_error = error_and_stack_trace(discussion.notes.first['body'])
114
+
115
+ return false if note_error.empty? || discussion_error.empty?
116
+
117
+ note_error == discussion_error
118
+ end
119
+
120
+ def error_and_stack_trace(text)
121
+ text.strip[/Error:(.*)/m, 1].to_s
122
+ end
123
+
124
+ def updated_description(_issue, test)
125
+ new_issue_description(test)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module Report
8
+ # Uses the API to create or update GitLab test cases with the results of tests from RSpec report files.
9
+ class ResultsInTestCases < ReportAsIssue
10
+ include Concerns::ResultsReporter
11
+
12
+ attr_reader :issue_type, :gitlab
13
+
14
+ def initialize(**kwargs)
15
+ super
16
+ @issue_type = 'test_case'
17
+ end
18
+
19
+ def find_or_create_testcase(test)
20
+ find_testcase(test) || create_issue(test)
21
+ end
22
+
23
+ def add_issue_link_to_testcase(testcase, issue, test)
24
+ results_section = testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE) ? '' : TEST_CASE_RESULTS_SECTION_TEMPLATE
25
+
26
+ gitlab.edit_issue(iid: testcase.iid,
27
+ options: { description: (testcase.description + results_section + "\n\n#{issue.web_url}") })
28
+ # We are using test.testcase for the url here instead of testcase.web_url since it has the updated test case path
29
+ puts "Added results issue #{issue.web_url} link to test case #{test.testcase}"
30
+ end
31
+
32
+ def update_testcase(testcase, test)
33
+ puts "Test case labels updated." if update_labels(testcase, test)
34
+ puts "Test case quarantine section updated." if update_quarantine_link(testcase, test)
35
+ end
36
+
37
+ private
38
+
39
+ def find_testcase(test)
40
+ testcase = find_testcase_by_iid(test)
41
+
42
+ if testcase
43
+ testcase = update_issue_title(testcase, test) if issue_title_needs_updating?(testcase, test)
44
+ else
45
+ testcase = find_issue(test)
46
+ end
47
+
48
+ testcase
49
+ end
50
+
51
+ def find_testcase_by_iid(test)
52
+ iid = testcase_iid_from_url(test.testcase)
53
+
54
+ return unless iid
55
+
56
+ find_issue_by_iid(iid)
57
+ end
58
+
59
+ def testcase_iid_from_url(url)
60
+ return warn(%(\nPlease update #{url} to test case url")) if url&.include?('/-/issues/')
61
+
62
+ url && url.split('/').last.to_i
63
+ end
64
+
65
+ def new_issue_description(test)
66
+ quarantine_section = test.quarantine? && test.quarantine_issue ? "\n\n### Quarantine issue\n\n#{test.quarantine_issue}" : ''
67
+
68
+ "#{super}#{quarantine_section}\n\n#{execution_graph_section(test)}"
69
+ end
70
+
71
+ def execution_graph_section(test)
72
+ formatted_title = ERB::Util.url_encode(test.name)
73
+
74
+ <<~MKDOWN.strip
75
+ ### Executions
76
+
77
+ All Environments:
78
+ <img src="https://dashboards.quality.gitlab.net/render/d-solo/cW0UMgv7k/spec-health?orgId=1&var-run_type=All&var-name=#{formatted_title}&panelId=4&width=1000&height=500" />
79
+ MKDOWN
80
+ end
81
+
82
+ def updated_description(testcase, test)
83
+ historical_results_section = testcase.description.match(/### DO NOT EDIT BELOW THIS LINE[\s\S]+/)
84
+
85
+ "#{new_issue_description(test)}\n\n#{historical_results_section}"
86
+ end
87
+
88
+ def issue_title_needs_updating?(testcase, test)
89
+ super || (!testcase.description.include?(execution_graph_section(test)) && !%w[canary production preprod
90
+ release].include?(pipeline))
91
+ end
92
+
93
+ def quarantine_link_needs_updating?(testcase, test)
94
+ if test.quarantine? && test.quarantine_issue
95
+ return false if testcase.description.include?(test.quarantine_issue)
96
+ else
97
+ return false unless testcase.description.include?('Quarantine issue')
98
+ end
99
+
100
+ true
101
+ end
102
+
103
+ def update_quarantine_link(testcase, test)
104
+ return unless quarantine_link_needs_updating?(testcase, test)
105
+
106
+ new_description = updated_description(testcase, test)
107
+
108
+ gitlab.edit_issue(iid: testcase.iid, options: { description: new_description })
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'json'
5
+ require 'active_support/core_ext/object/blank'
6
+
7
+ module GitlabQuality
8
+ module TestTooling
9
+ module Report
10
+ class UpdateScreenshotPath
11
+ def initialize(junit_files:)
12
+ @junit_files = junit_files
13
+ end
14
+
15
+ REGEX = %r{(?<gitlab_qa_run>gitlab-qa-run-.*?(?=/))/(?<gitlab_ce_ee_qa>gitlab-(?:ee|ce)-qa-.*?(?=/))}
16
+ CONTAINER_PATH = File.join('/home', 'gitlab', 'qa', 'tmp').freeze
17
+
18
+ def invoke!
19
+ Dir.glob(junit_files).each do |junit_file|
20
+ match_data = junit_file.match(REGEX)
21
+ next unless match_data
22
+
23
+ host_relative_path = "#{match_data[:gitlab_qa_run]}/#{match_data[:gitlab_ce_ee_qa]}"
24
+
25
+ rewrite_schreenshot_paths_in_junit_file(junit_file, host_relative_path)
26
+ rewrite_schreenshot_paths_in_json_file(junit_file.gsub('.xml', '.json'), host_relative_path)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :junit_files
33
+
34
+ def rewrite_schreenshot_paths_in_junit_file(junit_file, host_relative_path)
35
+ File.write(
36
+ junit_file,
37
+ rewrite_each_junit_screenshot_path(junit_file, host_relative_path).to_s
38
+ )
39
+
40
+ puts "Saved #{junit_file}"
41
+ end
42
+
43
+ def rewrite_schreenshot_paths_in_json_file(json_file, host_relative_path)
44
+ File.write(
45
+ json_file,
46
+ JSON.pretty_generate(
47
+ rewrite_each_json_screenshot_path(json_file, host_relative_path)
48
+ )
49
+ )
50
+
51
+ puts "Saved #{json_file}"
52
+ end
53
+
54
+ def rewrite_each_junit_screenshot_path(junit_file, host_relative_path)
55
+ Nokogiri::XML(File.open(junit_file)).tap do |report|
56
+ report.xpath('//system-out').each do |system_out|
57
+ system_out.content = remove_container_absolute_path_prefix(system_out.content, host_relative_path)
58
+ end
59
+ end
60
+ end
61
+
62
+ def rewrite_each_json_screenshot_path(json_file, host_relative_path)
63
+ JSON.parse(File.read(json_file)).tap do |report|
64
+ examples = report['examples']
65
+
66
+ examples.each do |example|
67
+ next unless example['screenshot'].present?
68
+
69
+ example['screenshot']['image'] =
70
+ remove_container_absolute_path_prefix(example.dig('screenshot', 'image'), host_relative_path)
71
+ end
72
+ end
73
+ end
74
+
75
+ def remove_container_absolute_path_prefix(image_container_absolute_path, host_relative_path)
76
+ image_container_absolute_path.gsub(CONTAINER_PATH, host_relative_path)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'securerandom'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module Runtime
9
+ module Env
10
+ extend self
11
+ using Rainbow
12
+
13
+ ENV_VARIABLES = {
14
+ 'GITLAB_API_BASE' => :api_base,
15
+ 'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
16
+ 'GITLAB_CI_API_TOKEN' => :gitlab_ci_api_token,
17
+ 'CI_COMMIT_REF_NAME' => :ci_commit_ref_name,
18
+ 'CI_JOB_NAME' => :ci_job_name,
19
+ 'CI_JOB_URL' => :ci_job_url,
20
+ 'CI_PROJECT_ID' => :ci_project_id,
21
+ 'CI_PROJECT_NAME' => :ci_project_name,
22
+ 'CI_PIPELINE_ID' => :ci_pipeline_id,
23
+ 'CI_PIPELINE_URL' => :ci_pipeline_url,
24
+ 'CI_API_V4_URL' => :ci_api_v4_url,
25
+ 'SLACK_QA_CHANNEL' => :slack_qa_channel,
26
+ 'DEPLOY_VERSION' => :deploy_version
27
+ }.freeze
28
+
29
+ ENV_VARIABLES.each do |env_name, method_name|
30
+ define_method(method_name) do
31
+ env_var_value_if_defined(env_name) || (instance_variable_get("@#{method_name}") if instance_variable_defined?("@#{method_name}"))
32
+ end
33
+ end
34
+
35
+ def log_level
36
+ env_var_value_if_defined('QA_LOG_LEVEL')&.upcase || 'INFO'
37
+ end
38
+
39
+ def log_path
40
+ env_var_value_if_defined('QA_LOG_PATH') || host_artifacts_dir
41
+ end
42
+
43
+ def default_branch
44
+ env_var_value_if_defined('QA_DEFAULT_BRANCH') || 'main'
45
+ end
46
+
47
+ def gitlab_api_base
48
+ env_var_value_if_defined('GITLAB_API_BASE') || 'https://gitlab.com/api/v4'
49
+ end
50
+
51
+ def pipeline_from_project_name
52
+ ci_project_name.to_s.start_with?('gitlab') ? default_branch : ci_project_name
53
+ end
54
+
55
+ def run_id
56
+ @run_id ||= "gitlab-qa-run-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}-#{SecureRandom.hex(4)}"
57
+ end
58
+
59
+ def colorized_logs?
60
+ enabled?(ENV.fetch('COLORIZED_LOGS', nil), default: false)
61
+ end
62
+
63
+ def deploy_environment
64
+ env_var_value_if_defined('DEPLOY_ENVIRONMENT') || pipeline_from_project_name
65
+ end
66
+
67
+ def host_artifacts_dir
68
+ @host_artifacts_dir ||= File.join(
69
+ env_var_value_if_defined('QA_ARTIFACTS_DIR') || '/tmp/gitlab-qa', Runtime::Env.run_id
70
+ )
71
+ end
72
+
73
+ def qa_run_type
74
+ return env_var_value_if_defined('QA_RUN_TYPE') if env_var_value_valid?('QA_RUN_TYPE')
75
+
76
+ live_envs = %w[staging staging-canary staging-ref canary preprod production]
77
+ return unless live_envs.include?(ci_project_name)
78
+
79
+ test_subset = if env_var_value_if_defined('NO_ADMIN') == 'true'
80
+ 'sanity-no-admin'
81
+ elsif env_var_value_if_defined('SMOKE_ONLY') == 'true'
82
+ 'sanity'
83
+ else
84
+ 'full'
85
+ end
86
+
87
+ "#{ci_project_name}-#{test_subset}"
88
+ end
89
+
90
+ private
91
+
92
+ def enabled?(value, default: true)
93
+ return default if value.nil?
94
+
95
+ (value =~ /^(false|no|0)$/i) != 0
96
+ end
97
+
98
+ def env_var_value_valid?(variable)
99
+ !ENV[variable].blank?
100
+ end
101
+
102
+ def env_var_value_if_defined(variable)
103
+ return ENV.fetch(variable) if env_var_value_valid?(variable)
104
+ end
105
+
106
+ def env_var_name_if_defined(variable)
107
+ # Pass through the variables if they are defined and not empty in the environment
108
+ return "$#{variable}" if env_var_value_valid?(variable)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'fileutils'
5
+ require 'rainbow'
6
+ require 'active_support/logger'
7
+
8
+ module GitlabQuality
9
+ module TestTooling
10
+ module Runtime
11
+ class Logger
12
+ extend SingleForwardable
13
+
14
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
15
+
16
+ TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
17
+ LEVEL_COLORS = {
18
+ "DEBUG" => :magenta,
19
+ "INFO" => :green,
20
+ "WARN" => :yellow,
21
+ "ERROR" => :red,
22
+ "FATAL" => :indianred
23
+ }.freeze
24
+
25
+ Rainbow.enabled = Runtime::Env.colorized_logs?
26
+
27
+ class << self
28
+ # Combined logger instance
29
+ #
30
+ # @param [String] source
31
+ # @return [ActiveSupport::Logger]
32
+ def logger(source: 'Gitlab QA')
33
+ @logger ||= begin
34
+ log_path = Env.log_path
35
+ ::FileUtils.mkdir_p(log_path)
36
+
37
+ console_log = console_logger(source: source, level: Env.log_level)
38
+ file_log = file_logger(source: source, path: log_path)
39
+
40
+ console_log.extend(ActiveSupport::Logger.broadcast(file_log))
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Console logger instance
47
+ #
48
+ # @param [String] source
49
+ # @param [<Symbol, String>] level
50
+ # @return [ActiveSupport::Logger]
51
+ def console_logger(source:, level:)
52
+ ActiveSupport::Logger.new($stdout, level: level, datetime_format: TIME_FORMAT).tap do |logger|
53
+ logger.formatter = proc do |severity, datetime, _progname, msg|
54
+ msg_prefix = message_prefix(datetime, source, severity)
55
+
56
+ Rainbow(msg_prefix).public_send(LEVEL_COLORS.fetch(severity, :silver)) + "#{msg}\n" # rubocop:disable GitlabSecurity/PublicSend
57
+ end
58
+ end
59
+ end
60
+
61
+ # File logger
62
+ #
63
+ # @param [String] source
64
+ # @param [String] path
65
+ # @return [ActiveSupport::Logger]
66
+ def file_logger(source:, path:)
67
+ log_file = "#{path}/#{source.downcase.tr(' ', '-')}.log"
68
+
69
+ ActiveSupport::Logger.new(log_file, level: :debug, datetime_format: TIME_FORMAT).tap do |logger|
70
+ logger.formatter = proc do |severity, datetime, _progname, msg|
71
+ msg_prefix = message_prefix(datetime, source, severity)
72
+
73
+ "#{msg_prefix}#{msg}\n".gsub(/\e\[(\d+)(?:;\d+)*m/, "")
74
+ end
75
+ end
76
+ end
77
+
78
+ # Log message prefix
79
+ #
80
+ # @note when outputted, the date will be formatted as "Jun 07 2022 11:30:00 UTC"
81
+ # @param [DateTime] date
82
+ # @param [String] source
83
+ # @param [String] severity
84
+ # @return [String]
85
+ def message_prefix(date, source, severity)
86
+ "[#{date.strftime('%h %d %Y %H:%M:%S %Z')} (#{source})] #{severity.ljust(5)} -- "
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Runtime
6
+ class TokenFinder
7
+ def self.find_token!(token, suffix: nil)
8
+ new(token, suffix).find_token!
9
+ end
10
+
11
+ attr_reader :token, :suffix
12
+
13
+ def initialize(token, suffix)
14
+ @token = token
15
+ @suffix = suffix
16
+ end
17
+
18
+ def find_token!
19
+ find_token_from_attrs || find_token_from_env || find_token_from_file
20
+ end
21
+
22
+ def find_token_from_attrs
23
+ token
24
+ end
25
+
26
+ def find_token_from_env
27
+ Env.gitlab_ci_api_token
28
+ end
29
+
30
+ def find_token_from_file
31
+ @token_from_file ||= File.read(token_file_path).strip
32
+ rescue Errno::ENOENT
33
+ fail "Please provide a valid access token with the `-t/--token` option, the `GITLAB_CI_API_TOKEN` environment variable, or in the `#{token_file_path}` file!"
34
+ end
35
+
36
+ private
37
+
38
+ def token_file_path
39
+ @token_file_path ||= File.expand_path("../api_token#{"_#{suffix}" if suffix}", __dir__)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Slack
6
+ class PostToSlack
7
+ def initialize(slack_webhook_url:, channel:, message:, username:, icon_emoji:)
8
+ @slack_webhook_url = slack_webhook_url
9
+ @channel = channel
10
+ @message = message
11
+ @username = username
12
+ @icon_emoji = icon_emoji
13
+ end
14
+
15
+ def invoke!
16
+ params = {}
17
+ params['channel'] = channel
18
+ params['username'] = username
19
+ params['icon_emoji'] = icon_emoji
20
+ params['text'] = message
21
+
22
+ Support::HttpRequest.make_http_request(
23
+ method: 'post',
24
+ url: slack_webhook_url,
25
+ params: params,
26
+ show_response: true
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :slack_webhook_url, :channel, :message, :username, :icon_emoji
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'table_print'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ class SummaryTable
9
+ def self.create(input_files:)
10
+ "```\n#{TablePrint::Printer.table_print(collect_results(input_files))}```\n"
11
+ end
12
+
13
+ # rubocop:disable Metrics/AbcSize
14
+ def self.collect_results(input_files)
15
+ stage_wise_results = []
16
+
17
+ Dir.glob(input_files).each do |report_file|
18
+ stage_hash = {}
19
+ stage_hash["Dev Stage"] = File.basename(report_file, ".*").capitalize
20
+
21
+ report_stats = Nokogiri::XML(File.open(report_file)).children[0].attributes
22
+
23
+ stage_hash["Total"] = report_stats["tests"].value
24
+ stage_hash["Failures"] = report_stats["failures"].value
25
+ stage_hash["Errors"] = report_stats["errors"].value
26
+ stage_hash["Skipped"] = report_stats["skipped"].value
27
+ stage_hash["Result"] = result_emoji(report_stats)
28
+
29
+ stage_wise_results << stage_hash
30
+ end
31
+
32
+ stage_wise_results
33
+ end
34
+ # rubocop:enable Metrics/AbcSize
35
+
36
+ def self.result_emoji(report_stats)
37
+ report_stats["failures"].value.to_i.positive? || report_stats["errors"].value.to_i.positive? ? "❌" : "✅"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http'
4
+ require 'json'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module Support
9
+ class HttpRequest
10
+ # rubocop:disable Metrics/AbcSize
11
+ def self.make_http_request(method: 'get', url: nil, params: {}, headers: {}, show_response: false, fail_on_error: true)
12
+ raise "URL not defined for making request. Exiting..." unless url
13
+
14
+ res = HTTP.follow.method(method).call(url, json: params, headers: headers)
15
+
16
+ if show_response
17
+ if res.content_type.mime_type == "application/json"
18
+ res_body = JSON.parse(res.body.to_s)
19
+ pp res_body
20
+ else
21
+ res_body = res.body.to_s
22
+ puts res_body
23
+ end
24
+ end
25
+
26
+ raise "#{method.upcase} request failed!\nCode: #{res.code}\nResponse: #{res.body}\n" if fail_on_error && !res.status.success?
27
+
28
+ res
29
+ end
30
+ # rubocop:enable Metrics/AbcSize
31
+ end
32
+ end
33
+ end
34
+ end