gitlab_quality-test_tooling 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -4
- data/Gemfile.lock +1 -1
- data/Guardfile +0 -22
- data/exe/generate-test-session +49 -0
- data/exe/post-to-slack +57 -0
- data/exe/prepare-stage-reports +38 -0
- data/exe/relate-failure-issue +59 -0
- data/exe/report-results +56 -0
- data/exe/update-screenshot-paths +38 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +194 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +26 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +51 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +75 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +275 -0
- data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +79 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +377 -0
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +134 -0
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +130 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_testcases.rb +113 -0
- data/lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb +81 -0
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +113 -0
- data/lib/gitlab_quality/test_tooling/runtime/logger.rb +92 -0
- data/lib/gitlab_quality/test_tooling/runtime/token_finder.rb +44 -0
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +36 -0
- data/lib/gitlab_quality/test_tooling/summary_table.rb +41 -0
- data/lib/gitlab_quality/test_tooling/support/http_request.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb +65 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/log.rb +38 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/api_log.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/application_log.rb +27 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/exception_log.rb +23 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb +30 -0
- data/lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb +29 -0
- data/lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb +65 -0
- data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +39 -0
- data/lib/gitlab_quality/test_tooling/test_results/builder.rb +35 -0
- data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +27 -0
- data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +29 -0
- data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +184 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +11 -2
- 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
|