gitlab_quality-test_tooling 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) 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/exe/generate-test-session +49 -0
  6. data/exe/post-to-slack +57 -0
  7. data/exe/prepare-stage-reports +38 -0
  8. data/exe/relate-failure-issue +59 -0
  9. data/exe/report-results +56 -0
  10. data/exe/update-screenshot-paths +38 -0
  11. data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +194 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +26 -0
  13. data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +51 -0
  14. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +75 -0
  15. data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +49 -0
  16. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +275 -0
  17. data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +79 -0
  18. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +377 -0
  19. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +134 -0
  20. data/lib/gitlab_quality/test_tooling/report/report_results.rb +83 -0
  21. data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +130 -0
  22. data/lib/gitlab_quality/test_tooling/report/results_in_testcases.rb +113 -0
  23. data/lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb +81 -0
  24. data/lib/gitlab_quality/test_tooling/runtime/env.rb +113 -0
  25. data/lib/gitlab_quality/test_tooling/runtime/logger.rb +92 -0
  26. data/lib/gitlab_quality/test_tooling/runtime/token_finder.rb +44 -0
  27. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +36 -0
  28. data/lib/gitlab_quality/test_tooling/summary_table.rb +41 -0
  29. data/lib/gitlab_quality/test_tooling/support/http_request.rb +34 -0
  30. data/lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb +65 -0
  31. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +21 -0
  32. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +21 -0
  33. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +21 -0
  34. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +21 -0
  35. data/lib/gitlab_quality/test_tooling/system_logs/log_types/log.rb +38 -0
  36. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/api_log.rb +34 -0
  37. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/application_log.rb +27 -0
  38. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/exception_log.rb +23 -0
  39. data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb +30 -0
  40. data/lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb +29 -0
  41. data/lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb +65 -0
  42. data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +39 -0
  43. data/lib/gitlab_quality/test_tooling/test_results/builder.rb +35 -0
  44. data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +27 -0
  45. data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +29 -0
  46. data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +184 -0
  47. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  48. data/lib/gitlab_quality/test_tooling.rb +11 -2
  49. 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